package com.ga.common.auth; import com.ga.common.exception.BizException; import com.ga.common.exception.ErrorCode; import io.jsonwebtoken.Claims; import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.JwtException; import io.jsonwebtoken.security.Keys; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import javax.crypto.SecretKey; import java.nio.charset.StandardCharsets; import java.util.Date; import java.util.List; import java.util.Map; /** * JWT(JSON Web Token)를 발급하고 검증하는 핵심 유틸 컴포넌트. * *

왜 필요한가: 로그인 성공 시 여기서 토큰을 만들어 클라이언트에 주고, 이후 요청마다 * {@link JwtAuthFilter}가 여기서 토큰을 검증한다. 즉 인증 토큰의 "생성"과 "해석"을 한곳에 모은 것. * *

토큰 종류: access(짧은 수명, 실제 API 호출용)와 refresh(긴 수명, access 재발급용) 두 가지. * 서명 비밀키(secret)와 만료시간(ms)은 application 설정(ga.jwt.*)에서 주입받는다. */ @Slf4j @Component public class JwtTokenProvider { private final SecretKey key; // HMAC 서명에 쓰는 비밀키(이 키로 서명/검증) private final long accessExpire; // access 토큰 만료시간(ms) private final long refreshExpire; // refresh 토큰 만료시간(ms) // 생성자 주입: 설정값(@Value)이 없으면 기본값(access 2시간, refresh 7일) 사용. public JwtTokenProvider( @Value("${ga.jwt.secret}") String secret, @Value("${ga.jwt.access-token-expire:7200000}") long accessExpire, @Value("${ga.jwt.refresh-token-expire:604800000}") long refreshExpire ) { byte[] raw = secret.getBytes(StandardCharsets.UTF_8); // HMAC-SHA256은 키 길이가 최소 256비트(32바이트)여야 한다. 설정값이 짧으면 0으로 패딩해 길이를 맞춘다. if (raw.length < 32) { byte[] padded = new byte[32]; System.arraycopy(raw, 0, padded, 0, raw.length); raw = padded; } this.key = Keys.hmacShaKeyFor(raw); this.accessExpire = accessExpire; this.refreshExpire = refreshExpire; } /** 로그인 시 발급하는 access 토큰. 사용자 ID/로그인ID/역할을 담는다. */ public String createAccessToken(Long userId, String loginId, List roles) { return create(userId, loginId, roles, accessExpire, "ACCESS"); } /** access 만료 시 재발급에 쓰는 refresh 토큰. 역할은 담지 않고 수명이 길다. */ public String createRefreshToken(Long userId, String loginId) { return create(userId, loginId, List.of(), refreshExpire, "REFRESH"); } // 토큰 생성 공통 로직: subject=로그인ID, claims에 uid/roles/type, 발급/만료 시각을 넣고 비밀키로 서명. private String create(Long userId, String loginId, List roles, long expireMs, String type) { Date now = new Date(); return Jwts.builder() .subject(loginId) .claims(Map.of( "uid", userId, "roles", roles, "type", type )) .issuedAt(now) .expiration(new Date(now.getTime() + expireMs)) .signWith(key) .compact(); } /** * 토큰을 비밀키로 검증하고 안의 claims(uid, roles 등)를 꺼낸다. * 만료/위조 시 일반 예외 대신 업무 예외(BizException)로 바꿔 던져, 상위에서 표준 에러응답으로 처리하게 한다. */ public Claims parse(String token) { try { return Jwts.parser().verifyWith(key).build() .parseSignedClaims(token).getPayload(); } catch (ExpiredJwtException e) { throw new BizException(ErrorCode.TOKEN_EXPIRED); // 만료된 토큰 } catch (JwtException | IllegalArgumentException e) { throw new BizException(ErrorCode.TOKEN_INVALID); // 서명 불일치/형식 오류 등 } } /** 예외를 던지지 않고 true/false 로만 유효성을 확인하고 싶을 때 사용하는 편의 메서드. */ public boolean isValid(String token) { try { parse(token); return true; } catch (Exception e) { return false; } } // 만료시간 조회용 getter. 로그인 응답에 "토큰 만료까지 남은 시간"을 함께 내려줄 때 등에 쓰인다. public long getAccessExpire() { return accessExpire; } public long getRefreshExpire() { return refreshExpire; } }