본문 바로가기
Back-End/Security

Spring Security + JWT (2)

by 어렵다어려웡 2022. 4. 18.

자세한 예외 처리 로직이나 DTO는 생략했습니다.

 

Spring Security 기반으로 JWT토큰을 구현할 경우

기존의 Security의 세션&쿠키 방식의 통신에서 설정을 변경해줘야 한다.

  1. Config 설정
@Override
    protected void configure(HttpSecurity http) throws Exception {

        http.authorizeRequests()
                    .antMatchers("/h2-console/**")
                        .permitAll()
                    .antMatchers("/v1/**")
                        .permitAll()
                .and()
                    .headers()
                        .frameOptions().sameOrigin() // H2 Page XFrame Error
                .and()
                    // form  로그인 비활성화
                    .formLogin().permitAll()
                    // Session 기반 미사용
                .and()
                    .sessionManagement()
                        .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                    .logout()
                .and()
                    .csrf().disable();
}

우선적으로 FormLogin을 비활성화를 설정하며, sessionManagement 설정을

변경해서 Stateless로 설정해줘야 한다.

 

2. JwtAuthenticationFilter

@Log4j2
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
		// 생략
    /*
        로그인시 가장 먼저 호출된다.
     */
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request,
                                                HttpServletResponse response) throws AuthenticationException {
        try {
            AttemptLoginDto attemptLoginDto = new ObjectMapper().
                    readValue(request.getInputStream(), AttemptLoginDto.class);

            return getAuthenticationManager().authenticate(attemptLoginDto.bindToAuthenticationToken());
        } catch (IOException e) {
            // TODO : 예외 추가
            throw new RuntimeException(e);
        }
    }

    // 인증이 성공(UserDetailsService)되면 해당 메서드 호출
    @Override
    protected void successfulAuthentication(HttpServletRequest request,
                                            HttpServletResponse response,
                                            FilterChain chain,
                                            Authentication authResult) throws IOException, ServletException {
        log.info("success Login...");
        ResponseLoginDto authenticationUser = (ResponseLoginDto) authResult.getPrincipal();
        String jwtToken = JwtProvider.createToken(authenticationUser.getEmail());
        log.info("jwtToken : {}", jwtToken);

        response.setHeader("token", jwtToken);
    }
}

UsernamePasswordAuthenticationFilter 를 상속받아 구현했다.

Security의 여러개의 필터중 하나로 다음과 같은 역할을 한다.

  1. attemptAuthentication : 최초 로그인 요청시 request 로 전달된 데이터를 사용해서 AuthenticationManager를 통해 인증을 요청합니다.
  2. successfulAuthentication : 내부동작 이후 인증을 마친 뒤 AuthenticationManager를 통해 인증이 완료된 토큰을 받은 뒤에 진행되는 로직을 작성합니다.

기본적으로 로그인은 웹 페이지라고 가정했을 때, <form> 태그를 이용해서 사용합니다.

그리고 이 방식은 기존에 Security가 사용하는 기본 동작 방식입니다.

 

API 개발을 위해 사용할 경우 form 태그를 통한 요청이 아니라 대부분 JSON 형태의 데이터를 전달하기 때문에, UsernamePasswordAuthenticationFilter 를 구현해서 사용합니다.

그리고, 인증 이후에 JWT 토큰을 발급해줘야 하기 때문에 successfulAuthentication 의 로직에서 JWT 토큰을 생성하는 메서드를 호출합니다.

 

 

3. JwtAuthenticationFilter 등록

http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);

직접 구현한 JwtAuthenticationFilter를 등록합니다.

 

  4. Jwt 유효성 검사.

유효성을 검사하는 로직을 구현하는 부분은 여러가지가 있겠지만,

인터셉터를 사용해서 특정한 URL에서 동작하도록 설정합니다.

public class JwtTokenCheckInterceptor implements HandlerInterceptor {

    private static final String AUTHENTICATION_HEADER_PREFIX = "Bearer ";
 
    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response,
                             Object handler) throws Exception {
        log.info("preHandle....");

        String header = request.getHeader(HttpHeaders.AUTHORIZATION);
        log.info("header : {}", header);
        if(Objects.isNull(header) || !header.contains(AUTHENTICATION_HEADER_PREFIX) || header.isEmpty()){
            // TODO : 예외발생
            log.info("Objects.isNull(header) || !header.contains(AUTHENTICATION_HEADER_PERFIX) || header.isEmpty() ..");
            response.sendError(HttpServletResponse.SC_FORBIDDEN);
            return false;
        }

        String token = header.replace("Bearer ", "");
        log.info("token ... : {}", token);
        boolean jwtValid = JwtProvider.isJwtValid(token);
        log.info("jwtValid : {}", jwtValid);

        if(!jwtValid) {
            // TODO : 예외발생
            log.info("Not Valid JWT Token");
            response.sendError(HttpServletResponse.SC_NOT_ACCEPTABLE);
            return false;
        }

        return true;
    }
}
@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(jwtTokenCheckInterceptor())
                .addPathPatterns("/v1/todo/**");
    }

    @Bean
    public JwtTokenCheckInterceptor jwtTokenCheckInterceptor() {
        return new JwtTokenCheckInterceptor();
    }
}

5. CORS 설정

서로 다른 위치의 서비스 끼리 호출할 때, 주로 발생하는 문제라고 합니다.

해당 설정은 서버 뿐만 아니라 클라이언트에서도 추가적인 Header를 작성해서 요청을 해야 합니다.

 

@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class CORSFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {

        response.setHeader("Access-Control-Allow-Origin", "*");
        response.setHeader("Access-Control-Allow-Credentials", "true");
        response.setHeader("Access-Control-Allow-Methods", "*");
        response.setHeader("Access-Control-Max-Age", "3600");
        response.setHeader("Access-Control-Allow-Headers",
                "Origin, X-Requested-With, Content-Type, Accept, Key, Authorization");

        if("OPTIONS".equalsIgnoreCase(request.getMethod())) {
            response.setStatus(HttpServletResponse.SC_OK);
        } else {
            filterChain.doFilter(request, response);
        }
    }
}