본문 바로가기

프로젝트/멋사 개인 프로젝트 (mutsa-SNS)

[08] mutsa-SNS 3일차 - (3) JwtTokenFilter Exception 추가

낮에 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

 

How to convert HttpStatus code to int in Java?

I am using RestTemplate postForEntity method to POST to an endpoint. If POST is success, the statusCode variable should change its value to status code of 201, but I am having difficulty converting

stackoverflow.com

http://daplus.net/java-java-%EC%84%9C%EB%B8%94%EB%A6%BF%EC%97%90%EC%84%9C-json-%EA%B0%9D%EC%B2%B4%EB%A5%BC-%EB%B0%98%ED%99%98%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95/

 

[java] Java 서블릿에서 JSON 객체를 반환하는 방법 - 리뷰나라

Java 서블릿에서 JSON 객체를 어떻게 반환합니까? 이전에는 서블릿으로 AJAX를 수행 할 때 문자열을 반환했습니다. 사용해야하는 JSON 객체 유형이 있습니까, 아니면 JSON 객체처럼 보이는 문자열을

daplus.net

 

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

 

Restful API 구현을 위한 Spring Security 설정해보기 #4 (AuthenticationEntryPoint, AccessDeniedHandler 구현)

 

sas-study.tistory.com

https://velog.io/@dltkdgns3435/%EC%8A%A4%ED%94%84%EB%A7%81%EC%8B%9C%ED%81%90%EB%A6%AC%ED%8B%B0-JWT-%EC%98%88%EC%99%B8%EC%B2%98%EB%A6%AC

 

스프링시큐리티 JWT 예외처리

🎈시작하며 > 이 포스트는 정답이 아니며, 주관적인 생각에 의해 작성한 글입니다. 해당 포스트를 보고 제가 잘못 알고 있는 점이나, 지적사항이 있으시면 댓글로 남겨주시면 정말 감사하겠습

velog.io

https://sol-devlog.tistory.com/20

 

발급받은 JWT로 요청하기

깃허브 저장소 GitHub - solchan98/Playground: 🛝 개발 공부 놀이터 🛝 🛝 개발 공부 놀이터 🛝. Contribute to solchan98/Playground development by creating an account on GitHub. github.com +2022-09-28 새로 작성되었습니다.

sol-devlog.tistory.com

 

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

 

Spring Boot Unit Tests with JWT Token Security

I am creating a backend using Spring Boot and I have just added JWT security to it. I have done some tests using a REST Client and the JWT security is working fine, however all of my unit tests ar...

stackoverflow.com

https://hermeslog.tistory.com/459

 

[Spring Boot] JUnit Test

개발환경 Spring Boot 2.2.4 JDK 1.8.0_202 REST Postman : REST Client 목표 1. Spring Boot REST 환경 2. Log4j2 추가 3. JWT + Spring Security 를 통한 인증 4. DB 연결 ( Hibernate 제거 )을 통한 사용자 인증 5. TDD 인증 테스트 impor

hermeslog.tistory.com

https://stackoverflow.com/questions/61500578/how-to-mock-jwt-authentication-in-a-spring-boot-unit-test

 

How to mock JWT authentication in a Spring Boot Unit Test?

I have added JWT Authentication using Auth0 to my Spring Boot REST API following this example. Now, as expected, my previously working Controller unit tests give a response code of401 Unauthorized ...

stackoverflow.com

https://stackoverflow.com/questions/48004367/bearer-token-failure-mockmvc-test-java-spring-boot

 

Bearer token failure MockMvc test Java Spring Boot

I'm failing to understand why this does not work. I'm assuming it's something simple that I'm overlooking. All other other test methods not utilizing a token work fine. There is no expiration curre...

stackoverflow.com

 

2. @WithUserDetail

혹은 authentication을 따로 설정해 주어야 하는가 하는 고민을 해 보았지만, 이에 대해선 보다 공부가 필요할 듯 하고, jwt의 경우에도 맞나 하는 생각이 들었다.

 

<Reference>

https://godekdls.github.io/Spring%20Security/testing/#1915-withsecuritycontext

 

Testing

스프링 시큐리티를 테스트하는 방법을 설명합니다. 공식 문서에 있는 “Testing” 챕터를 한글로 번역한 문서입니다.

godekdls.github.io

https://tecoble.techcourse.co.kr/post/2020-09-30-spring-security-test/

 

Spring Security가 적용된 곳을 효율적으로 테스트하자.

Spring Security와 관련된 기능을 테스트하다보면 인증 정보를 미리 주입해야 하는 경우가 종종 발생한다. 기본적으로 생각할 수 있는 가장 간단한 방법은 테스트 전에 SecurityContext에 직접 Authenticatio

tecoble.techcourse.co.kr

https://flyburi.com/584

 

[SpringSecurity] Authentication(인증) 관련 클래스와 처리

Spring Security에 대해 큰 흐름은 알지만, 처음부터 적용하는게 아니면 어떤 권한을 주고 권한 체크하는 로직만 추가하거나 수정하며 생각없이 쓰게 되는데,어떤 흐름으로 되는지 전보다 좀 더 살

flyburi.com

 

<전체 Reference>

https://stackoverflow.com/questions/59873404/how-to-convert-httpstatus-code-to-int-in-java

http://daplus.net/java-java-%EC%84%9C%EB%B8%94%EB%A6%BF%EC%97%90%EC%84%9C-json-%EA%B0%9D%EC%B2%B4%EB%A5%BC-%EB%B0%98%ED%99%98%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95/

https://sas-study.tistory.com/362

https://velog.io/@dltkdgns3435/%EC%8A%A4%ED%94%84%EB%A7%81%EC%8B%9C%ED%81%90%EB%A6%AC%ED%8B%B0-JWT-%EC%98%88%EC%99%B8%EC%B2%98%EB%A6%AC

https://stackoverflow.com/questions/45241566/spring-boot-unit-tests-with-jwt-token-security

https://hermeslog.tistory.com/459

https://stackoverflow.com/questions/61500578/how-to-mock-jwt-authentication-in-a-spring-boot-unit-test

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