[SPRING] ๐Ÿšจ Spring Security + JWT๋กœ ์ธ์ฆ ์ธ๊ฐ€ ๊ตฌํ˜„ํ•˜๊ธฐ

๋ฆผ๋ฏผ์ง€ยท2025๋…„ 5์›” 5์ผ

์Šคํ”„๋ง ๋ณด์•ˆ์˜ ๊ฝƒ์ธ ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ๋ฅผ ๊ตฌํ˜„ํ•ด๋ณด์ž!


โœ… ์ธ์ฆ ํ๋ฆ„ ํฐ๊ทธ๋ฆผ์œผ๋กœ ํ†บ์•„๋ณด๊ธฐ!

  1. ์‚ฌ์šฉ์ž๊ฐ€ ๋กœ๊ทธ์ธ
  2. JWT๋ฅผ ๋ฐœ๊ธ‰ํ•ด์ค€๋‹ค
  3. ํด๋ผ์ด์–ธํŠธ๊ฐ€ JWT๋ฅผ Authorization ํ—ค๋”์— ํฌํ•จ์‹œ์ผœ์„œ ๋‚˜ ์ ‘๊ทผํ•ด๋„ ๋˜๋‹ˆ? ๋ผ๊ณ  ์š”์ฒญ์„ ๋ณด๋‚ธ๋‹ค
  4. JWTํ•„ํ„ฐ๊ฐ€ ์š”์ฒญ์„ ๊ฐ€๋กœ์ฑ„์„œ ํ† ํฐ์ด ์œ ํšจํ•œ์ง€ ๊ฒ€์ฆํ•œ๋‹ค
  5. ์œ ํšจํ•˜๋ฉด? Authorization ๊ฐ์ฒด๋ฅผ ๋งŒ๋“ค์–ด์„œ SecurityContextHolder์— ์ €์žฅํ•œ๋‹ค
  6. filterChain์„ ํ†ตํ•ด ๋‹ค์Œ ํ•„ํ„ฐ๋กœ ๋„˜์–ด๊ฐ€์„œ ์ปจํŠธ๋กค๋Ÿฌ์—์„œ ์ธ์ฆ๋œ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•œ๋‹ค.

๐Ÿ‘€ ์™œ ๊ทธ๋ƒฅ Filter๋ง๊ณ  SpringSecurity๋ฅผ ์‚ฌ์šฉํ•˜๋Š”๊ฑธ๊นŒ?

ํ•ญ๋ชฉ์ผ๋ฐ˜ ์„œ๋ธ”๋ฆฟ ํ•„ํ„ฐSpring Security
์ธ์ฆ/์ธ๊ฐ€ ๋ถ„๋ฆฌ์ง์ ‘ ๊ตฌํ˜„ํ•ด์•ผ ํ•จ๊ตฌ์กฐํ™”๋˜์–ด ์žˆ์Œ (Filter, Provider, Context ๋“ฑ)
์žฌ์‚ฌ์šฉ์„ฑ/์œ ์ง€๋ณด์ˆ˜๋กœ์ง ์ค‘๋ณต ๋งŽ์Œ๊ฐ ๋กœ์ง์ด ๋ชจ๋“ˆํ™”๋˜์–ด ์žฌ์‚ฌ์šฉ ์‰ฌ์›€
์˜ˆ์™ธ ์ฒ˜๋ฆฌtry-catch ์ง์ ‘ ์ฒ˜๋ฆฌExceptionTranslationFilter๊ฐ€ ์ฒ˜๋ฆฌ
ThreadLocal ์ธ์ฆ ์ •๋ณด ์ €์žฅ์ง์ ‘ ๊ตฌํ˜„ํ•ด์•ผ ํ•จSecurityContextHolder ์ œ๊ณต
์„ค์ •/ํ™•์žฅ์„ฑํ•˜๋“œ์ฝ”๋”ฉ/๋ณต์žกํ•จDSL ๋ฐฉ์‹ (SecurityFilterChain)์œผ๋กœ ์ง๊ด€์ 

์ฆ‰, ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ๋Š” ๋ณด์•ˆ์— ํ•„์š”ํ•œ ๊ณตํ†ต ๊ตฌ์กฐ๋ฅผ ์ž˜ ๋ถ„๋ฆฌํ•˜๊ณ  ํ™•์žฅํ•˜๊ธฐ ์‰ฝ๊ฒŒ ๋งŒ๋“ค์–ด์ค€๋‹ค!!

๐Ÿš“ ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ ๊ตฌ์กฐ

์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ๋Š” Filter ๊ธฐ๋ฐ˜ ๋ณด์•ˆ ํ”„๋ ˆ์ž„์›Œํฌ!
๊ตฌ์กฐ๋Š” ์•„๋ž˜์ฒ˜๋Ÿผ ๊ณ„์ธต์ ์œผ๋กœ ๊ตฌ์„ฑ๋˜์–ด ์žˆ๋‹ค

์ฃผ์š” ๊ธฐ๋Šฅ

  1. SecurityFilterChain
    HTTP ์š”์ฒญ๋งˆ๋‹ค ์ž‘๋™ํ•˜๋Š” ๋ณด์•ˆ ํ•„ํ„ฐ ์ฒด์ธ
    ์šฐ๋ฆฌ๊ฐ€ httpSecurity๋ฅผ ํ†ตํ•ด ์„ค์ •ํ•˜๋Š” ๊ณณ์ด ์—ฌ๊น๋‹ˆ๋‹ค.

  2. Filter ๊ณ„์ธต
    ์‹œํ๋ฆฌํ‹ฐ๋Š” ์—ฌ๋Ÿฌ ๊ฐœ์˜ Filter๋ฅผ ์ฒด์ธ์œผ๋กœ ๋‘๊ณ  ์žˆ์œผ๋ฉฐ, ์š”์ฒญ์ด ์˜ค๋ฉด ์ด ํ•„ํ„ฐ๋“ค์„ ํ†ต๊ณผํ•˜๋ฉด์„œ ์ธ์ฆ/์ธ๊ฐ€ ๋“ฑ์„ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค. ์ฃผ์š” ํ•„ํ„ฐ๋Š” ์•„๋ž˜์™€ ๊ฐ™์Šต๋‹ˆ๋‹ค:

  3. UsernamePasswordAuthenticationFilter
    ์œ ์ €์˜ ๋กœ๊ทธ์ธ ์ฒ˜๋ฆฌ

  4. ExceptionTranslationFilter
    ์˜ˆ์™ธ ์ฒ˜๋ฆฌ

  5. FilterSecurityInterceptor
    ์ธ๊ฐ€(์ ‘๊ทผ ๊ถŒํ•œ) ์ฒ˜๋ฆฌ

  6. AuthenticationManager
    ์ธ์ฆ ๋กœ์ง์„ ์ฒ˜๋ฆฌํ•˜๋Š” ํ•ต์‹ฌ ๊ฐ์ฒด์ž…๋‹ˆ๋‹ค. ์ธ์ฆ ์š”์ฒญ์„ ๋ฐ›์•„์„œ ์‹ค์ œ ์ธ์ฆ์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.

  7. AuthenticationProvider
    ํŠน์ • ๋ฐฉ์‹์˜ ์ธ์ฆ์„ ์ฒ˜๋ฆฌํ•˜๋Š” ์ปดํฌ๋„ŒํŠธ
    (ex. ์‚ฌ์šฉ์ž๋ช…/๋น„๋ฐ€๋ฒˆํ˜ธ, JWT ๋“ฑ)

  8. SecurityContext + SecurityContextHolder
    : ์ธ์ฆ๋œ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ์ €์žฅ!

โ˜‘๏ธ ๊ธฐ๋Šฅ๋ณ„ ์—ญํ•  ์ •๋ฆฌ

์ปดํฌ๋„ŒํŠธ์—ญํ• 
JwtFilter (JwtAuthenticationFilter)์š”์ฒญ์—์„œ JWT ํ† ํฐ์„ ์ถ”์ถœํ•˜๊ณ , ์œ ํšจ์„ฑ ๊ฒ€์ฆ ํ›„ Authentication ๊ฐ์ฒด ์ƒ์„ฑ ๋ฐ SecurityContext์— ์ €์žฅ
JwtUtil (JwtProvider)JWT ์ƒ์„ฑ, ํŒŒ์‹ฑ, ๋งŒ๋ฃŒ ์—ฌ๋ถ€, ์‹œ๊ทธ๋‹ˆ์ฒ˜ ๊ฒ€์ฆ ๋“ฑ JWT ๊ด€๋ จ ๊ธฐ๋Šฅ ์ „๋‹ด
SecurityFilterChain์–ด๋–ค ์š”์ฒญ์— ์ธ์ฆ์ด ํ•„์š”ํ•œ์ง€, ์–ด๋–ค ํ•„ํ„ฐ๋ฅผ ๋จผ์ € ์‹คํ–‰ํ• ์ง€, ์–ด๋–ค ๋ฐฉ์‹์œผ๋กœ ๋กœ๊ทธ์ธ/์ธ๊ฐ€๋ฅผ ์ฒ˜๋ฆฌํ• ์ง€ ์„ค์ •

๐Ÿšฆ ์ƒํ™ฉ๋ณ„ ํ๋ฆ„ ์ •๋ฆฌ

1. ๋กœ๊ทธ์ธํ•  ๋•Œ (Jwt ํ† ํฐ ํ•„์š”ํ•˜์ง€ ์•Š์Œ)
โ†’ filter์™€ security๋ฅผ ๊ฑฐ์น˜์ง€ ์•Š๊ณ , ๋ฐ”๋กœ jwtUtil์—์„œ ํ† ํฐ์„ ์ƒ์„ฑํ•ด์„œ ๋ฐœ๊ธ‰ํ•ด์ค€๋‹ค

[Client] โ†’ POST /login {username, password}
            โ†’ [AuthController]
              โ†’ AuthenticationManager.authenticate()
              โ†’ JWT ์ƒ์„ฑ โ†’ Response

2. Jwtํ† ํฐ(์ธ์ฆ/์ธ๊ฐ€)๊ฐ€ ํ•„์š”ํ•œ ์š”์ฒญ์ผ ๋•Œ
โ†’ jwtFilter๊ฐ€ ์š”์ฒญ์„ ๊ฐ€๋กœ์ฑ„์„œ ํ† ํฐ์ด ์œ ํšจํ•œ์ง€ ๊ฒ€์ฆ
โ†’ ์œ ํšจํ•˜๋‹ค๋ฉด securityContext์— ์ €์žฅ
โ†’ ์š”์ฒญํ•œ controller์˜ ๊ธฐ๋Šฅ์œผ๋กœ ์ „๋‹ฌ (AuthUser๋“ฑ์˜ ์ •๋ณด)

[Client] โ†’ GET /protected-resource
  Authorization: Bearer <JWT>

  โ†’ [JwtAuthenticationFilter]
     โ†’ ํ† ํฐ ์œ ํšจ์„ฑ ๊ฒ€์ฆ
     โ†’ ์‚ฌ์šฉ์ž ์ •๋ณด ๋กœ๋”ฉ โ†’ Authentication ์ƒ์„ฑ
     โ†’ SecurityContext์— ์ €์žฅ
     โ†’ ๋‹ค์Œ ํ•„ํ„ฐ ๋˜๋Š” ์ปจํŠธ๋กค๋Ÿฌ๋กœ ์ „๋‹ฌ

โ†’ [Controller] ์ธ์ฆ๋œ ์‚ฌ์šฉ์ž ์ •๋ณด ์‚ฌ์šฉ ๊ฐ€๋Šฅ

๐ŸŽ JwtUtil ๊ตฌํ˜„ํ•˜๊ธฐ

JwtUtil์€ ํ† ํฐ์„ ์ƒ์„ฑ, ํŒŒ์‹ฑ, ๋งŒ๋ฃŒ ๋“ฑ์„ ๋‹ด๋‹นํ•œ๋‹ค

๐Ÿ”Ž ์—ญํ• 

  • ๋งŒ๋ฃŒ ์‹œ๊ฐ„ ์„ค์ •
  • ํ† ํฐ ๋ฐœ๊ธ‰ (ํ† ํฐ์— ๋‹ด๊ธธ ์ •๋ณด ์„ค์ •)
  • ํ† ํฐ ๊บผ๋‚ด์˜ค๊ธฐ
  • ์•”ํ˜ธํ™”๋œ ํ† ํฐ์˜ ์ •๋ณด๋ฅผ ๊บผ๋‚ด์„œ ์œ ํšจํ•œ์ง€ ๊ฒ€์ฆํ•˜๊ณ  ์ „๋‹ฌํ•ด์ฃผ๊ธฐ(ํŒŒ์‹ฑ)

์ „์ฒด ์ฝ”๋“œ

@Component
public class JwtUtil {

    private static final String BEARER_PREFIX = "Bearer ";
    private static final long TOKEN_TIME = 60 * 60 * 1000L; // 60๋ถ„

    @Value("${jwt.secret.key}")
    private String secretKey;
    private Key key;
    private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

    @PostConstruct
    public void init() {
        byte[] bytes = Base64.getDecoder().decode(secretKey);
        key = Keys.hmacShaKeyFor(bytes);
    }

    public String createToken(Long userId, String email, UserRole userRole, String nickName) {
        Date date = new Date();

        return BEARER_PREFIX +
                Jwts.builder()
                        .setSubject(String.valueOf(userId))
                        .claim("email", email)
                        .claim("userRole", userRole)
                        .claim("nickName", nickName) //ํ”„๋ก ํŠธ์—์„œ ๊บผ๋‚ด ์“ธ ์ˆ˜ ์žˆ๋„๋ก nickName ์ถ”๊ฐ€
                        .setExpiration(new Date(date.getTime() + TOKEN_TIME))
                        .setIssuedAt(date) // ๋ฐœ๊ธ‰์ผ
                        .signWith(key, signatureAlgorithm) // ์•”ํ˜ธํ™” ์•Œ๊ณ ๋ฆฌ์ฆ˜
                        .compact();
    }

    public String substringToken(String tokenValue) {
        if (StringUtils.hasText(tokenValue) && tokenValue.startsWith(BEARER_PREFIX)) {
            return tokenValue.substring(7);
        }
        throw new ServerException("Not Found Token");
    }

    public Claims extractClaims(String token) {
        return Jwts.parserBuilder()
                .setSigningKey(key)
                .build()
                .parseClaimsJws(token)
                .getBody();
    }
}

๐Ÿ”Ž ํ•˜๋‚˜์”ฉ ๋œฏ์–ด๋ณด๊ธฐ

1. ๋งŒ๋ฃŒ ์‹œ๊ฐ„, ์•”ํ˜ธํ™”ํ•  jwt secretkey ์„ค์ •ํ•˜๊ธฐ

@Component
public class JwtUtil {

    private static final String BEARER_PREFIX = "Bearer ";
    private static final long TOKEN_TIME = 60 * 60 * 1000L; // 60๋ถ„

    @Value("${jwt.secret.key}")
    private String secretKey;
    private Key key;
    private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
  • ํ† ํฐ ๋ฐœ๊ธ‰ํ•  ๋•Œ, ์•ž์— "Bearer "๋ผ๊ณ  prefix๋ผ๊ณ  ๋‹ฌ์•„์ฃผ๊ธฐ
  • ๋งŒ๋ฃŒ์‹œ๊ฐ„์€ 60๋ถ„์œผ๋กœ ์„ค์ •
  • ๊ธฐ์กด์— properties์— ๋“ฑ๋กํ•ด๋‘” jwt secretkey๋ฅผ ์‚ฌ์šฉ
    (Base64 ์ธ์ฝ”๋”ฉ๋œ ๋ฌธ์ž์—ด)

2. ์„œ๋ช…์„ ์œ„ํ•œ key ๊ฐ์ฒด ๋งŒ๋“ค๊ธฐ

@PostConstruct
    public void init() {
        byte[] bytes = Base64.getDecoder().decode(secretKey);
        key = Keys.hmacShaKeyFor(bytes);
    }

์šฐ๋ฆฌ๊ฐ€ ๊ฐ€์ ธ์˜จ secretKey๋Š” ํ…์ŠคํŠธ์ด๊ธฐ ๋•Œ๋ฌธ์—, ์ด๊ฑธ ๋ฐ”์ดํŠธ ๋ฐฐ์—ด๋กœ ๋ณ€ํ™˜ํ•ด์•ผ ์‹ค์ œ HMAC ํ‚ค๋กœ ์“ธ ์ˆ˜ ์žˆ๋‹ค.
โ†’ Base64 ๋””์ฝ”๋”ฉ์„ ํ†ตํ•ด ๊ทธ ๋ฐ”์ดํŠธ ๋ฐฐ์—ด์„ ๋ณต์›

Keys.hmacShaKeyFor(bytes)
: ๋””์ฝ”๋”ฉ๋œ ๋ฐ”์ดํŠธ ๋ฐฐ์—ด์„ javax.crypto.SecretKey๋กœ ๋ณ€ํ™˜
(JWT๋ฅผ ์ƒ์„ฑํ•  ๋•Œ ์‚ฌ์šฉ๋  ์„œ๋ช…์šฉ Key ๊ฐ์ฒด)

โžก๏ธ ์ด ํ‚ค๋Š” HS256 ๊ฐ™์€ ๋Œ€์นญ ์„œ๋ช… ์•Œ๊ณ ๋ฆฌ์ฆ˜(HMAC)์— ์‚ฌ์šฉ๋ค๋‹ค

3. ํ† ํฐ ๋ฐœ๊ธ‰ ๋ฉ”์„œ๋“œ

public String createToken(Long userId, String email, UserRole userRole, String nickName) {
    Date date = new Date();

    return BEARER_PREFIX +
            Jwts.builder()
                .setSubject(String.valueOf(userId))
                .claim("email", email)
                .claim("userRole", userRole)
                .claim("nickName", nickName) //ํ”„๋ก ํŠธ์—์„œ ๊บผ๋‚ด ์“ธ ์ˆ˜ ์žˆ๋„๋ก nickName ์ถ”๊ฐ€
                .setExpiration(new Date(date.getTime() + TOKEN_TIME))
                .setIssuedAt(date) // ๋ฐœ๊ธ‰์ผ
                .signWith(key, signatureAlgorithm) // ์•”ํ˜ธํ™” ์•Œ๊ณ ๋ฆฌ์ฆ˜
                .compact();
    }

๐Ÿซง JwtFilter ๊ตฌํ˜„ํ•˜๊ธฐ

์ด์ œ ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์š”์ฒญ์„ ๋ณด๋‚ผ ๋•Œ, ๋กœ๊ทธ์ธ ์‹œ ๋ฐœ๊ธ‰๋ฐ›์€ ํ† ํฐ์„ ํ•จ๊ป˜ ๋ณด๋‚ผ ๊ฒƒ ์ด๋‹ค!
์ด ํ† ํฐ ๊ฐ’์„ ๊ฒ€์ฆํ•˜๊ณ , ์ธ๊ฐ€ํ•  ์ˆ˜ ์žˆ๋Š” JwtFilter๋ฅผ ๊ตฌํ˜„ํ•ด๋ณด์ž

๋จผ์ € Spring Security๋ฅผ ๊ตฌํ˜„ํ•  ๋•Œ๋Š” ๊ทธ๋ƒฅ Filter๋ฅผ ์ƒ์†ํ•˜๋Š”๊ฒŒ ์•„๋‹ˆ๋ผ,OncePerRequestFilter๋ฅผ ์ƒ์†ํ•œ๋‹ค

โžก๏ธ ๋‹จ ํ•œ ๋ฒˆ๋งŒ ๊ฒ€์ฆ์ด ์ด๋ฃจ์–ด์ง€๋ฉด ๋˜๋ฏ€๋กœ, OncePerRequestFilter๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ!
(์š”์ฒญ๋‹น ํ•œ ๋ฒˆ๋งŒ ์‹คํ–‰๋˜๋Š” JWT ํ•„ํ„ฐ)

์ด ํ•„ํ„ฐ๋Š” ์šฐ๋ฆฌ๊ฐ€ ์š”์ฒญํ•  ๋•Œ ํ—ค๋”์— ๋‹ด๊ธด "๋น„๋ฐ€๋ฒˆํ˜ธ ๊ฐ™์€ ํ† ํฐ(JWT)"์„ ๊ฒ€์‚ฌํ•ด์„œ,
โ€œ์ด ์‚ฌ๋žŒ์ด ์ง„์งœ ๋กœ๊ทธ์ธํ•œ ์‚ฌ๋žŒ์ธ๊ฐ€?โ€ ๋ฅผ ํ™•์ธํ•ด์ฃผ๋Š” ๊ฒฝ๋น„์› ๊ฐ™์€ ์—ญํ• 

์ „์ฒด ์ฝ”๋“œ

๋œฏ์–ด๋ณด๊ธฐ

๐Ÿ” 1. ํด๋ž˜์Šค ๊ตฌ์„ฑ ๋จผ์ € ๋ณด๊ธฐ

@AllArgsConstructor
@Component
@Slf4j
public class JwtAuthFilter extends OncePerRequestFilter {

@Component: ์ด๊ฑธ ์Šคํ”„๋ง์ด ์ž๋™์œผ๋กœ ๋งŒ๋“ค์–ด์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ
@AllArgsConstructor: JwtUtil์„ ์ƒ์„ฑ์ž์— ์ž๋™์œผ๋กœ ์‚ฝ์ž…

@Slf4j: ๋กœ๊ทธ๋ฅผ ๋‚จ๊ธฐ๊ธฐ (์˜ˆ: ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ๊ธฐ๋ก)

๐Ÿ” 2. ํ•ต์‹ฌ ๋ฉ”์„œ๋“œ: doFilterInternal

โ€œ์š”์ฒญ์ด ๋“ค์–ด์˜ค๋ฉด ํ† ํฐ์ด ์žˆ๋Š”์ง€ ํ™•์ธํ•˜๊ณ , ์žˆ์œผ๋ฉด ๊ฒ€์‚ฌํ•ด์„œ, ์ง„์งœ ์‚ฌ์šฉ์ž๋ผ๋ฉด ์ธ์ฆ ์ฒ˜๋ฆฌํ•ด์ฃผ๊ณ , ์•„๋‹ˆ๋ฉด ๊ฑฐ์ ˆํ•ด!โ€

โœ… Step 1. Authorization ํ—ค๋”์—์„œ ํ† ํฐ ๊บผ๋‚ด๊ธฐ

String bearerToken = request.getHeader("Authorization");
String url = request.getRequestURI();

๋ˆ„๊ตฐ๊ฐ€ ์›น ์š”์ฒญ์„ ํ•˜๋ฉด, ๊ทธ ์š”์ฒญ ํ—ค๋”์— ๋‹ด๊ธด ํ† ํฐ("Authorization"์ด๋ผ๋Š” ์ด๋ฆ„)์„ ๊บผ๋‚ด๊ธฐ

โœ… Step 2. ํ† ํฐ์ด ์—†๊ฑฐ๋‚˜ ํ˜•์‹์ด ์ด์ƒํ•˜๋ฉด ๊ทธ๋ƒฅ ํ†ต๊ณผ

if (bearerToken == null || !bearerToken.startsWith("Bearer ")) {
    chain.doFilter(request, response);
    return;
}

ํ† ํฐ์ด ์•„์˜ˆ ์—†๊ฑฐ๋‚˜ "Bearer "๋กœ ์‹œ์ž‘ํ•˜์ง€ ์•Š์œผ๋ฉด,์ธ์ฆ์„ ๋ฐ›์ง€ ์•Š์€ ๊ฒƒ.
๐Ÿ‘‰ ๊ทธ๋ƒฅ ๋‹ค์Œ ํ•„ํ„ฐ๋กœ ๋„˜๊ธฐ์ž
(๋น„ํšŒ์› ํŽ˜์ด์ง€ ๊ฐ™์€ ๊ณณ์€ ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•˜๊ธฐ ์œ„ํ•จ)

โœ… Step 3. ํ† ํฐ์—์„œ ์‹ค์ œ ๋‚ด์šฉ๋งŒ ๋ฝ‘๊ธฐ

String jwt = jwtUtil.substringToken(bearerToken);

"Bearer abc.def.ghi"์ฒ˜๋Ÿผ ๋˜์–ด ์žˆ๋Š” ํ† ํฐ์—์„œ "abc.def.ghi"๋งŒ ์ž˜๋ผ๋‚ด๊ธฐ

โœ… Step 4. ํ† ํฐ์—์„œ ์ •๋ณด ๋ฝ‘์•„์˜ค๊ธฐ

claims = ํ† ํฐ ์•ˆ์˜ ์ •๋ณด

Claims claims = jwtUtil.extractClaims(jwt);

ํ† ํฐ์„ ํ•ด์„ํ•ด์„œ ๊ทธ ์•ˆ์— ๋“ค์–ด ์žˆ๋Š” ์‚ฌ์šฉ์ž ์ •๋ณด(email, userRole ๋“ฑ)๋ฅผ ๊บผ๋‚ด๊ธฐ

๐Ÿšซ Step 5. ์ž˜๋ชป๋œ ํ† ํฐ์ด๋ฉด ๊ฑฐ์ ˆ

if (claims == null) {
    response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid JWT");
    return;
}

์ •๋ณด๋ฅผ claims๋กœ ๊บผ๋ƒˆ์„๋•Œ ์•„๋ฌด๊ฒƒ๋„ ์—†์œผ๋ฉด? ์ž˜๋ชป๋œ ํ† ํฐ์ด๊ฒ ์ ธ
๐Ÿ‘‰ "์ด๊ฑฐ ์ž˜๋ชป๋œ ํ† ํฐ์ด์•ผ!" ํ•˜๊ณ  ์š”์ฒญ์„ ๋Š๋Š”๋‹ค

๐Ÿ‘ฎโ€โ™‚๏ธ Step 6. ๊ด€๋ฆฌ์ž ํŽ˜์ด์ง€๋ผ๋ฉด ๊ด€๋ฆฌ์ž์ธ์ง€ ํ™•์ธ!

if (url.startsWith("/admin")) {
    if (!UserRole.ADMIN.equals(userRole)) {
        response.sendError(HttpServletResponse.SC_FORBIDDEN, "๊ด€๋ฆฌ์ž ๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค.");
        return;
    }
    chain.doFilter(request, response);
    return;
}

์š”์ฒญํ•œ ์ฃผ์†Œ๊ฐ€ /admin์œผ๋กœ ์‹œ์ž‘ํ•˜๋ฉด ๐Ÿ‘‰ โ€œ๊ด€๋ฆฌ์ž๋งŒ ๋“ค์–ด์˜ฌ ์ˆ˜ ์žˆ๋Š” ๊ณณ!โ€

๊ทธ๋Ÿฐ๋ฐ ์ผ๋ฐ˜ ์œ ์ €๊ฐ€ ์ ‘๊ทผํ•˜๋ฉด ๐Ÿ‘‰ 403 Forbidden ์˜ค๋ฅ˜๋กœ ์ฐจ๋‹จ

โœ… Step 7. ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๊บผ๋‚ด์„œ ๋‹ด์•„๋‘๊ธฐ

AuthUser authUser = new AuthUser(
        Long.parseLong(claims.getSubject()),
        (String) claims.get("email"),
        userRole
);

ํ† ํฐ์— ๋‹ด๊ธด ๋‚ด์šฉ์œผ๋กœ AuthUser ๊ฐ์ฒด๋ฅผ ๋งŒ๋“ค์–ด์„œ,
"์ด ์‚ฌ๋žŒ์€ ๋ˆ„๊ตฌ์ธ์ง€" ์ •๋ณด๋ฅผ ๊ฐ–๊ณ  ์žˆ๊ฒŒ!

๐Ÿ›  Step 8. ์‚ฌ์šฉ์ž ์ •๋ณด request์— ์ €์žฅ

request.setAttribute("userId", ...);

์ปจํŠธ๋กค๋Ÿฌ์—์„œ request.getAttribute("userId") ํ•˜๋ฉด
๐Ÿ‘‰ ํ† ํฐ์—์„œ ๊บผ๋‚ธ ์‚ฌ์šฉ์ž id, email, role์„ ๋‹ค์‹œ ๊บผ๋‚ผ ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์คŒ

๐Ÿ” Step 9. ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ์— ๋“ฑ๋ก

Collection<? extends GrantedAuthority> authorities =
    List.of(new SimpleGrantedAuthority(authUser.getUserRole().toString()));

Authentication authToken = new UsernamePasswordAuthenticationToken(authUser,
        null, authorities);

SecurityContextHolder.getContext().setAuthentication(authToken);

์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ์—์„œ๋Š” Authentication ๊ฐ์ฒด๋ฅผ ๋งŒ๋“ค์–ด์„œ
๐Ÿ‘‰ "์ด ์‚ฌ๋žŒ์€ ๋กœ๊ทธ์ธํ•œ ์‚ฌ๋žŒ์ด๊ณ , ๊ถŒํ•œ์€ ์ด๋Ÿฐ ๊ฑธ ๊ฐ€์กŒ์–ด"๋ผ๊ณ  ์•Œ๋ ค์ค˜์•ผํ•จ

๋งˆ์ง€๋ง‰ ์ค„์€ ์‹œํ๋ฆฌํ‹ฐ ์„ธ์…˜์— ์ธ์ฆ ์ •๋ณด ์ €์žฅํ•˜๋Š” ๋ถ€๋ถ„

โœ… Step 10. ํ†ต๊ณผ์‹œํ‚ค๊ธฐ

chain.doFilter(request, response);

์—ฌ๊ธฐ๊นŒ์ง€ ์„ฑ๊ณตํ–ˆ๋‹ค๋ฉด ๋“œ๋””์–ด ์ธ์ฆ๋œ ์‚ฌ์šฉ์ž๋กœ์„œ ๋‹ค์Œ ํ•„ํ„ฐ๋‚˜ ์ปจํŠธ๋กค๋Ÿฌ๋กœ ๋„˜์–ด๊ฐ„๋‹ค

โ—๏ธStep 11. ์˜ˆ์™ธ ์ฒ˜๋ฆฌ

} catch (ExpiredJwtException e) { ... }

ํ† ํฐ์ด ๋งŒ๋ฃŒ๋˜์—ˆ๊ฑฐ๋‚˜, ์„œ๋ช…์ด ์ด์ƒํ•˜๊ฑฐ๋‚˜, ํฌ๋งท์ด ํ‹€๋ ธ์œผ๋ฉด
๊ฐ๊ฐ์˜ ์ƒํ™ฉ์— ๋งž๊ฒŒ ๋กœ๊ทธ ๊ธฐ๋ก + ์—๋Ÿฌ ์ฝ”๋“œ๋ฅผ ๋ฐ˜ํ™˜

ํ๋ฆ„ ๋ณด๊ธฐ

์‚ฌ์šฉ์ž
 |
 | HTTP ์š”์ฒญ (Authorization: Bearer {JWT})
 v
JwtAuthFilter (OncePerRequestFilter)
 |
 |---> JWT ํ† ํฐ ์ถ”์ถœ ๋ฐ ๊ฒ€์ฆ
 |       |
 |       |---> JwtUtil.extractClaims(token)
 |               |
 |               |---> ์„œ๋ช… ๊ฒ€์ฆ ๋ฐ Claims ์ถ”์ถœ
 |               |
 |               |<--- Claims ๋ฐ˜ํ™˜
 |       |
 |       |---> ์‚ฌ์šฉ์ž ์ •๋ณด ๋ฐ ๊ถŒํ•œ ํ™•์ธ
 |       |---> Authentication ๊ฐ์ฒด ์ƒ์„ฑ
 |       |---> SecurityContextHolder์— ์ธ์ฆ ์ •๋ณด ์ €์žฅ
 |
 | HTTP ์š”์ฒญ ์ „๋‹ฌ
 v
๋‹ค์Œ ํ•„ํ„ฐ ๋˜๋Š” ์ปจํŠธ๋กค๋Ÿฌ

Spring Security

@Configuration
@EnableWebSecurity
@AllArgsConstructor
public class SecurityConfig {

    private final JwtAuthFilter jwtAuthFilter;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                // CSRF, Form ๋กœ๊ทธ์ธ, HTTP Basic ์ธ์ฆ ๋น„ํ™œ์„ฑํ™”
                .csrf(AbstractHttpConfigurer::disable)
                .formLogin(AbstractHttpConfigurer::disable)
                .httpBasic(AbstractHttpConfigurer::disable)
                // JWT ์‚ฌ์šฉ์„ ์œ„ํ•ด ์„ธ์…˜์„ STATELESS๋กœ ์„ค์ • (์„ธ์…˜ ์ •๋ณด ์ €์žฅ x)
                .sessionManagement(session -> session
                        .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                )

                //์ธ์ฆ ์ธ๊ฐ€ URL ์„ค์ •
                .authorizeHttpRequests((auth)-> auth
                .requestMatchers("/", "/auth/signup", "/auth/signin").permitAll() //์ธ์ฆ ์—†์ด ์‚ฌ์šฉ ๊ฐ€๋Šฅ
                .requestMatchers("/admin/**").hasRole("ADMIN") //ADMIN ๊ถŒํ•œ์ด ์žˆ์–ด์•ผ๋งŒ ์‚ฌ์šฉ ๊ฐ€๋Šฅ
                .anyRequest().authenticated()) //๋‚˜๋จธ์ง€๋Š” ์ธ์ฆ๋งŒ ํ•„์š”

        .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
        return http.build();
    }
}

๋งˆ์ง€๋ง‰์˜

.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);

์ด ๋ถ€๋ถ„ ๋•๋ถ„์—, Spring Security์˜
UsernamePasswordAuthenticationFilter.class ์ด๋ถ€๋ถ„์ด ์‹คํ–‰๋˜๊ธฐ ์ „์—
์•„๊นŒ ์„ค์ •ํ•œ jwtAuthFilter๊ฐ€ ์‹คํ–‰๋˜๋ฉด์„œ ํ•„ํ„ฐ ์ฒด์ธ์ด ๋Œ์•„๊ฐˆ ์ˆ˜ ์žˆ๊ฒŒ ๋˜๋Š”๊ฒƒ!


FilterConfig

์žŠ์ง€๋ง๊ณ , filterConfig์—์„œ๋„ ์•„๊นŒ๋งŒ๋“  ํ•„ํ„ฐ ๋“ฑ๋กํ•˜๊ธฐ

@Configuration
@RequiredArgsConstructor
public class FilterConfig {

    private final JwtUtil jwtUtil;

    @Bean
    public FilterRegistrationBean<JwtAuthFilter> jwtFilter() {
        FilterRegistrationBean<JwtAuthFilter> registrationBean = new FilterRegistrationBean<>();
        registrationBean.setFilter(new JwtAuthFilter(jwtUtil));
        registrationBean.addUrlPatterns("/*"); // ํ•„ํ„ฐ๋ฅผ ์ ์šฉํ•  URL ํŒจํ„ด์„ ์ง€์ •ํ•ฉ๋‹ˆ๋‹ค.

        return registrationBean;
    }
}

0๊ฐœ์˜ ๋Œ“๊ธ€