TIL - 26.01.21

이준연·2026년 1월 21일

학습 키워드


  • Filter 개념과 동작 원리
  • JWT 이해

Filter


Filter란?

Spring 로직이 실행되기 전에 사용자의 요청을 한번 걸러주는 역할을 합니다.

Spring 흐름도

  • Client → Request → Filter → Controller → Filter → Response → Client
  • 모든 요청은 Controller에 들어가기 전에 Filter를 통과하고, 반환될 때도 Filter를 통과하여 반환됩니다.

Filter 구조

Spring에서는 Filter를 직접 구현할 필요 없이 많이 사용되는 목적에 따라 이미 구현이 되어 있습니다. 우리는 그 중 하나를 선택하여 사용하면되는데 OncePerRequestFilter 를 제외하고는 거의 사용할 일이 별로 없습니다.

  • OncePerRequestFilter

    • 요청 당 한 번의 필터만 실행되도록 보장
    • 인증/인가, 로깅, 트랜잭션 관리 같은 곳에서 자주 사용
  • CharacterEncodingFilter

    • 모든 요청/응답에 지정한 인코딩(URF-8 등)을 적용
  • HiddenHttpMethodFilter

    • HTML form은 GET, POST 만을 지원 → _method 파라미터로 PUT, DELETE 등 매핑 가능하게 함
  • FormContentFilter

    • application/x-www-form-urlencoded 데이터를 PUT, PATCH, DELETE 요청에서도 읽을 수 있게 함
  • RequestContextFilter

    • 현재 요청을 RequestContextHolder 에 바인딩
    • RequestScope Bean 사용 시 필요
  • ETC...

💡 옛날에는 서버에 Front, Back이 공존했는데 이때 리다이렉트가 발생해서 Filter가 두 번 호출되는 경우가 있었습니다.

OncePerRequestFilter의 한 번의 필터 실행 보장 기능이 존재하는 이유는 이 때문입니다. 현재는 REST API를 사용하고 있어 리다이렉트가 발생하는 경우는 거의 존재하지 않습니다.

Filter 구현

@Component
public class Filter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
        throws ServletException, IOException {

        // 요청이 들어갈 때 실행되는 부분
        System.out.println("Filter로 들어간다 ");

        // 필터 계속 진행
        filterChain.doFilter(request, response);

        // 요청이 나갈 때 실행되는 부분
        System.out.println("Filter로 나간다 ");
    }
}

filterChain.doFilter(request, response); 를 기준으로 필터를 통과해서 들어올 때와 나갈 때를 구분 짓습니다.

FilterChain 활용

FilterChain이라는 Filter의 묶음이 있고 필요하면 Filter를 하나씩 추가하여 사용할 수 있습니다.

@Component
@Order(1)
public class Filter1 extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
        throws ServletException, IOException {

        // 요청이 들어갈 때 실행되는 부분
        System.out.println("Filter1로 들어간다 ");

        // 필터 계속 진행
        filterChain.doFilter(request, response);

        // 요청이 나갈 때 실행되는 부분
        System.out.println("Filter1로 나간다 ");
    }
}
@Component
@Order(2)
public class Filter2 extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
        throws ServletException, IOException {

        // 요청이 들어갈 때 실행되는 부분
        System.out.println("Filter2로 들어간다 ");

        // 필터 계속 진행
        filterChain.doFilter(request, response);

        // 요청이 나갈 때 실행되는 부분
        System.out.println("Filter2로 나간다 ");
    }
}

@Order 어노테이션을 사용하여 Filter의 순서를 지정할 수 있습니다.

JWT


JWT란?

JWT는 출입증과 같습니다.

JWT 구성 요소

  1. Header
    • 암호화에서 사용된 알고리즘
  2. Payload
    • 암호화된 값
  3. Signature
    • 암호화 할 때 사용한 Key 값

JWT 필요사항

  • 암호화 알고리즘(HS256)
  • 정보(name, age, email, role)
  • 암호화키(Secret Key)
  • 만료 시간
    • Token 탈취를 예상한 최소한의 안전장치

JWT 흐름

토큰을 발행하는 경우

  • Client → 로그인 요청 → Server → 로그인 시도 → 로그인 성공 → 토큰 발행 → Client

토큰을 검사하는 경우

  • Client → API 요청 → Server → 토큰 유무 확인 → 유효성 검증 → 복호화 → Client

JWT 실습

의존성

implementation "io.jsonwebtoken:jjwt-api:0.12.5"
runtimeOnly  "io.jsonwebtoken:jjwt-impl:0.12.5"
runtimeOnly  "io.jsonwebtoken:jjwt-jackson:0.12.5" // JSON 직렬화

비밀키

// 터미널에 openssl rand -base64 64 입력 시 비밀 키 생성
jwt.secret.key = 0ydHRc8EG9P/RJDjjPbE8dezJ0IXc8z61Y7hDZRJKkkrDW81g9FPKb0XdU5Knun3kIBEabqtzGnsNSqbufv79g==

JwtUtil

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.JwtParser;
import io.jsonwebtoken.Jwts;

import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import jakarta.annotation.PostConstruct;

import java.util.Date;
import javax.crypto.SecretKey;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;


@Component
public class JwtUtil {

    public static final String BEARER_PREFIX = "Bearer ";
    private static final long TOKEN_TIME = 60 * 60 * 1000L; // 60분

    @Value("${jwt.secret.key}") // application 에 있는 key 가져오기
    private String secretKeyString;

    private SecretKey key;
    private JwtParser parser;

    // @PostConstruct 어플리케이션 실행 될 때 가장 먼저 실행 되게 하는 어노테이션
    @PostConstruct
    public void init() {
        byte[] bytes = Decoders.BASE64.decode(secretKeyString);
        this.key = Keys.hmacShaKeyFor(bytes);
        this.parser = Jwts.parser()
            .verifyWith(this.key)
            .build();
    }


    // 토큰 생성
    public String generateToken(String username) {
        Date now = new Date();
        return BEARER_PREFIX + Jwts.builder()
            .claim("username", username)
            .issuedAt(now)
            .expiration(new Date(now.getTime() + TOKEN_TIME))
            .signWith(key, Jwts.SIG.HS256)
            .compact();
    }

    // 토큰 검증
    public boolean validateToken(String token) {
        if (token == null || token.isBlank()) return false;
        try {
            parser.parseSignedClaims(token);
            return true;
        } catch (JwtException | IllegalArgumentException e) {
            // 개별 예외 분리 없음: 서명/형식/만료 등 모든 실패를 한 번에 처리
            log.debug("Invalid JWT: {}", e.toString());
            return false;
        }
    }

	// 토큰 복호화
    private Claims extractAllClaims(String token) {
        return parser.parseSignedClaims(token).getPayload();
    }

    public String extractUsername(String token) {
        return extractAllClaims(token).get("username", String.class);
    }
}
profile
반갑습니다!

0개의 댓글