πŸ”‘ Spring Bootμ—μ„œ JWT(Json Web Token) 인증 κ΅¬ν˜„ν•˜κΈ°

DUΒ·2025λ…„ 9μ›” 5일
0

πŸ“Œ JWTλž€ 무엇인가?

JWT(Json Web Token)λŠ” JSON ν˜•μ‹μœΌλ‘œ μ‚¬μš©μž 정보λ₯Ό μ•ˆμ „ν•˜κ²Œ μ „λ‹¬ν•˜κΈ° μœ„ν•œ 토큰 기반 인증 λ°©μ‹μž…λ‹ˆλ‹€. μ‚¬μš©μžκ°€ λ‘œκ·ΈμΈν•˜λ©΄ μ„œλ²„λŠ” JWTλ₯Ό λ°œκΈ‰ν•˜κ³ , 이후 μš”μ²­λ§ˆλ‹€ ν΄λΌμ΄μ–ΈνŠΈλŠ” 이 토큰을 헀더에 λ‹΄μ•„ λ³΄λƒ…λ‹ˆλ‹€. μ„œλ²„λŠ” λ³„λ„μ˜ μ„Έμ…˜ μ €μž₯μ†Œ 없이도 ν† ν°λ§ŒμœΌλ‘œ μ‚¬μš©μžλ₯Ό 인증할 수 μžˆμŠ΅λ‹ˆλ‹€.


πŸ“¦ JWT의 ꡬ쑰

JWTλŠ” .으둜 κ΅¬λΆ„λœ μ„Έ λΆ€λΆ„μœΌλ‘œ μ΄λ£¨μ–΄μ§‘λ‹ˆλ‹€.

xxxxx.yyyyy.zzzzz

  • Header (헀더)
    ν† ν°μ˜ νƒ€μž…(JWT)κ³Ό ν•΄μ‹± μ•Œκ³ λ¦¬μ¦˜(예: HS256) 정보λ₯Ό λ‹΄μŠ΅λ‹ˆλ‹€.

    {
      "alg": "HS256",
      "typ": "JWT"
    }
  • Payload (νŽ˜μ΄λ‘œλ“œ)
    μ‹€μ œ λ‹΄κ³  싢은 μ‚¬μš©μž 정보(Claims)μž…λ‹ˆλ‹€.
    예: μ‚¬μš©μž 이름, κΆŒν•œ, 만료 μ‹œκ°„ λ“±.

    {
      "sub": "user123",
      "iat": 1691234567,
      "exp": 1691238167
    }
  • Signature (μ„œλͺ…)
    Header와 Payloadλ₯Ό λΉ„λ°€ ν‚€(Secret Key)둜 μ„œλͺ…ν•œ κ°’μž…λ‹ˆλ‹€. 토큰 μœ„Β·λ³€μ‘° μ—¬λΆ€λ₯Ό 검증할 λ•Œ μ‚¬μš©λ©λ‹ˆλ‹€.


πŸ›  μ½”λ“œ μ†Œκ°œ

μ΄λ²ˆμ— μž‘μ„±ν•œ JwtTokenProvider ν΄λž˜μŠ€λŠ” Spring Securityμ—μ„œ JWTλ₯Ό μƒμ„±ν•˜κ³  κ²€μ¦ν•˜λŠ” 핡심 역할을 λ‹΄λ‹Ήν•©λ‹ˆλ‹€. κ΅¬μ²΄μ μœΌλ‘œλŠ” λ‹€μŒκ³Ό 같은 역할을 μˆ˜ν–‰ν•©λ‹ˆλ‹€.

  • 토큰 생성 (generateToken)
  • ν† ν°μ—μ„œ μ‚¬μš©μž 이름 μΆ”μΆœ (getUsername)
  • 토큰 μœ νš¨μ„± 검증 (validate)

πŸ“ μ½”λ“œ 전체

package com.example.demo.security;

import io.jsonwebtoken.*;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import javax.crypto.SecretKey;
import java.util.Date;

@Component
public class JwtTokenProvider {

    // HMAC-SHA μ•Œκ³ λ¦¬μ¦˜μ„ μœ„ν•œ μ‹œν¬λ¦Ώ 킀와 토큰 만료 μ‹œκ°„
    private final SecretKey key;
    private final long expirationMs;

    // μƒμ„±μž: application.ymlμ—μ„œ μ‹œν¬λ¦Ώ 킀와 만료 μ‹œκ°„ μ£Όμž…
    public JwtTokenProvider(
            @Value("${jwt.secret}") String secret,
            @Value("${jwt.expiration-ms}") long expirationMs
    ) {
        byte[] keyBytes = (secret.matches("^[A-Za-z0-9+/=]+$") ? Decoders.BASE64.decode(secret) : secret.getBytes());
        this.key = Keys.hmacShaKeyFor(keyBytes);
        this.expirationMs = expirationMs;
    }
    // 핡심: 토큰을 λ§Œλ“€κ³  κ²€μ¦ν•˜λŠ” 데 ν•„μš”ν•œ μ‹œν¬λ¦Ώ 킀와 만료 μ‹œκ°„μ„ application.ymlμ—μ„œ κ°€μ Έμ™€μ„œ μ€€λΉ„ν•˜λŠ” κ³Όμ •

    // βœ… JWT 토큰 생성
    public String generateToken(UserDetails user) {
        Date now = new Date();
        Date exp = new Date(now.getTime() + expirationMs);

        return Jwts.builder()
                .setSubject(user.getUsername())   // ν† ν°μ˜ 주체(μ‚¬μš©μž 이름)
                .setIssuedAt(now)                 // 토큰 λ°œν–‰ μ‹œκ°„
                .setExpiration(exp)               // 토큰 만료 μ‹œκ°„
                .signWith(key, Jwts.SIG.HS256)    // μ‹œν¬λ¦Ώ ν‚€λ‘œ HS256 μ•Œκ³ λ¦¬μ¦˜μ„ μ‚¬μš©ν•˜μ—¬ 토큰에 μ„œλͺ…
                .compact();                       // μ΅œμ’…μ μœΌλ‘œ λ¬Έμžμ—΄λ‘œ μ••μΆ•ν•˜μ—¬ λ°˜ν™˜
    }
    // 핡심: 토큰에 μ‚¬μš©μž 이름, λ°œν–‰ μ‹œκ°„, 만료 μ‹œκ°„ λ“±μ˜ 정보λ₯Ό λ‹΄κ³ , μ‹œν¬λ¦Ώ ν‚€λ‘œ μ„œλͺ…ν•˜μ—¬ μ•ˆμ „ν•˜κ²Œ μƒμ„±ν•˜κ³  λ¬Έμžμ—΄ ν˜•νƒœλ‘œ λ³€ν™˜ν•˜λŠ” κ³Όμ •

    // βœ… ν† ν°μ—μ„œ μ‚¬μš©μž 이름 μΆ”μΆœ
    public String getUsername(String token) {
        return Jwts.parser()
                .verifyWith(key)                 // ν† ν°μ˜ μ„œλͺ…을 μ‹œν¬λ¦Ώ ν‚€λ‘œ 검증
                .build()
                .parseSignedClaims(token)        // 토큰을 νŒŒμ‹±ν•˜κ³  ν΄λ ˆμž„(정보)을 κ°€μ Έμ˜΄
                .getPayload()
                .getSubject();                   // ν† ν°μ˜ 주체(μ‚¬μš©μž 이름) λ°˜ν™˜
    }
    // 핡심: 토큰이 μœ νš¨ν•œμ§€ λ¨Όμ € ν™•μΈν•œ λ‹€μŒ, κ·Έ μ•ˆμ— λ‹΄κΈ΄ μ‚¬μš©μž 이름을 μΆ”μΆœν•˜λŠ” κ³Όμ •

    // βœ… 토큰 μœ νš¨μ„± 검증
    public boolean validate(String token) {
        try {
            // 토큰을 νŒŒμ‹±ν•˜κ³  μ„œλͺ…을 검증
            Jwts.parser().verifyWith(key).build().parseSignedClaims(token);
            return true;
        } catch (JwtException | IllegalArgumentException e) {
            // 토큰이 잘λͺ»λ˜μ—ˆκ±°λ‚˜ 만료된 경우 μ˜ˆμ™Έ λ°œμƒ
            return false;
        }
    }

    // 토큰 만료 μ‹œκ°„ λ°˜ν™˜
    public long getExpirationMs() {
        return expirationMs;
    }
}

πŸ” μ„ΈλΆ€ μ„€λͺ…

1. μƒμ„±μž

public JwtTokenProvider(@Value("${jwt.secret}") String secret, @Value("${jwt.expiration-ms}") long expirationMs)

application.ymlμ—μ„œ jwt.secretκ³Ό jwt.expiration-ms 값을 μ½μ–΄μ˜΅λ‹ˆλ‹€. 이 κ°’λ“€λ‘œ HMAC-SHA256 μ•Œκ³ λ¦¬μ¦˜μ— λ§žλŠ” SecretKey 객체λ₯Ό μƒμ„±ν•˜μ—¬ 토큰 μ„œλͺ…에 μ‚¬μš©ν•©λ‹ˆλ‹€.

2. 토큰 생성 (generateToken)

public String generateToken(UserDetails user)

user.getUsername()을 ν† ν°μ˜ Subject둜 μ„€μ •ν•˜κ³ , λ°œν–‰ μ‹œκ°„(IssuedAt)κ³Ό 만료 μ‹œκ°„(Expiration)을 μ§€μ •ν•©λ‹ˆλ‹€. signWith(key, Jwts.SIG.HS256)λ₯Ό 톡해 μ‹œν¬λ¦Ώ ν‚€ 기반으둜 μ„œλͺ…ν•˜μ—¬ μœ„λ³€μ‘°λ₯Ό λ°©μ§€ν•©λ‹ˆλ‹€. μ΅œμ’…μ μœΌλ‘œ λ¬Έμžμ—΄ ν˜•νƒœμ˜ JWTλ₯Ό λ°˜ν™˜ν•©λ‹ˆλ‹€.

3. ν† ν°μ—μ„œ μ‚¬μš©μž 이름 μΆ”μΆœ (getUsername)

public String getUsername(String token)

토큰을 νŒŒμ‹±ν•˜κ³  μ„œλͺ…을 κ²€μ¦ν•œ λ’€, Payload의 Subject(μ‚¬μš©μž 이름)λ₯Ό κΊΌλ‚΄μ˜΅λ‹ˆλ‹€.

4. 토큰 μœ νš¨μ„± 검증 (validate)

public boolean validate(String token)

parseSignedClaims(token) μ‹€ν–‰ μ‹œ μ˜ˆμ™Έκ°€ λ°œμƒν•˜μ§€ μ•ŠμœΌλ©΄ trueλ₯Ό λ°˜ν™˜ν•©λ‹ˆλ‹€. λ§Œλ£Œλ˜μ—ˆκ±°λ‚˜ μœ„μ‘°λœ 경우 JwtException이 λ°œμƒν•˜μ—¬ falseλ₯Ό λ°˜ν™˜ν•©λ‹ˆλ‹€.


βœ… 정리 및 배운 점

  • JWTλŠ” μ„Έμ…˜ μ €μž₯μ†Œ 없이 인증 μƒνƒœλ₯Ό μœ μ§€ν•  수 μžˆλŠ” κ°•λ ₯ν•œ λ°©λ²•μž…λ‹ˆλ‹€.
    -JwtTokenProviderλŠ” 토큰 λ°œκΈ‰κ³Ό 검증 λ‘œμ§μ„ λ‹΄λ‹Ήν•˜λ©° Spring Security와 μžμ—°μŠ€λŸ½κ²Œ μ—°λ™λ©λ‹ˆλ‹€.
  • λ³΄μ•ˆμ„ μœ„ν•΄ μ‹œν¬λ¦Ώ ν‚€λŠ” λ°˜λ“œμ‹œ ν™˜κ²½ λ³€μˆ˜λ‘œ 관리해야 ν•˜λ©°, 외뢀에 λ…ΈμΆœλ˜μ§€ μ•Šλ„λ‘ μ£Όμ˜ν•΄μ•Ό ν•©λ‹ˆλ‹€.

πŸš€ ν™•μž₯

  • Refresh Token λ°œκΈ‰ 및 μž¬λ°œκΈ‰ κΈ°λŠ₯ μΆ”κ°€: Access Token 만료 μ‹œ μ‚¬μš©μž κ²½ν—˜μ„ κ°œμ„ ν•  수 μžˆμŠ΅λ‹ˆλ‹€.
  • κΆŒν•œ(Role) 정보λ₯Ό 토큰에 ν•¨κ»˜ μ €μž₯ν•˜μ—¬ Spring Security의 Authentication 객체둜 λ³€ν™˜ν•  수 μžˆμŠ΅λ‹ˆλ‹€.
  • μ˜ˆμ™Έ 처리 κ°•ν™”: λ‹¨μˆœνžˆ true/false λŒ€μ‹  "만료됨 / μœ„μ‘°λ¨ / ν˜•μ‹ 였λ₯˜" λ“± ꡬ체적인 응닡을 μ œκ³΅ν•˜μ—¬ ν΄λΌμ΄μ–ΈνŠΈκ°€ 문제λ₯Ό μ •ν™•νžˆ νŒŒμ•…ν•  수 μžˆλ„λ‘ λ•μŠ΅λ‹ˆλ‹€.

0개의 λŒ“κΈ€