낮에 JwtTokenFilter에서 Exception을 추가를 시도해 보았지만, AuthenticationEntryPoint를 추가하지 않아 오류가 났었다.
곰곰히 생각해보니, JwtTokenFilter에서 토큰이 없거나, 토큰이 잘못된 경우 Exception을 throw하여 json형식으로 view에 출력하는 것이 좋을 것 같아 추가해 보았다.
다만, Jwt token형식을 주어 token이 잘못되었을 때 testcode를 작성하고 싶었으나, 실패하였다.
이에 대해서는 좀 더 고민이 필요 할 듯 하다.
1. Configuration
JwtTokenFilter
@RequiredArgsConstructor
@Slf4j
public class JwtTokenFilter extends OncePerRequestFilter {
private final UserService userService;
private final String secretKey;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
final String authorizationHeader = request.getHeader(HttpHeaders.AUTHORIZATION); //헤더에 토큰을 넘긴 것을 가져옴.
//토큰이 없는 경우
if(authorizationHeader == null) {
request.setAttribute("exception", ErrorCode.INVALID_PERMISSION.name());
filterChain.doFilter(request, response);
return;
}
//bearer로 시작하는 토큰이 아닌 경우
if(!authorizationHeader.startsWith("Bearer ")) {
request.setAttribute("exception", ErrorCode.INVALID_TOKEN.name());
filterChain.doFilter(request, response);
return;
}
//bearer 이후 문자열 token 분리 성공 실패
String token;
try {
token = authorizationHeader.split(" ")[1];
//만료된 토큰일 경우
if(JwtTokenUtil.isExpired(token, secretKey)) {
request.setAttribute("exception", ErrorCode.INVALID_TOKEN.name());
filterChain.doFilter(request, response);
return;
};
// Token에서 UserName꺼내기 (JwtTokenUtil에서 Claim에서 꺼냄)
String userName = JwtTokenUtil.getUserName(token, secretKey);
User user = userService.tokenGetUserByUserName(userName);
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(), null
, List.of(new SimpleGrantedAuthority(user.getRole().name())));
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); //권한 부여
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
filterChain.doFilter(request,response);
} catch (Exception e) {
request.setAttribute("exception", ErrorCode.INVALID_TOKEN.name());
filterChain.doFilter(request, response);
}
}
}
request에 exception을 추가하여 각 상황에 맞는 ErrorCode (Enum)의 name을 넣어주었다.
- 토큰이 없는 경우 : INVALID_PERMISSION
- 토큰이 잘못 된 경우 : INVALID_TOKEN
위와 같이 분류하였으며, 두가지 경우 모두 HttpStatus는 UNAUTHORIZED (401) 으로 동일하다.
requests에 exception을 담아주면, AuthenticationEntryPoint에서 ErrorCode (Enum)의 name에 따라 각각 response를 설정하여 view에 출력하게 된다.
CustomAuthenticationEntryPoint
@Component
@Slf4j
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
private ObjectMapper objectMapper = new ObjectMapper();
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
String exception = (String)request.getAttribute("exception");
if(exception.equals(ErrorCode.INVALID_TOKEN.name())) {
setResponse(response, ErrorCode.INVALID_TOKEN);
}
else if(exception.equals(ErrorCode.INVALID_PERMISSION.name())) {
setResponse(response, ErrorCode.INVALID_PERMISSION);
}
}
private void setResponse(HttpServletResponse response, ErrorCode errorCode) throws IOException {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(errorCode.getStatus().value());
response.getWriter().print(
objectMapper.writeValueAsString(
Response.error(new ErrorResponse(errorCode.name(), errorCode.getMessage()))
)
);
}
}
JwtTokenFilter에서 exception을 설정하였을 때, 위의 AuthenticationEntryPoint이 실행되면서 각각 exception에 따른 response를 출력하게 된다.
response의 setStatus는 HttpStatus형태가 아니라 int형으로 지정해 주어야 하여 errorCode.getStatus().value()를 통해 int로 바꾸어주었다.
response형태는 controller의 exception출력 형태와 동일한 형태로 하기 위해, Response.error로 감싸주었고 ObjectMapper를 이용하여 json형태로 만들어 주었다.
<Reference>
https://stackoverflow.com/questions/59873404/how-to-convert-httpstatus-code-to-int-in-java
SecurityConfig
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
return httpSecurity
.httpBasic().disable()
.csrf().disable()
.cors().and()
.exceptionHandling()
.authenticationEntryPoint(new CustomAuthenticationEntryPoint())
.and()
.authorizeRequests()
.antMatchers(PERMIT_URL_ARRAY).permitAll()
.antMatchers(HttpMethod.POST,"/api/**").authenticated() // post는 인가자만 허용
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS) // jwt사용하는 경우 씀
.and()
.addFilterBefore(new JwtTokenFilter(userService, secretKey), UsernamePasswordAuthenticationFilter.class) //UserNamePasswordAuthenticationFilter적용하기 전에 JWTTokenFilter를 적용
.build();
}
SecurityConfig에 위에서 설정한 CustomAuthenticationEntryPoint에 대해 추가하였다.
.exceptionHandling() 과 .authenticationEntryPoint(new CustomAuthenticationEntryPoint())가 추가되었다.
<Reference>
https://sas-study.tistory.com/362
https://sol-devlog.tistory.com/20
TestCode
권한이 있는 유저는 @WithMockUser로, 권한이 없는 유저는 @WithAnonymousUser annotation을 통해 간단히 테스트가 가능하다.
다만, 세부유저 설정을 통해 만료된 토큰, 토큰 형태가 Baerer가 아닌경우 등 잘못된 토큰을 가지고 있을 때의 테스트를 하고 싶었다.
다양한 시도에 대해 찾아보았지만, 해결되지는 못했다.
1. mockMvc header에 token 부여
String token = "eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyTmFtZSI6ImFkbWluIiwiaWF0IjoxNjcxNjg1MDU5LCJleHAiOjE2NzE2ODg2NTl9.DQxLPeon2UZ0la3z6seo3beHwR1r6RhacBpnOguF1xg";
MvcResult mvcResult = mockMvc.perform(post("/api/v1/posts")
.header(HttpHeaders.AUTHORIZATION, "Bearer " + token)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsBytes(postCreateRequest)))
.andExpect(status().isUnauthorized())
.andDo(print())
.andReturn();
위와 같이 만료된 token을 설정하고 header에 부여한 후 mockMvc를 생성하여 test하였지만 403 error가 발생하였다.
<Reference>
https://stackoverflow.com/questions/45241566/spring-boot-unit-tests-with-jwt-token-security
https://hermeslog.tistory.com/459
https://stackoverflow.com/questions/48004367/bearer-token-failure-mockmvc-test-java-spring-boot
2. @WithUserDetail
혹은 authentication을 따로 설정해 주어야 하는가 하는 고민을 해 보았지만, 이에 대해선 보다 공부가 필요할 듯 하고, jwt의 경우에도 맞나 하는 생각이 들었다.
<Reference>
https://godekdls.github.io/Spring%20Security/testing/#1915-withsecuritycontext
https://tecoble.techcourse.co.kr/post/2020-09-30-spring-security-test/
<전체 Reference>
https://stackoverflow.com/questions/59873404/how-to-convert-httpstatus-code-to-int-in-java
https://sas-study.tistory.com/362
https://stackoverflow.com/questions/45241566/spring-boot-unit-tests-with-jwt-token-security
https://hermeslog.tistory.com/459
https://stackoverflow.com/questions/48004367/bearer-token-failure-mockmvc-test-java-spring-boot
https://godekdls.github.io/Spring%20Security/testing/#1915-withsecuritycontext
https://tecoble.techcourse.co.kr/post/2020-09-30-spring-security-test/
https://flyburi.com/584
'프로젝트 > 멋사 개인 프로젝트 (mutsa-SNS)' 카테고리의 다른 글
[10] mutsa-SNS 5일차 - (1) Post 조회 기능 TestCode 추가 (0) | 2022.12.26 |
---|---|
[09] mutsa-SNS 4일차 - Post 조회 기능 추가 (0) | 2022.12.24 |
[07] mutsa-SNS 3일차 - (2) 글쓰기 기능 (2) | 2022.12.22 |
[06] mutsa-SNS 3일차 - (1) User 수정 (0) | 2022.12.22 |
[05] mutsa-SNS 2일차 - (2) 로그인 기능 (1) | 2022.12.21 |