Spring Boot

Spring Boot - JSON Web Token JWT 생성

sounglikane 2024. 10. 11. 20:41

JWT 생성의 과정

1. 비밀 키 초기화 (Secret Key Initialization)

  • 설정 (Application properties):
jwt.secret.key=7Iqk7YyM66W07YOA7L2U65Sp7YG065+9U3ByaW5n6rCV7J2Y7...

 

  • @Value("${jwt.secret.key}"): 이 애너테이션은 애플리케이션 속성 파일(application.properties 또는 application.yml)에서 비밀 키를 주입합니다. 비밀 키는 JWT를 서명하고 검증하는 데 사용됩니다.
  • init() 메서드:
    • @PostConstruct 애너테이션을 사용하여 객체가 생성된 후 실행됩니다.
    • Base64로 인코딩된 비밀 키를 바이트 배열로 디코딩한 후 Keys.hmacShaKeyFor를 사용하여 암호화 키(Key)를 생성합니다. 이 키는 나중에 JWT 서명에 사용됩니다.
@Component
public class JwtUtil {
    // Header KEY 값
    public static final String AUTHORIZATION_HEADER = "Authorization";
    // 사용자 권한 값의 KEY
    public static final String AUTHORIZATION_KEY = "auth";
    // Token 식별자
    public static final String BEARER_PREFIX = "Bearer ";
    // 토큰 만료시간
    private final long TOKEN_TIME = 60 * 60 * 1000L; // 60분

    @Value("${jwt.secret.key}") // Base64 Encode 한 SecretKey
    private String secretKey;
    private Key key;
    private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
    
     // 로그 설정
    public static final Logger logger = LoggerFactory.getLogger("JWT 관련 로그");

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

 

2. JWT 생성

createToken(String username, UserRoleEnum role):

  • 주체 (setSubject): JWT에 "주체" (일반적으로 사용자 ID 또는 사용자 이름)를 설정합니다.
  • 클레임 (claim): 사용자 권한과 같은 추가 정보를 토큰 페이로드에 저장합니다 (여기서는 auth 클레임으로 사용자 권한을 저장).
  • 만료 시간 (setExpiration): 토큰의 만료 시간을 정의하며, 생성된 후 1시간으로 설정됩니다 (TOKEN_TIME = 60 * 60 * 1000L).
  • 발급일 (setIssuedAt): 토큰이 생성된 시간을 기록합니다.
  • 서명 (signWith): HMAC-SHA256 알고리즘과 이전에 초기화된 암호화 키를 사용하여 JWT에 서명합니다. 이는 토큰의 무결성을 보장합니다.
  • 결과는 Bearer로 시작하는 전체 JWT 문자열입니다 (규약의 일환).
public String createToken(String username, UserRoleEnum role) {
    Date date = new Date();

    return BEARER_PREFIX +
            Jwts.builder()
                    .setSubject(username) // 사용자 식별자값(ID)
                    .claim(AUTHORIZATION_KEY, role) // 사용자 권한
                    .setExpiration(new Date(date.getTime() + TOKEN_TIME)) // 만료 시간
                    .setIssuedAt(date) // 발급일
                    .signWith(key, signatureAlgorithm) // 암호화 알고리즘
                    .compact();
}

 

3. JWT를 Cookie에 추가

addJwtToCookie(String token, HttpServletResponse res):

  • URLEncoder.encode: JWT를 쿠키에 추가하기 전에 URL 인코딩을 수행합니다. 쿠키는 공백을 포함할 수 없기 때문입니다.
  • 쿠키(CookieO 생성: Authorization이라는 이름(AUTHORIZATION_HEADER로 설정됨)의 새로운 쿠키를 만들고 JWT를 값으로 설정합니다.
  • 응답에 추가: 쿠키는 HttpServletResponse 객체에 추가되고 클라이언트로 전송됩니다.
public void addJwtToCookie(String token, HttpServletResponse res) {
    try {
        token = URLEncoder.encode(token, "utf-8").replaceAll("\\+", "%20"); // Cookie Value 에는 공백이 불가능해서 encoding 진행

        Cookie cookie = new Cookie(AUTHORIZATION_HEADER, token); // Name-Value
        cookie.setPath("/");

        // Response 객체에 Cookie 추가
        res.addCookie(cookie);
    } catch (UnsupportedEncodingException e) {
        logger.error(e.getMessage());
    }
}

 

4. Token 추출

substringToken(String tokenValue):

  • 이 메서드는 Bearer 로 시작하는 문자열에서 JWT를 추출합니다. 문자열이 비어 있지 않고 올바른 접두사로 시작하는지 확인한 후 접두사(Bearer)를 제거하고 토큰을 반환합니다 (tokenValue.substring(7)).
   public String substringToken(String tokenValue) {
        if (StringUtils.hasText(tokenValue) && tokenValue.startsWith(BEARER_PREFIX)) {
            return tokenValue.substring(7);
        }
        logger.error("Not Found Token");
        throw new NullPointerException("Not Found Token");
    }

 

5. Token 검증

validateToken(String token):

  • 이 메서드는 토큰을 파싱하여 유효한지 확인합니다.
  • Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token):
    • 이 코드는 토큰을 디코딩하고 서명을 검증하여 토큰의 유효성을 검사합니다.
  • 서명이 유효하지 않거나, 만료되었거나, 지원되지 않는 형식일 경우 예외가 발생하며, 해당 예외는 각각 처리되고 오류 메시지가 기록됩니다.
  • 토큰이 유효하면 true를 반환하고, 그렇지 않으면 false를 반환합니다.
 public boolean validateToken(String token) {
        try {
            Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
            return true;
        } catch (SecurityException | MalformedJwtException | SignatureException e) {
            logger.error("Invalid JWT signature, 유효하지 않는 JWT 서명 입니다.");
        } catch (ExpiredJwtException e) {
            logger.error("Expired JWT token, 만료된 JWT token 입니다.");
        } catch (UnsupportedJwtException e) {
            logger.error("Unsupported JWT token, 지원되지 않는 JWT 토큰 입니다.");
        } catch (IllegalArgumentException e) {
            logger.error("JWT claims is empty, 잘못된 JWT 토큰 입니다.");
        }
        return false;
    }

 

6. Token에서 사용자 정보를 가져오기 

getUserInfoFromToken(String token):

  • Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody():
    • 이 코드는 토큰을 파싱하고, 사용자 클레임(토큰 생성 시 설정된 사용자 이름과 역할 등)을 포함하는 본문을 반환합니다.
    public Claims getUserInfoFromToken(String token) {
        return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
    }

 

7. HTTPServletRequest에서 Cookie Value: JWT 가져오기

public String getTokenFromRequest(HttpServletRequest req) {
        Cookie[] cookies = req.getCookies();
        if(cookies != null) {
            for (Cookie cookie : cookies) {
                if (cookie.getName().equals(AUTHORIZATION_HEADER)) {
                    try {
                        return URLDecoder.decode(cookie.getValue(), "UTF-8"); // Encode 되어 넘어간 Value 다시 Decode
                    } catch (UnsupportedEncodingException e) {
                        return null;
                    }
                }
            }
        }
        return null;
    }