[ํธ๋ฌ๋ธ์ํ ] Spring Boot JWT ์ธ์ฆ ํ๋ฆ ๋ง๋ค๊ณ 200 OK๋ง ๋์ค๋ ๋ฌธ์ ํด๊ฒฐ
๐ฅ JWT ์ธ์ฆ ๊ตฌํ & ํํฐ ํธ๋ฌ๋ธ์ํ
ํ๋ก์ ํธ์์ JWT ์ธ์ฆ์ ์ฒ์์ผ๋ก ์ง์ ๊ตฌํํด ๋ดค๋ค.
ํ์๊ฐ์
๊ณผ ๋ก๊ทธ์ธ๊น์ง๋ ์ ์์ ์ผ๋ก ๋์ํ์ง๋ง.. ๋ฌธ์ ๋ ๊ทธ ์ดํ์๋ค.
Controller๋ก ์์ฒญ์ด ๋์ด๊ฐ์ง ์๊ณ , 200 OK๋ง ๋์ค๋ ํ์์ด ๋ฐ์ํ๋ค.
๋ฌธ์ ๋ฅผ ์ฐพ์ง ๋ชปํด์ ํ์ฐธ์ ํค๋งค๋ค๊ฐ, ๊ฒฐ๊ตญ ์ฒ์๋ถํฐ ์ฝ๋๋ฅผ ๋ค์ ์ ๋ฆฌํ๋ฉด์ ํด๊ฒฐํ๋ค.
โ๏ธ JwtUtil ํด๋์ค
JWT๋ฅผ ๋ฐ๊ธํ๊ณ , ๊ฒ์ฆํ๊ณ , Claim ์ ๋ณด๋ฅผ ์ถ์ถํ๋ ์ญํ ์ ๋ด๋นํ๋ ํด๋์ค์ด๋ค.
@Slf4j(topic = "JwtUtil")
@Component
@RequiredArgsConstructor
public class JwtUtil {
private static final String BEARER_PREFIX = "Bearer ";
private static final long ACCESS_TOKEN_EXPIRATION = 30 * 60 * 1000; // 30๋ถ
@Value("${jwt.secret.key}")
private String secretKey;
private Key key;
private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
// ์ ํ๋ฆฌ์ผ์ด์
์์ ์ SecretKey ์ด๊ธฐํ
@PostConstruct
public void init() {
if (!StringUtils.hasText(secretKey)) {
log.error("JWT secret key is null or empty.");
throw new BadRequestException(ErrorCode.MISSING_JWT_SECRET_KEY);
}
try {
byte[] bytes = Base64.getDecoder().decode(secretKey);
key = Keys.hmacShaKeyFor(bytes);
log.info("JWT secret key initialized successfully");
} catch (IllegalArgumentException e) {
log.error("Failed to decode JWT secret key: {}", e.getMessage());
throw new InternalServerException(ErrorCode.INVALID_JWT_SECRET_KEY);
}
}
// ๐น AccessToken ์์ฑ
public String createToken(Long memberId, String email, String nickname, Authority authority) {
Date now = new Date();
return BEARER_PREFIX + Jwts.builder()
.setSubject(String.valueOf(memberId))
.claim("email", email)
.claim("nickname", nickname)
.claim("authority", authority)
.setIssuedAt(now)
.setExpiration(new Date(now.getTime() + ACCESS_TOKEN_EXPIRATION))
.signWith(key, signatureAlgorithm)
.compact();
}
// ๐น ํ ํฐ ์ ํจ์ฑ ๊ฒ์ฆ
public boolean validateToken(String token) {
try {
token = removeBearer(token);
Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
return true;
} catch (Exception e) {
log.error("Invalid JWT token: {}", e.getMessage());
return false;
}
}
// ๐น ํ ํฐ Claim ํ์ฑ
public Claims parseClaims(String token) {
try {
return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
} catch (ExpiredJwtException e) {
throw new UnauthorizedException(ErrorCode.EXPIRED_JWT_TOKEN);
}
}
// ๐น ํ ํฐ์์ ์ ๋ณด ์ถ์ถ
public Long getMemberId(String token) {
return Long.valueOf(parseClaims(removeBearer(token)).getSubject());
}
public String getEmail(String token) {
return parseClaims(removeBearer(token)).get("email", String.class);
}
public String getAuthority(String token) {
return parseClaims(removeBearer(token)).get("authority", String.class);
}
// ๐น "Bearer " ์ ๋์ด ์ ๊ฑฐ
private String removeBearer(String token) {
if (token != null && token.startsWith(BEARER_PREFIX)) {
return token.substring(BEARER_PREFIX.length());
}
return token;
}
}
โ๏ธ JwtFilter ํด๋์ค
๋ชจ๋ ์์ฒญ์ ๊ฐ๋ก์ฑ์ ํ ํฐ ๊ฒ์ฆ์ ์ํํ๋ ํํฐ์ด๋ค.
@Slf4j(topic = "JwtFilter")
@RequiredArgsConstructor
public class JwtFilter implements Filter {
private final JwtUtil jwtUtil;
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
String requestURI = httpRequest.getRequestURI();
// ํ์๊ฐ์
, ๋ก๊ทธ์ธ์ ์ธ์ฆ์์ด ํต๊ณผ
if (requestURI.startsWith("/api/auth/")) {
chain.doFilter(httpRequest, httpResponse);
return;
}
String bearerJwt = httpRequest.getHeader("Authorization");
if (bearerJwt == null || !bearerJwt.startsWith("Bearer ")) {
log.error("Invalid Authorization header on URI: {}", requestURI);
throw new UnauthorizedException(ErrorCode.INVALID_BEARER_TOKEN);
}
String token = bearerJwt.substring(7);
if (!jwtUtil.validateToken(token)) {
throw new UnauthorizedException(ErrorCode.INVALID_JWT_TOKEN);
}
// ํ ํฐ์์ ์ ๋ณด ์ถ์ถ
Long memberId = jwtUtil.getMemberId(token);
String email = jwtUtil.getEmail(token);
String authority = jwtUtil.getAuthority(token);
// ์์ฒญ์ ์ฌ์ฉ์ ์ ๋ณด ์ ์ฅ
httpRequest.setAttribute("memberId", memberId);
httpRequest.setAttribute("email", email);
httpRequest.setAttribute("authority", authority);
chain.doFilter(httpRequest, httpResponse);
}
}
โ FilterConfig ํด๋์ค
JwtFilter๋ฅผ ์คํ๋ง์ ๋ฑ๋กํ๋ ์ค์ ํด๋์ค์ด๋ค.
@Configuration
@RequiredArgsConstructor
public class FilterConfig {
private final JwtUtil jwtUtil;
@Bean
public FilterRegistrationBean<JwtFilter> jwtFilter() {
FilterRegistrationBean<JwtFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new JwtFilter(jwtUtil));
registrationBean.addUrlPatterns("/api/*"); // /api/ ํ์ ๊ฒฝ๋ก์๋ง ์ ์ฉ
registrationBean.setOrder(1); // ํํฐ ์์ ์ง์
return registrationBean;
}
}
โก ํธ๋ฌ๋ธ์ํ ๊ณผ์ ์ ๋ฆฌ
1. ๋ฌธ์ ์ํฉ
๐ JWT๋ฅผ ์ ์ฉํ๋๋ฐ ํ์๊ฐ์
/๋ก๊ทธ์ธ์ ์ ์์ ์ผ๋ก ์ฒ๋ฆฌ๋์๋ค.
๐ ํ์ง๋ง ์ดํ ์์ฒญ์ด Controller๋ก ๋์ด๊ฐ์ง ์๊ณ , 200 OK ์๋ต๋ง ๋จ๋ ํ์์ด ๋ฐ์ํ๋ค.
2. ๋ฌธ์ ์์ธ
๐ ํํฐ(JwtFilter) ์์์ ์์ธ๋ฅผ ๋ฐ์์ํค๋ ๊ฒฝ์ฐ ๋ณ๋ค๋ฅธ ์์ธ ์ฒ๋ฆฌ๋ฅผ ํด์ฃผ์ง ์์ผ๋ฉด ๊ทธ๋ฅ "200 OK"๋ก ๋๋๋ฒ๋ฆด ์ ์๋ค.
๐ ํนํ throw ์ดํ ๋ณ๋๋ก response๋ฅผ ์์ฑํ๊ฑฐ๋ ์์ธ๋ฅผ ํธ๋ค๋งํ์ง ์์ผ๋ฉด, ํด๋ผ์ด์ธํธ๋ ์๋ฌด๊ฒ๋ ์๋ 200 OK๋ฅผ ๋ฐ๊ฒ ๋๋ค.
3. ํด๊ฒฐ ๋ฐฉ๋ฒ
๐ ๋ฌธ์ ๋ฅผ ์ ํํ ํ์
ํ๊ธฐ ์ํด ์ฝ๋๋ฅผ ์ฐจ๊ทผ์ฐจ๊ทผ ๋ค์ ์์ฑํ๋ค.
๐ ๊ทธ๋ฆฌ๊ณ ํํฐ ์์์ ๋ฐ์ํ๋ ์์ธ๋ฅผ Spring ์์ธ ์ฒ๋ฆฌ ํ๋ฆ์ ์ ๋๋ก ํ์์ ControllerAdvice ๊ฐ์ ๊ธ๋ก๋ฒ ์์ธ ์ฒ๋ฆฌ๊ฐ ์ก๋๋ก ์ค๊ณํ๋ค.
4. ๊ตํ
๐ "์๋ฌ๊ฐ ๋ฐ์ํ์ผ๋ฉด ๋ช
ํํ ํด๋ผ์ด์ธํธ์๊ฒ ์๋ฌ๋ฅผ ์๋ ค์ผ ํ๋ค."
๐ ํํฐ๋ ์ธํฐ์
ํฐ ๊ฐ์ "์์ฒญ ํ๋ฆ์ ์ ์ดํ๋ ๋ ์ด์ด"์์๋ ์์ธ ์ฒ๋ฆฌ๋ฅผ ๋ ๊ผผ๊ผผํ ์ ๊ฒฝ ์จ์ผ ํ๋ค.
์ด๋ฒ ๊ฒฝํ์ ํตํด JWT ์ธ์ฆ ํ๋ฆ์ ์ค์ค๋ก ์ค๊ณํ๊ณ , ํํฐ์์ ์์ฒญ ํ๋ฆ์ ์ ์ดํ๋ ๋ฒ์ ์ดํดํ๊ณ , ๋ฌธ์ ๊ฐ ์๊ฒผ์ ๋ ์ฒ์๋ถํฐ ๋ค์ ์ ๊ฒํ๋ ์ต๊ด์ ์ค์์ฑ์ ๋ชจ๋ ๋ฐฐ์ธ ์ ์์๋ค.
์์ผ๋ก๋ ์ด๋์ ๊ฐ ์๋ต์ด ๋๊ธด๋ค๋ฉด ์์ฒญ ํ๋ฆ์ ํ ๋จ๊ณ์ฉ ๊ผผ๊ผผํ ๋ฐ๋ผ๊ฐ ๋ด์ผ๊ฒ ๋ค!