지난 편에서는 Access Token 발급을 구현하였고 포스트맨으로 Access Token 값이 정상적으로 반환되는 것을 확인할 수 있었습니다. 이번에는 발급 받은 Access Token을 활용하여 인증을 처리하는 로직을 구현하고 테스트를 해보겠습니다.
MemberSecurity는 CustomUserDetails에 포함되어 있는 클래스로 회원 정보가 담겨져 있습니다.
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MemberSecurity {
private String uuid;
private String role;
}
다음으로는 CustomUserDetails입니다. CustomUserDetails은 UserDetails을 상속 받고 있으며 스프링 시큐리티 인증 토큰을 생성할 때 필요한 클래스입니다. 내부에는 MemberSecurity를 가지고 있습니다.
@RequiredArgsConstructor
public class CustomUserDetails implements UserDetails {
private final MemberSecurity memberSecurity;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> collection = new ArrayList<>();
collection.add(new GrantedAuthority() {
@Override
public String getAuthority() {
return memberSecurity.getRole();
}
});
return collection;
}
@Override
public String getPassword() {
return null;
}
@Override
public String getUsername() {
return memberSecurity.getUuid();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
JWT 토큰을 검증하기 위해서는 커스텀 필터가 필요합니다. 커스텀 필터는 OncePerRequestFilter를 상속받고 있는데 여기서 OncePerRequestFilter는 하나의 HTTP 요청당 필터가 단 한 번만 실행되도록 보장합니다. 이는 필터가 여러 번 실행되는 것을 방지하고, 효율성을 높이며, 불필요한 처리와 성능 저하를 방지합니다.
각 로직의 설명은 코드에 주석으로 작성하였습니다.
@RequiredArgsConstructor
public class JwtFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//Authorization 이름을 가진 헤더의 값을 가져옵니다.
String accessToken= request.getHeader("Authorization");
//가져온 값이 null 이거나 Bearer로 시작하지 않으면 다음 필터로 이동합니다.
if (accessToken == null || !accessToken.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
//Access Token 값을 가져옵니다.
String token = accessToken.split(" ")[1];
//가져온 Access Token이 만료되었는지 확인하고 만료되었다면 에러 response를 설정하고 리턴합니다.
try {
jwtUtil.isExpired(token);
}
catch (ExpiredJwtException e){
PrintWriter writer = response.getWriter();
writer.write("Access Token Expired");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
//Access Token의 category 값을 가져옵니다.
String category = jwtUtil.getCategory(token);
//category가 access가 아닌 경우에 에러 response를 설정하고 리턴합니다.
if(!category.equals("access")){
PrintWriter writer = response.getWriter();
writer.write("Invalid Access Token");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
//Access Token의 uuid와 role 값을 가져옵니다.
String uuid = jwtUtil.getUuid(token);
String role = jwtUtil.getRole(token);
//가져온 값으로 MemberSecurity을 생성합니다.
MemberSecurity memberSecurity = MemberSecurity.builder()
.uuid(uuid)
.role(role)
.build();
//memberSecurity으로 CustomUserDetails을 생성합니다.
CustomUserDetails customUserDetails = new CustomUserDetails(memberSecurity);
//스프링 시큐리티 인증 토큰을 생성합니다.
Authentication authToken = new UsernamePasswordAuthenticationToken(customUserDetails, null, customUserDetails.getAuthorities());
//세션에 사용자 등록을 진행합니다.
SecurityContextHolder.getContext().setAuthentication(authToken);
//다음 필터로 이동합니다.
filterChain.doFilter(request, response);
}
}
TestController는 인증이 잘 되는지 확인하는 컨트롤러입니다. /test url로 GET 요청을 했을 때 Test Success가 반환이 되면 성공적으로 인증 처리가 된 것입니다.
@RestController
public class TestController {
@GetMapping("/test")
public String test(){
return "Test Success";
}
}
위에서 작성한 JwtFilter를 필터에 추가하였습니다. 또한 /test url로 GET 요청을 할 때는 인증이 필요하다는 설정을 해주었습니다.
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtUtil jwtUtil;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
//JWTFilter 추가
//JwtFilter를 UsernamePasswordAuthenticationFilter 전에 설정하는 것은 JwtFilter가 JWT 인증을
//처리하고, 인증 정보를 SecurityContext에 설정한 후, 이후 필터들이 이 정보를 활용할 수 있게 하기 위해서 입니다.
http
.addFilterBefore(new JwtFilter(jwtUtil), UsernamePasswordAuthenticationFilter.class);
//경로별 인가 작업
http
.authorizeHttpRequests((auth) -> auth
.requestMatchers(HttpMethod.GET,"/test").authenticated()
.anyRequest().permitAll());
}
}
첫 번째 케이스는 Access Token을 포함하지 않고 요청을 했을 때 입니다. 결과를 보면 Invalid Access Token와 401 상태 코드를 확인할 수 있습니다.
두 번째 케이스는 Access Token을 포함하고 요청을 했을 때 입니다. 결과를 보면 Test Success와 200 상태 코드를 확인할 수 있습니다.
이렇게 Access Token을 발급 받고 인증을 처리하는 필터까지 구현하였습니다. 다음 포스트에서는 Refresh Token를 발급받는 로직을 작성해보겠습니다!