๐Ÿ–ฅ๏ธ EvoStyleํ”„๋กœ์ ํŠธ/๐Ÿ› ๏ธ ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ…

[ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ…] Spring Boot JWT ์ธ์ฆ ํ๋ฆ„ ๋งŒ๋“ค๊ณ  200 OK๋งŒ ๋‚˜์˜ค๋Š” ๋ฌธ์ œ ํ•ด๊ฒฐ

carrot0911 2025. 5. 13. 11:10

๐Ÿ”ฅ  JWT ์ธ์ฆ ๊ตฌํ˜„ & ํ•„ํ„ฐ ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ…

 

ํ”„๋กœ์ ํŠธ์—์„œ JWT ์ธ์ฆ์„ ์ฒ˜์Œ์œผ๋กœ ์ง์ ‘ ๊ตฌํ˜„ํ•ด ๋ดค๋‹ค.
ํšŒ์›๊ฐ€์ž…๊ณผ ๋กœ๊ทธ์ธ๊นŒ์ง€๋Š” ์ •์ƒ์ ์œผ๋กœ ๋™์ž‘ํ–ˆ์ง€๋งŒ.. ๋ฌธ์ œ๋Š” ๊ทธ ์ดํ›„์˜€๋‹ค.
Controller๋กœ ์š”์ฒญ์ด ๋„˜์–ด๊ฐ€์ง€ ์•Š๊ณ , 200 OK๋งŒ ๋‚˜์˜ค๋Š” ํ˜„์ƒ์ด ๋ฐœ์ƒํ–ˆ๋‹ค.
๋ฌธ์ œ๋ฅผ ์ฐพ์ง€ ๋ชปํ•ด์„œ ํ•œ์ฐธ์„ ํ—ค๋งค๋‹ค๊ฐ€, ๊ฒฐ๊ตญ ์ฒ˜์Œ๋ถ€ํ„ฐ ์ฝ”๋“œ๋ฅผ ๋‹ค์‹œ ์ •๋ฆฌํ•˜๋ฉด์„œ ํ•ด๊ฒฐํ–ˆ๋‹ค.

 

โœ๏ธ  JwtUtil ํด๋ž˜์Šค

JWT๋ฅผ ๋ฐœ๊ธ‰ํ•˜๊ณ , ๊ฒ€์ฆํ•˜๊ณ , Claim ์ •๋ณด๋ฅผ ์ถ”์ถœํ•˜๋Š” ์—ญํ• ์„ ๋‹ด๋‹นํ•˜๋Š” ํด๋ž˜์Šค์ด๋‹ค.

@Slf4j(topic = "JwtUtil")
@Component
@RequiredArgsConstructor
public class JwtUtil {
    private static final String BEARER_PREFIX = "Bearer ";
    private static final long ACCESS_TOKEN_EXPIRATION = 30 * 60 * 1000;  // 30๋ถ„

    @Value("${jwt.secret.key}")
    private String secretKey;

    private Key key;
    private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

    // ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์‹œ์ž‘ ์‹œ SecretKey ์ดˆ๊ธฐํ™”
    @PostConstruct
    public void init() {
        if (!StringUtils.hasText(secretKey)) {
            log.error("JWT secret key is null or empty.");
            throw new BadRequestException(ErrorCode.MISSING_JWT_SECRET_KEY);
        }
        try {
            byte[] bytes = Base64.getDecoder().decode(secretKey);
            key = Keys.hmacShaKeyFor(bytes);
            log.info("JWT secret key initialized successfully");
        } catch (IllegalArgumentException e) {
            log.error("Failed to decode JWT secret key: {}", e.getMessage());
            throw new InternalServerException(ErrorCode.INVALID_JWT_SECRET_KEY);
        }
    }

    // ๐Ÿ”น AccessToken ์ƒ์„ฑ
    public String createToken(Long memberId, String email, String nickname, Authority authority) {
        Date now = new Date();

        return BEARER_PREFIX + Jwts.builder()
            .setSubject(String.valueOf(memberId))
            .claim("email", email)
            .claim("nickname", nickname)
            .claim("authority", authority)
            .setIssuedAt(now)
            .setExpiration(new Date(now.getTime() + ACCESS_TOKEN_EXPIRATION))
            .signWith(key, signatureAlgorithm)
            .compact();
    }

    // ๐Ÿ”น ํ† ํฐ ์œ ํšจ์„ฑ ๊ฒ€์ฆ
    public boolean validateToken(String token) {
        try {
            token = removeBearer(token);
            Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
            return true;
        } catch (Exception e) {
            log.error("Invalid JWT token: {}", e.getMessage());
            return false;
        }
    }

    // ๐Ÿ”น ํ† ํฐ Claim ํŒŒ์‹ฑ
    public Claims parseClaims(String token) {
        try {
            return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
        } catch (ExpiredJwtException e) {
            throw new UnauthorizedException(ErrorCode.EXPIRED_JWT_TOKEN);
        }
    }

    // ๐Ÿ”น ํ† ํฐ์—์„œ ์ •๋ณด ์ถ”์ถœ
    public Long getMemberId(String token) {
        return Long.valueOf(parseClaims(removeBearer(token)).getSubject());
    }

    public String getEmail(String token) {
        return parseClaims(removeBearer(token)).get("email", String.class);
    }

    public String getAuthority(String token) {
        return parseClaims(removeBearer(token)).get("authority", String.class);
    }

    // ๐Ÿ”น "Bearer " ์ ‘๋‘์–ด ์ œ๊ฑฐ
    private String removeBearer(String token) {
        if (token != null && token.startsWith(BEARER_PREFIX)) {
            return token.substring(BEARER_PREFIX.length());
        }
        return token;
    }
}

 

โœ๏ธ  JwtFilter ํด๋ž˜์Šค

๋ชจ๋“  ์š”์ฒญ์„ ๊ฐ€๋กœ์ฑ„์„œ ํ† ํฐ ๊ฒ€์ฆ์„ ์ˆ˜ํ–‰ํ•˜๋Š” ํ•„ํ„ฐ์ด๋‹ค.

@Slf4j(topic = "JwtFilter")
@RequiredArgsConstructor
public class JwtFilter implements Filter {

    private final JwtUtil jwtUtil;

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
        throws IOException, ServletException {

        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        String requestURI = httpRequest.getRequestURI();

        // ํšŒ์›๊ฐ€์ž…, ๋กœ๊ทธ์ธ์€ ์ธ์ฆ์—†์ด ํ†ต๊ณผ
        if (requestURI.startsWith("/api/auth/")) {
            chain.doFilter(httpRequest, httpResponse);
            return;
        }

        String bearerJwt = httpRequest.getHeader("Authorization");

        if (bearerJwt == null || !bearerJwt.startsWith("Bearer ")) {
            log.error("Invalid Authorization header on URI: {}", requestURI);
            throw new UnauthorizedException(ErrorCode.INVALID_BEARER_TOKEN);
        }

        String token = bearerJwt.substring(7);

        if (!jwtUtil.validateToken(token)) {
            throw new UnauthorizedException(ErrorCode.INVALID_JWT_TOKEN);
        }

        // ํ† ํฐ์—์„œ ์ •๋ณด ์ถ”์ถœ
        Long memberId = jwtUtil.getMemberId(token);
        String email = jwtUtil.getEmail(token);
        String authority = jwtUtil.getAuthority(token);

        // ์š”์ฒญ์— ์‚ฌ์šฉ์ž ์ •๋ณด ์ €์žฅ
        httpRequest.setAttribute("memberId", memberId);
        httpRequest.setAttribute("email", email);
        httpRequest.setAttribute("authority", authority);

        chain.doFilter(httpRequest, httpResponse);
    }
}

 

โœ  FilterConfig ํด๋ž˜์Šค

JwtFilter๋ฅผ ์Šคํ”„๋ง์— ๋“ฑ๋กํ•˜๋Š” ์„ค์ • ํด๋ž˜์Šค์ด๋‹ค.

@Configuration
@RequiredArgsConstructor
public class FilterConfig {

    private final JwtUtil jwtUtil;

    @Bean
    public FilterRegistrationBean<JwtFilter> jwtFilter() {
        FilterRegistrationBean<JwtFilter> registrationBean = new FilterRegistrationBean<>();

        registrationBean.setFilter(new JwtFilter(jwtUtil));
        registrationBean.addUrlPatterns("/api/*");  // /api/ ํ•˜์œ„ ๊ฒฝ๋กœ์—๋งŒ ์ ์šฉ
        registrationBean.setOrder(1);  // ํ•„ํ„ฐ ์ˆœ์„œ ์ง€์ •

        return registrationBean;
    }
}

 

โšก  ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ… ๊ณผ์ • ์ •๋ฆฌ

1. ๋ฌธ์ œ ์ƒํ™ฉ

๐Ÿ‘‰  JWT๋ฅผ ์ ์šฉํ–ˆ๋Š”๋ฐ ํšŒ์›๊ฐ€์ž…/๋กœ๊ทธ์ธ์€ ์ •์ƒ์ ์œผ๋กœ ์ฒ˜๋ฆฌ๋˜์—ˆ๋‹ค.
๐Ÿ‘‰  ํ•˜์ง€๋งŒ ์ดํ›„ ์š”์ฒญ์ด Controller๋กœ ๋„˜์–ด๊ฐ€์ง€ ์•Š๊ณ , 200 OK ์‘๋‹ต๋งŒ ๋œจ๋Š” ํ˜„์ƒ์ด ๋ฐœ์ƒํ–ˆ๋‹ค.

2. ๋ฌธ์ œ ์›์ธ

๐Ÿ‘‰  ํ•„ํ„ฐ(JwtFilter) ์•ˆ์—์„œ ์˜ˆ์™ธ๋ฅผ ๋ฐœ์ƒ์‹œํ‚ค๋Š” ๊ฒฝ์šฐ ๋ณ„๋‹ค๋ฅธ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๋ฅผ ํ•ด์ฃผ์ง€ ์•Š์œผ๋ฉด ๊ทธ๋ƒฅ "200 OK"๋กœ ๋๋‚˜๋ฒ„๋ฆด ์ˆ˜ ์žˆ๋‹ค.
๐Ÿ‘‰  ํŠนํžˆ throw ์ดํ›„ ๋ณ„๋„๋กœ response๋ฅผ ์ž‘์„ฑํ•˜๊ฑฐ๋‚˜ ์˜ˆ์™ธ๋ฅผ ํ•ธ๋“ค๋งํ•˜์ง€ ์•Š์œผ๋ฉด, ํด๋ผ์ด์–ธํŠธ๋Š” ์•„๋ฌด๊ฒƒ๋„ ์—†๋Š” 200 OK๋ฅผ ๋ฐ›๊ฒŒ ๋œ๋‹ค.

3. ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•

๐Ÿ‘‰  ๋ฌธ์ œ๋ฅผ ์ •ํ™•ํžˆ ํŒŒ์•…ํ•˜๊ธฐ ์œ„ํ•ด ์ฝ”๋“œ๋ฅผ ์ฐจ๊ทผ์ฐจ๊ทผ ๋‹ค์‹œ ์ž‘์„ฑํ–ˆ๋‹ค.
๐Ÿ‘‰  ๊ทธ๋ฆฌ๊ณ  ํ•„ํ„ฐ ์•ˆ์—์„œ ๋ฐœ์ƒํ•˜๋Š” ์˜ˆ์™ธ๋ฅผ Spring ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ํ๋ฆ„์— ์ œ๋Œ€๋กœ ํƒœ์›Œ์„œ ControllerAdvice ๊ฐ™์€ ๊ธ€๋กœ๋ฒŒ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๊ฐ€ ์žก๋„๋ก ์„ค๊ณ„ํ–ˆ๋‹ค.

4. ๊ตํ›ˆ

๐Ÿ‘‰  "์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ–ˆ์œผ๋ฉด ๋ช…ํ™•ํžˆ ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ์—๋Ÿฌ๋ฅผ ์•Œ๋ ค์•ผ ํ•œ๋‹ค."
๐Ÿ‘‰  ํ•„ํ„ฐ๋‚˜ ์ธํ„ฐ์…‰ํ„ฐ ๊ฐ™์€ "์š”์ฒญ ํ๋ฆ„์„ ์ œ์–ดํ•˜๋Š” ๋ ˆ์ด์–ด"์—์„œ๋Š” ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๋ฅผ ๋” ๊ผผ๊ผผํžˆ ์‹ ๊ฒฝ ์จ์•ผ ํ•œ๋‹ค.

 

์ด๋ฒˆ ๊ฒฝํ—˜์„ ํ†ตํ•ด JWT ์ธ์ฆ ํ๋ฆ„์„ ์Šค์Šค๋กœ ์„ค๊ณ„ํ•˜๊ณ , ํ•„ํ„ฐ์—์„œ ์š”์ฒญ ํ๋ฆ„์„ ์ œ์–ดํ•˜๋Š” ๋ฒ•์„ ์ดํ•ดํ•˜๊ณ , ๋ฌธ์ œ๊ฐ€ ์ƒ๊ฒผ์„ ๋•Œ ์ฒ˜์Œ๋ถ€ํ„ฐ ๋‹ค์‹œ ์ ๊ฒ€ํ•˜๋Š” ์Šต๊ด€์˜ ์ค‘์š”์„ฑ์„ ๋ชจ๋‘ ๋ฐฐ์šธ ์ˆ˜ ์žˆ์—ˆ๋‹ค.
์•ž์œผ๋กœ๋„ ์–ด๋””์„ ๊ฐ€ ์‘๋‹ต์ด ๋Š๊ธด๋‹ค๋ฉด ์š”์ฒญ ํ๋ฆ„์„ ํ•œ ๋‹จ๊ณ„์”ฉ ๊ผผ๊ผผํžˆ ๋”ฐ๋ผ๊ฐ€ ๋ด์•ผ๊ฒ ๋‹ค!