JWT(JSON Web Token)는 당사자 간에 정보를 JSON 객체로 안전하게 전송하기 위한 개방형 표준(RFC 7519)입니다.
JWT의 구조:
@Slf4j
@Component
public class JwtTokenProvider {
private String secretKey;
private final long tokenValidityInMilliseconds;
private Key key;
public JwtTokenProvider(
@Value("${jwt.token-validity-in-milliseconds}") long tokenValidityInMilliseconds) {
this.tokenValidityInMilliseconds = tokenValidityInMilliseconds;
}
@PostConstruct
protected void init() {
try {
// HS512 알고리즘용 키 자동 생성
this.key = Keys.secretKeyFor(SignatureAlgorithm.HS512);
this.secretKey = Base64.getEncoder().encodeToString(key.getEncoded());
log.info("JWT secret key successfully generated for HS512");
} catch (Exception e) {
log.error("JWT secret key initialization error: ", e);
throw new RuntimeException("Failed to initialize JWT secret key", e);
}
}
// Access Token 생성
public String createAccessToken(String id, Long memberNo, String name, MemberRole role) {
return createToken(id, memberNo, name, role, tokenValidityInMilliseconds);
}
// Refresh Token 생성
public String createRefreshToken(String id, Long memberNo, String name, MemberRole role) {
return createToken(id, memberNo, name, role, tokenValidityInMilliseconds * 2);
}
// 토큰 생성 공통 메서드
private String createToken(String id, Long memberNo, String name,
MemberRole role, long validityInMilliseconds) {
Claims claims = Jwts.claims().setSubject(id);
claims.put("memberNo", memberNo);
claims.put("name", name);
claims.put("role", role.name());
Date now = new Date();
Date validity = new Date(now.getTime() + validityInMilliseconds);
return Jwts.builder()
.setClaims(claims)
.setIssuedAt(now)
.setExpiration(validity)
.signWith(key, SignatureAlgorithm.HS512)
.compact();
}
// 토큰 유효성 검증
public boolean validateToken(String token) {
try {
Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token);
return true;
} catch (SecurityException | MalformedJwtException e) {
log.warn("잘못된 JWT 서명입니다.");
} catch (ExpiredJwtException e) {
log.warn("만료된 JWT 토큰입니다.");
} catch (UnsupportedJwtException e) {
log.warn("지원되지 않는 JWT 토큰입니다.");
} catch (IllegalArgumentException e) {
log.warn("JWT 토큰이 잘못되었습니다.");
}
return false;
}
// 토큰에서 정보 추출 메서드들
public String getIdFromToken(String token) {
return parseClaims(token).getSubject();
}
public Long getMemberNoFromToken(String token) {
return parseClaims(token).get("memberNo", Long.class);
}
public MemberRole getRoleFromToken(String token) {
String roleStr = parseClaims(token).get("role", String.class);
return MemberRole.valueOf(roleStr);
}
private Claims parseClaims(String token) {
return Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody();
}
}
주요 구현 포인트:
1. 보안 키 자동 생성 (PostConstruct)
2. Access Token과 Refresh Token 분리
3. Claims에 사용자 정보 포함
4. 다양한 예외 상황 처리
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
@Slf4j
public class SecurityConfig {
private final JwtTokenProvider jwtTokenProvider;
private final CustomUserDetailsService userDetailsService;
private final OAuth2AuthenticationSuccessHandler oAuth2AuthenticationSuccessHandler;
private final OAuth2AuthenticationFailureHandler oAuth2AuthenticationFailureHandler;
@Bean
public DefaultOAuth2UserService defaultOAuth2UserService() {
return new DefaultOAuth2UserService();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.cors(cors -> cors.disable())
.csrf(csrf -> csrf.disable())
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers(
"/",
"/auth/**",
"/oauth2/**",
"/login/**",
"/resources/**",
"/css/**",
"/js/**",
"/images/**"
).permitAll()
.anyRequest().authenticated()
)
.oauth2Login(oauth2 -> oauth2
.authorizationEndpoint(authorization -> authorization
.baseUri("/oauth2/authorization"))
.redirectionEndpoint(redirection -> redirection
.baseUri("/login/oauth2/code/*"))
.userInfoEndpoint(userInfo -> userInfo
.userService(defaultOAuth2UserService()))
.successHandler(oAuth2AuthenticationSuccessHandler)
.failureHandler(oAuth2AuthenticationFailureHandler)
);
return http.build();
}
}
기본 설정:
URL 권한 설정:
.authorizeHttpRequests(auth -> auth
.requestMatchers("/auth/**", "/oauth2/**", "/login/**").permitAll()
.anyRequest().authenticated()
)
OAuth2 설정:
/oauth2/authorization/login/oauth2/code/*