프로젝트에서 Spring Security + JWT + Redis 방식으로 인증/인가 로직을 구현하였고, 전체 과정을 기록으로 남겨두려 한다.
// JwtAuthenticationEntryPoint
@Slf4j
@Component
@RequiredArgsConstructor
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
private final ObjectMapper objectMapper;
private final String UTF_8 = "utf-8";
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setCharacterEncoding(UTF_8);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.getWriter().write(
objectMapper.writeValueAsString(
ResponseDto.create(AUTHENTICATION_FAILED.getMessage())
)
);
}
}
// ExceptionMessage
public enum ExceptionMessage {
AUTHENTICATION_FAILED("인증 실패"),
AUTHORIZATION_FAILED("접근 권한 없음");
private final String message;
ExceptionMessage(String message) {
this.message = message;
}
public String getMessage() {
return this.message;
}
}
// SecurityConfig
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtTokenProvider jwtTokenProvider;
private final ObjectMapper objectMapper;
...
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.httpBasic().disable()
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers(HttpMethod.GET, "/~~~").permitAll()
.antMatchers("/~~~").authenticated()
.regexMatchers(HttpMethod.POST, "/~~~").authenticated()
.anyRequest().authenticated()
.and()
.and()
.exceptionHandling()
.authenticationEntryPoint(new JwtAuthenticationEntryPoint(objectMapper))
// addFilterBefore() 메서드를 통해 Filter를 등록할 수 있다.
.addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider, objectMapper),
UsernamePasswordAuthenticationFilter.class);;
return http.build();
}
...
commence()
메서드의 인자로 AuthenticationException authException
을 받아오지만, AuthenticationFilter에서 직접 발생시킨 예외는 해당 인자로 담겨오지 않는다.// JwtAuthenticationFilter
...
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
// try {
// 1. Request Header 로부터 JWT 토큰 받아옴
String token = jwtTokenProvider.resolveToken((HttpServletRequest) request);
// 2. JwtTokenProvider.validateToken() 으로 유효성 검사 진행
if (token != null && jwtTokenProvider.validateToken(token)) {
// 토큰 유효할 경우, 토큰으로부터 Authentication 객체를 받아와 SecurityContext에 저장
Authentication authentication = jwtTokenProvider.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
// } catch (TokenException e) {
// response.setStatus(HttpStatus.UNAUTHORIZED.value());
// response.setCharacterEncoding(UTF_8);
// response.setContentType(MediaType.APPLICATION_JSON_VALUE);
//
// response.getWriter().write(
// objectMapper.writeValueAsString(
// ResponseDto.create(e.getMessage())
// )
// );
// }
}
...
jwtTokenProvider.validateToken()
메서드를 통해 JWT 유효성 검사를 수행하는 것을 확인할 수 있다.validateToken()
메서드를 다시 가져와보자.// JwtTokenProvider
public boolean validateToken(String token) throws TokenException {
try {
Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
return true;
} catch (SecurityException | MalformedJwtException | UnsupportedJwtException | IllegalArgumentException e) {
throw new InvalidTokenException(INVALID_TOKEN.getMessage());
} catch (ExpiredJwtException e) {
throw new ExpiredTokenException(EXPIRED_TOKEN.getMessage());
}
}
AuthenticationEntryPoint.commence()
메서드의 AuthenticationException authException
인자로 전달될 줄 알았는데 아니었다..!JwtTokenProvider.validateToken()
메서드가 호출되어 유효성 검사를 수행한다.validateToken()
메서드에서 어떻게 유효하지 않은지에 대한 정보를 담은 예외를 발생시킨다.token
변수에 null이 담겨 jwtTokenProvider.validateToken()
메서드를 수행하지 않고 filterChain.doFilter()
로 다음 Filter로 넘어간다.filterChain.doFilter()
도 try 문 안에 넣어두었다.// JwtAccessDeniedHandler
@Component
@RequiredArgsConstructor
public class JwtAccessDeniedHandler implements AccessDeniedHandler {
private final ObjectMapper objectMapper;
private final String UTF_8 = "utf-8";
@Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException, ServletException {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding(UTF_8);
response.getWriter().write(
objectMapper.writeValueAsString(
ResponseDto.create(ExceptionMessage.AUTHORIZATION_FAILED.getMessage())
)
);
}
}
// SecurityConfig
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtTokenProvider jwtTokenProvider;
private final ObjectMapper objectMapper;
...
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.httpBasic().disable()
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers(HttpMethod.GET, "/~~~").permitAll()
.antMatchers("/~~~").authenticated()
.regexMatchers(HttpMethod.POST, "/~~~").authenticated()
.anyRequest().authenticated()
.and()
.and()
.exceptionHandling()
.accessDeniedHandler(new JwtAccessDeniedHandler(objectMapper))
.authenticationEntryPoint(new JwtAuthenticationEntryPoint(objectMapper))
// addFilterBefore() 메서드를 통해 Filter를 등록할 수 있다.
.addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider, objectMapper),
UsernamePasswordAuthenticationFilter.class);;
return http.build();
}
...