이제 본격적으로 Spring boot에서 JWT 기반 Access Token, Refresh Token 활용 코드를 정리합니다.
참고로, 이 방법 외에도 다양한 구현 방법이 있을 수 있다는 점을 알립니다.
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
Spring security 를 사용하고 있어 필요한 부분입니다.
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.cors()
.and()
.csrf().disable()
.httpBasic().disable()
.formLogin().disable()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.exceptionHandling()
.authenticationEntryPoint(new CustomAuthenticationEntryPoint())
.and()
.addFilterBefore(
new JwtAuthenticationFilter(jwtTokenProvider, jwtService),
UsernamePasswordAuthenticationFilter.class);
return http.build();
}
몇 가지 설명을 남기면,
@Component
@RequiredArgsConstructor
public class JwtTokenProvider {
@Value("${JWT_SECRET_KEY}")
private String secretKey;
@Value("${ACCESS_TOKEN_EXPIRE_TIME}")
private Long tokenValidTime;
@Value("${REFRESH_TOKEN_EXPIRE_TIME}")
private Long refreshTokenValidTime;
private final UserDetailsService userDetailsService;
@PostConstruct
protected void init() {
secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes());
}
public String createToken(String email) {
Claims claims = Jwts.claims().setSubject(email);
Date now = new Date();
return Jwts.builder()
.setClaims(claims)
.setIssuedAt(now)
.setExpiration(new Date(now.getTime() + tokenValidTime))
.signWith(SignatureAlgorithm.HS256, secretKey)
.compact();
}
public String createRefreshToken() {
Date now = new Date();
return Jwts.builder()
.setIssuedAt(now)
.setExpiration(new Date(now.getTime() + refreshTokenValidTime))
.signWith(SignatureAlgorithm.HS256, secretKey)
.compact();
}
public Authentication getAuthentication(String token) {
UserDetails userDetails = userDetailsService.loadUserByUsername(getMemberEmail(token));
return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities());
}
public String getMemberEmail(String token) {
try {
return Jwts.parserBuilder()
.setSigningKey(secretKey)
.build()
.parseClaimsJws(token)
.getBody()
.getSubject();
} catch (ExpiredJwtException e) {
return e.getClaims().getSubject();
}
}
public String resolveToken(HttpServletRequest request) {
return JwtHeaderUtils.getAccessToken(request);
}
public boolean validateTokenExpiration(String token) {
try {
Jwts.parserBuilder().setSigningKey(secretKey).build().parseClaimsJws(token);
return true;
} catch (Exception e) {
return false;
}
}
}
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends GenericFilter {
private final JwtTokenProvider jwtTokenProvider;
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
String token = jwtTokenProvider.resolveToken((HttpServletRequest) request);
if (token != null && jwtTokenProvider.validateTokenExpiration(token)) {
Authentication authentication = jwtTokenProvider.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
chain.doFilter(request, response);
}
}
토큰 인증이 실패할 때 불릴 코드이며 앞서 SpringSecurity에서 등록했던 것을 리마인드합시다.
@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(
HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException)
throws IOException, ServletException {
//Access Token 만료 시 줄 응답에 대한 로직을 자유롭게 작성
}
}
프로젝트에서 사용한 코드는 범용성이 없어 생략하고, 로직의 흐름을 정리한다.
2편에서 살펴본 토큰 인증 플로우를 코드로 적용하였습니다.
이 과정에서 spring security를 활용했을 때, 배운 점은 다음과 같습니다.
추가로, 이후 유저 인증이 끝나면 식별을 어떻게 할 수 있을까요?
아래는 프로젝트에서 활용한 ArgumentResolver에서 유저 식별에 사용한 코드입니다.
public class LoginUserArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter parameter) {
boolean hasLoginAnnotation = parameter.hasParameterAnnotation(Login.class);
boolean hasMemberType = User.class.isAssignableFrom(parameter.getParameterType());
return hasLoginAnnotation && hasMemberType;
}
@Override
public Object resolveArgument(
MethodParameter parameter,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory)
throws Exception {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
PrincipalDetails principal = (PrincipalDetails) authentication.getPrincipal();
return principal.getUser();
}
}