있을 유, 참 진

[JWT] 로그인 구현 2::로그인 필터 구현, Spring Security에 적용하기 본문

Spring/JWT

[JWT] 로그인 구현 2::로그인 필터 구현, Spring Security에 적용하기

U_ma 2023. 4. 16. 00:01
JWT Filter 생성
   - JwtSecurityConfig
Spring Security 공통 에러처리
   - JwtAccessDeniedHandler
   - JwtAuthenticationEntryPoint
WebSecurity에 값 설정

JWT Filter 생성

💡 JWT 토큰을 활용해 Security에서 인가를 처리하는 필터를 생성, 차후 `UsernamePasswordAuthenticationFilter` 전에 들어가 JWT의 정보를 통해 유저의 인가를 처리한다.
@Slf4j
@RequiredArgsConstructor
public class JwtFilter extends GenericFilter {
    //헤더에서 받아올 이름 지정
    public static final String AUTHORIZATION_HEADER = "Authorization";
    private final JwtTokenProvider tokenProvider;

    /**
     * JWT 필터 추가
     * @param request  The request to process
     * @param response The response associated with the request
     * @param chain    Provides access to the next filter in the chain for this
     *                 filter to pass the request and response to for further
     *                 processing
     *
     * @throws IOException
     * @throws ServletException
     */
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String jwtToken = resolveToken(httpServletRequest);
        String requestURI = httpServletRequest.getRequestURI();

        if(StringUtils.hasText(jwtToken) && tokenProvider.validateToken(jwtToken)) {
            //토큰 값에서 Authentication 값으로 가공해서 반환 후 저장
            Authentication authentication = tokenProvider.getAuthentication(jwtToken);
            SecurityContextHolder.getContext().setAuthentication(authentication);
            log.info("Security Context에 '{}' 인증 정보를 저장했습니다, uri: {}", authentication.getName(), requestURI);
        } else {
            log.info("유효한 JWT 토큰이 없습니다. requestURI : {}", requestURI);
        }

        //다음 필터로 넘기기
        chain.doFilter(request, response);
    }

    /**
     * HttpServletRequest에서 `Authorization` 헤더를 받음.
     * 헤더에서 'Bearer'로 시작하는 토큰이 있으면 'Bearer' 부분 제거하고 토큰 값 반환 아니면 널 값 반환
     * @param request
     * @return
     */
    private String resolveToken(HttpServletRequest request) {
        String bearerToken = request.getHeader(AUTHORIZATION_HEADER);
        if(StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) return bearerToken.substring(7);

        return null;
    }
}

JwtSecurityConfig

💡 앞서 만든 필터를 Security에 적용하기 위한 Config 생성, Security filter에 앞서 말한 `UsernamePasswordAuthenticationFilter` 앞에 만들어진 JWT필터를 적용해 준다.
@RequiredArgsConstructor
public class JwtSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
    private final JwtTokenProvider jwtTokenProvider;

    @Override
    public void configure(HttpSecurity httpSecurity) {
        //UsernamePasswordAuthenticationFilter 뒤에 저장 생성자 주입 받은 JwtTokenProvider를 넣어준다.
        httpSecurity.addFilterBefore(
                new JwtFilter(jwtTokenProvider),
                UsernamePasswordAuthenticationFilter.class
        );
    }
}

Spring Security 공통 에러처리

JwtAccessDeniedHandler

💡 권한이 없는 사용자(ROLE_ADMIN을 가지고 싶다던가..)가 보호된 자원에 액세스하려 할 때 처리 방법
public class JwtAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        response.sendError(HttpServletResponse.SC_FORBIDDEN);
    }
}

JwtAuthenticationEntryPoint

💡 인증되지 않은 사용자(로그인 되지 않은)가 보호된 리소스에 접근하려고 시도하면 실행되는 예외 처리
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
    }
}

WebSecurity에 값 설정

@EnableGlobalMethodSecurity(prePostEnabled = true)
@RequiredArgsConstructor
@EnableWebSecurity
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    private final JwtTokenProvider jwtTokenProvider;
    private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
    private final JwtAccessDeniedHandler jwtAccessDeniedHandler;

    // 회원가입 시 비밀번호 암호화를 위한 PasswordEncoder 빈 등록
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    public void configure(WebSecurity web) {
        web
                .ignoring()
                .antMatchers("/h2-console/**", "/favicon.ico");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                //jwt는 필요가 없으므로 csrf 비활성화
                .csrf().disable() 

                //세션 사용하지 않음
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()

                //Exception 처리를 위한 부분
                .exceptionHandling()
                .authenticationEntryPoint(jwtAuthenticationEntryPoint)
                .accessDeniedHandler(jwtAccessDeniedHandler)
                .and()

                .authorizeRequests() //ServletRequest를 사용하는 요청에 대한 접근 제한 설정
                .antMatchers(HttpMethod.POST, "/api/auth/**").permitAll() //검증 없이 이용가능(Post 요청 가능)
                .anyRequest().authenticated()
                .and()

                //JwtSecurityConfig 적용
                .apply(new JwtSecurityConfig(jwtTokenProvider));

    }
}
Comments