공부기록/Spring Security

[실습] 스프링 시큐리티 JWT 처리

jhs0129 2023. 2. 20. 15:00
320x100
반응형

목차

이제 그럼 로그인 후 JWT 생성 처리와 Token을 가지고 접근 시 처리를 알아보자

Config

SecurityConfig

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
            .csrf().disable()
            .cors();
    http
            .sessionManagement()
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        .and()
            .authorizeHttpRequests()
            .mvcMatchers("/loginhome").authenticated()
            .anyRequest().permitAll();
    http
            .addFilterAfter(jsonLoginProcessFilter, LogoutFilter.class)
            //jwt 관련 필터 추가
            .addFilterAfter(jwtAuthorizationFilter, JwtAuthenticationFilter.class);
    return http.build();
}

JWTFilterConfig

@Configuration
@RequiredArgsConstructor
public class SecurityFilterBeanConfig {

    private final ObjectMapper objectMapper;
    private final AuthenticationManager authenticationManager;
    private final JwtService jwtService;
    private final UserRepository userRepository;

    @Bean
    public JsonLoginProcessFilter jsonLoginProcessFilter(JwtProviderHandler jwtProviderHandler) {
        JsonLoginProcessFilter jsonLoginProcessFilter = new JsonLoginProcessFilter(objectMapper, authenticationManager);
        //성공시 JWT 발행
        jsonLoginProcessFilter.setAuthenticationSuccessHandler(jwtProviderHandler);
        return jsonLoginProcessFilter;
    }

    //client로 부터 받은 jwt 유효성 검사 필터
    @Bean
    public JwtAuthorizationFilter jwtAuthorizationFilter() {
        return new JwtAuthorizationFilter(jwtService, userRepository);
    }

    // 로그인 성공 시 JWT 발행
    @Bean
    public JwtProviderHandler jwtProviderHandler() {
        return new JwtProviderHandler(jwtService, userRepository);
    }
}

JsonLoginProcessFilter에 대해서는 [실습] 스프링시큐리티 Json data Login 처리에서 확인해보자

이제부터 알아볼 것은 JwtAuthorizationFilter와 JwtProviderHandler이다 이름을 보다시피 Jwt를 사용해서 권한을 부여하는 것과 Jwt를 발행하는 두가지 역할을 한다

JwtProviderHandler

위 설정에서 authenticationSuccesHandler를 설정해준 것이 AbstractAuthenticationProcessingFilter.doFilter에서 마지막 successfulAuthentication()부분에서 실행이 된다

@RequiredArgsConstructor
public class JwtProviderHandler extends SimpleUrlAuthenticationSuccessHandler {

    private final JwtService jwtService;
    private final UserRepository userRepository;

    @Override
    @Transactional
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        CustomUser customUser = (CustomUser) authentication.getPrincipal();
        String email = customUser.getEmail();

        //토큰 생성
        String accessToken = jwtService.createAccessToken(email);
        String refreshToken = jwtService.createRefreshToken();

        //client로 보내기
        jwtService.sendBothToken(response, accessToken, refreshToken);

        System.out.println("accessToken = " + accessToken);
        System.out.println("refreshToken = " + refreshToken);

        //refresh토큰 저장
        userRepository.findUserByEmail(email).get()
                .updateRefreshToken(refreshToken);
    }
}

JsonLoginProcessFilter가 상속 한 AbstractAuthenticationProcessingFilter.doFilter

private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
    //생략
    try {
        //생략
        successfulAuthentication(request, response, chain, authenticationResult);
    }
    catch (InternalAuthenticationServiceException failed) {
        //생략
    }
}

JwtAuthorizationFilter

JwtAuthorizationFilter를 구현을 할때는 accessToken과 refreshToken의 재발행에 있어서 아래와 같이 결정을 했다

  • accessToken 유효 -> authentication 저장
  • accessToken 만료
    • refreshToken 유효 -> authentication 저장, accessToken 갱신
    • refreshToken 만료 -> throw Exception

accessToken이 유효하다면 어떠한 토큰도 재발행을 할 필요없이 SecurityContextHolder에 저장을 하면된다

만일 만료가되었더라도 refreshToken이 유효하다면 accessToken을 갱신후 저장을 하도록 했다

하지만 refreshToken이 만료가 되었다면 다시 로그인을 시도하도록 예외를 던지도록 결정을 했다

위 결정은 각자의 상황에 맞게 변경하여 사용하면 될 듯 하다

@RequiredArgsConstructor
public class JwtAuthorizationFilter extends OncePerRequestFilter {

    private final JwtService jwtService;
    private final UserRepository userRepository;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        jwtService.extractAccessToken(request)
                .filter(jwtService::isTokenValid)
                .ifPresentOrElse(
                        this::saveAuthentication,
                        () -> checkRefreshToken(request, response)
                );
        filterChain.doFilter(request, response);
    }

    private void saveAuthentication(String accessToken) {
        String email = jwtService.extractUserEmail(accessToken);
        CustomUser customUser = new CustomUser(userRepository.findUserByEmail(email).get());
        UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(customUser, null, customUser.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(authentication);
    }

    private void checkRefreshToken(HttpServletRequest request, HttpServletResponse response) {
        Optional<String> refreshToken = jwtService.extractRefreshToken(request)
                .filter(jwtService::isTokenValid);

        if (refreshToken.isPresent()) {
            User user = userRepository.findUserByRefreshToken(refreshToken.get())
                    .orElseThrow(IllegalArgumentException::new);
            String accessToken = jwtService.createAccessToken(user.getEmail());
            jwtService.setAccessTokenInHeader(response, accessToken);
            saveAuthentication(accessToken);
            return;
        }
        throw new IllegalArgumentException("Both tokens are not valid please Login again");
    }
}

이로써 formLogin, json형식의 Login, Login 후 JWT 발행, JWT를 통한 권한 부여까지 알아보았다

이제 마지막으로 OAuth로그인에 대해서 추후 알아보도록 하자

320x100
반응형