🔌 SPARTA/Courses

JWT와 Filter

eunjiom 2026. 3. 3. 12:30

Filter

1. Filter

 

1) 스프링 흐름도

2) OncePerRequestFilter

  • 요청 당 한 번만 실행되도록 보장
  • ex) 인증/인가, 로깅, 트랜잭션 관리
  • 가장 많이 사용됨

3) When

  • 로깅(요청/응답 로깅): 로그를 필터에만 남겨둠
  • 공통 인증/권한 체크(JWT 등): 컨트롤러 전 필터로 인증 실패 걸러냄

2. FilterChain 활용

 

1) 코드

@Component
public class NbcamFilter extends OncePerRequestFilter {

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

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

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

        // 요청이 나갈 때 실행되는 부분
        System.out.println("✅ NbcamFilter로 나간다 ");
    }
}
  • OncePerRequestFilter 사용

2) FilterChain

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

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

        // 요청이 나갈 때 실행되는 부분
        System.out.println("✅ NbcamFilter로 나간다 ");
  • 요청 들어갈 때 실행되는 부분 > FilterChain 코드(구분기준) > 요청 나갈 때 실행되는 부분
  • Controller 앞에 실행
  • FilterChain: 필터 묶음 단위 / 필터들을 여러개 만들어 진행 가능

3) 필터 순서 지정

  • 필터 순서 지정하는 방법: @Order 어노테이션 통해 지정
  • 요청 들어올 때: @Order(1) -> @Order(2)
  • 요청 나갈 때: @Order(2) -> Ordef(1)
  • 순서: 요청 들어옴 > 필터1 > 필터2 > 컨트롤러 > 필터2 > 필터1

JWT 이해

 

1. JWT

 

1) 구성

  • HEADER(암호화된 알고리즘, 정보)
  • PAYLOAD(회원정보)
  • SIGNATURE(암호화된 인증키)

 

2) 사용할 때 필요한 것

  • 어떤 암호화 알고리즘을 사용할 것인지 ex) HS256
  • 어떤 정보를 넣을 것인지 ex) name, role
  • 어떤 암호화키로 암호화 할 것인지 ex) Secret Key
  • 만료시간: 만료시간이 없으면 보안에 취약함

2. JWT 흐름

 

1) 토큰을 발행해주는 경우

  • 사용자 입장: 인증/인가를 위해 로그인
  • 서버 입장: 사용자가 보낸 정보 기반으로 로그인 시도, 로그인 성공하면 JWT 토큰 발행

2) 토큰을 검사하는 경우

  • 사용자 입장: postman을 통해 요청 보냄, JWT 토큰 보내고 싶으면 헤더에 포함해서 보냄
  • 서버 입장: JWT 토큰 있는지 검사, 유효한지 검사, 유효하면 JWT 토큰 안에 있는 값을 복호화

3. JWT 실습

  • 기본 셋팅
더보기
더보기
더보기
  • 프로젝트 구성
  • build.gradle 추가
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 직렬화
  • application.yml
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/nbcam
    username: root
    password: 1234
    driver-class-name: com.mysql.cj.jdbc.Driver
  jpa:
    show-sql: true
    hibernate:
      ddl-auto: create-drop
    properties:
      hibernate:
        format_sql: true
    defer-datasource-initialization: true

## 암호화에 사용할 비밀키 
jwt:
  secret:
    key: IfthisgetsstolenitsabigproblemIfthelengthistooshortthesecurityisnotsufficientsoanerroroccurs
  • JWT
더보기
더보기
더보기
  • JwtUtil
package org.example.nbcam_addvanced_1.common.utils;

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.yml 에 있는 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);
    }


}
  • UserController
package org.example.nbcam_addvanced_1.user.controller;

import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.example.nbcam_addvanced_1.common.utils.JwtUtil;
import org.example.nbcam_addvanced_1.user.model.response.LoginResponseDto;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/user")
@RequiredArgsConstructor
@Slf4j
public class UserController {

    private final JwtUtil jwtUtil;

    @GetMapping("/get")
    public String getUserInfo() {
        log.info("호출");
        return "호출 되었습니다.";
    }

    // 토큰 생성 테스트
    @PostMapping("/login")
    public ResponseEntity<LoginResponseDto> login() {

        String token  = jwtUtil.generateToken("kim-dong-hyun");

        return ResponseEntity.ok(new LoginResponseDto(token));
    }

    // 토큰 검증 테스트
    @GetMapping("/validate")
    public ResponseEntity<Boolean> checkValidate(HttpServletRequest request) {

        String authorizationHeader = request.getHeader("Authorization");

        String jwt = authorizationHeader.substring(7);

        Boolean validate = jwtUtil.validateToken(jwt);

        return ResponseEntity.ok(validate);
    }

    // 토큰 복호화 테스트
    @GetMapping("/username")
    public ResponseEntity<String> getUsername(HttpServletRequest request) {

        String authorizationHeader = request.getHeader("Authorization");

        String jwt = authorizationHeader.substring(7);

        String username = jwtUtil.extractUsername(jwt);

        return ResponseEntity.ok(username);
    }

}

 

Filter와 같이 사용하는 JWT

 

1. JwtFilter

package org.example.nbcam_addvanced_1.common.filter;

import static org.example.nbcam_addvanced_1.common.utils.JwtUtil.BEARER_PREFIX;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.example.nbcam_addvanced_1.common.utils.JwtUtil;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

@Component
@Slf4j
@RequiredArgsConstructor
public class JwtFilter extends OncePerRequestFilter {

    private final JwtUtil jwtUtil;

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

        // JWT 검증이 필요 없는 경우 조 ex) 로그인
        String requestURI = request.getRequestURI();

        if(requestURI.equals("/api/user/login")) {
            filterChain.doFilter(request,response);
            return;
        }

        // JWT 토큰이 있는지 없는지 검사

        String authorizationHeader = request.getHeader("Authorization");


        if (authorizationHeader == null || !authorizationHeader.startsWith(BEARER_PREFIX)) {
            log.info("JWT 토큰이 필요 합니다.");
            response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "JWT 토큰이 필요 합니다.");
            return;
        }


        // JWT 토큰이 있다면 유효한 토큰인지 검사

        String jwt = authorizationHeader.substring(7);

        if (!jwtUtil.validateToken(jwt)) {
            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
            response.getWriter().write("{\"error\": \"Unauthorized\"}");
        }

        // JWT 토큰에서 복호화 한 데이터 저장하기

        String username = jwtUtil.extractUsername(jwt);

        request.setAttribute("username", username);

        filterChain.doFilter(request, response);


    }
}

 

2. HttpServletRequest

 

1) 흐름

  • Postman 요청을 보낼 때 응답 부분은 비어 있고 API 통신이 완료되면 채워지는 것처럼 1번의 요청당 1개의 servlet이 위의 그림처럼의 흐름을 돌면서 빈칸을 채워 오는 것

 

2) attribute

  • Postman 기준 윗쪽 request에 있는 모든 정보를 담고 있고 그것 이외로 Map 타입의 attribute 라는 저장공간이 있어서 attribute 에 JWT 에서 복호화한 정보를 담아서 Controller로 넘겨주는 것