JWT를 적용하기 위해서는 다양한 방법이 존재하지만
제가 사용한 방식의 구현 단계는 아래와 같습니다.
@Configuration
@EnableWebSecurity
public class SecurityConfiguration {
@Bean
public SecurityFilterChain basicConfig (HttpSecurity http) throws Exception {
http
.formLogin(AbstractHttpConfigurer::disable)
.sessionManagement((session) -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
...
...
기존에 학습 시에도 JWT를 적용했었다.
일전에 살펴보았던 SecurityFilterchain 작성법에서
위 3가지 코드를 사용하면 토큰 방식을 활용하는데 필요한 기초세팅이 끝난다.
formLogin(AbstractHttpConfigurer::disable)
은 말그대로 폼로그인 방식을 활용할 경우 활성화하는 설정이며
sessionManagement
설정은 session 방식을 활용해서 로그인을 구현할때 사용된다.
위와같이 STATELESS 로 적용할 경우 session 방식을 사용하지 않는다는 의미로 적용된다.
추가로 formLogin
일때 사용되는 필터인
JWT를 생성하는 토큰 생성기 입니다.
@Getter
@Value("${jwt.key}")
private String secretKey;
@Getter
@Value("${jwt.access-token-expiration-minutes}")
private int accessTokenTime;
@Getter
@Value("${jwt.refresh-token-expiration-minutes}")
private int refreshTokenTime;
기본적으로 JWT 설정관련된 파일은 위와같이 환경변수 처리하여
키값의 유출로부터 보호합니다.
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
@Override
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain,
Authentication authResult) throws IOException {
토큰 인증성공후 로직 작성
...
...
...
}
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException failed) throws IOException {
토큰 인증실패후 로직 작성
...
...
...
}
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException {
// 유효한 자격증명을 제공하지 않고 접근하려 할때 401
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
}
}
@Component
public class JwtAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request,
HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException {
//필요한 권한이 없이 접근하려 할때 403
response.sendError(HttpServletResponse.SC_FORBIDDEN);
}
}
기존에 활용했던 방식은 UsernamePasswordAuthenticationFilter
를 상속받아 구현체인 JwtAuthenticationFilter
를 구현하여 인증성공과 실패시의 로직을 작성했지만
변경된 코드에서는 AccessDeniedHandler
와 AuthenticationEntryPoint
를 활용해서 Security 인증절차가 진행되는 과정에서 예외처리를 진행하였습니다.
더 자세한 사항은 공식문서를 참조해주시기 바랍니다.
@Override
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain,
Authentication authResult) throws IOException {
Member member = (Member) authResult.getPrincipal();
String accessToken = delegateAccessToken(member);
Long memberId = member.getMemberId();
Date expiration = jwtTokenizer.getTokenExpiration(jwtTokenizer.getAccessTokenTime());
String TokenExpirationDate = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(expiration);
response.setHeader("TokenExpiration", TokenExpirationDate);
String refreshToken = delegateRefreshToken(member);
String nickName = member.getNickName();
String profileImage = member.getProfileImage();
response.setHeader("Authorization", "Bearer " + accessToken);
response.setHeader("Refresh", refreshToken);
response.setHeader("memberId", String.valueOf(memberId));
Map<String, Object> responseMessage = new HashMap<>();
responseMessage.put("nickName", nickName);
responseMessage.put("profileImage", profileImage);
String responseBody = new ObjectMapper().writeValueAsString(responseMessage);
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
response.getWriter().write(responseBody);
}
또한 UsernamePasswordAuthenticationFilter
를 구현하여 필요한 유저 정보를 하나하나 전달하다보니 코드 내용이 길어지고 가독성이 떨어졌습니다.
// 토큰에 유저정보 담기
CustomUserDetails userDetails = (CustomUserDetails) authentication.getPrincipal();
Long memberId = userDetails.getMemberId();
String email = userDetails.getUsername();
String accessToken = Jwts.builder()
.setSubject(authentication.getName())
.claim(AUTHORITIES_KEY, authorities)
// 필요한 정보만 추가
.claim("memberId", memberId)
.signWith(key, SignatureAlgorithm.HS512)
.setExpiration(validity)
.compact();
UsernamePasswordAuthenticationToken
을 활용하여 UserDetails
UserDetailsService
로 부터 보다 간편하게 회원정보를 넘겨받아 토큰에 추가하도록 하여, 토큰에 담긴 정보들을 활용했습니다.
Spring Security 공식문서에서는 JWT 관련 세팅 정보를 제공하지 않아서 그밖의 정보들은 블로그를 참조하여 구현하였습니다.