Spring Boot (29) 스프링 시큐리티 실습 JwtAuthenticationFilter, SecurityConfiguration 등등

넙데데맨·2022년 11월 30일
0
post-thumbnail
post-custom-banner

지난 시간에 이은 스프링 시큐리티 실습

JwtAuthenticationFilter

JwtAuthenticationFilter는 JwtTokenProvider를 통해 HttpServletRequest 객체에서 토큰을 추출하고 토큰에 대한 유효성 검사를 실시한다.
토큰이 유효할 경우에는 Authentication 객체를 생성해서 SecurityContextHolder에 추가하는 작업을 추가로 수행한다.
JwtAuthentication

public class JwtAuthentication extends OncePerRequestFilter {
    private final Logger LOGGER = LoggerFactory.getLogger(JwtAuthentication.class);
    private final JwtTokenProvider jwtTokenProvider;

    public JwtAuthentication(JwtTokenProvider jwtTokenProvider) {
        this.jwtTokenProvider = jwtTokenProvider;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String token = jwtTokenProvider.resolveToken(request);
        LOGGER.info("[doFilterInternal] token 값 추출 완료. token : {}", token);

        LOGGER.info("[doFilterInternal] token 값 유효성 체크 시작");
        if(token != null && jwtTokenProvider.validateToken(token)){
            Authentication authentication = jwtTokenProvider.getAuthentication(token);
            SecurityContextHolder.getContext().setAuthentication(authentication);
            LOGGER.info("[doFilterInternal] token 값 유효성 체크 완료");
        }

        filterChain.doFilter(request,response);
    }
}

SecurityConfiguration

스프링 시큐리티 설정을 위한 설정파일을 만든다.
WebSecurityConfigureAdpater를 상속받는 클래스를 만든다고 되어있는데 검색해보니 이미 deprecated 되었다고 해서 다른 해결책을 찾아보기로 했다.

SecurityFilterChain, WebSecurityCustomizer

정확히는 모르겠지만 책에 있는 HttpSecurity, WebSecurity를 각각 인자로 가지는 configure 메소드들은 각각 SecurityFilterChain, WebSecurityCustomizer를 Bean으로 등록해서 해결하는 것 같다.


SecurityConfiguration

@Configuration
public class SecurityConfiguration {
    private final JwtTokenProvider jwtTokenProvider;

    @Autowired
    public SecurityConfiguration(JwtTokenProvider jwtTokenProvider){
        this.jwtTokenProvider = jwtTokenProvider;
    }
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception{
        httpSecurity
                .httpBasic().disable() // UI 사용 기본 값으로 가진 시큐리티 설정 비활성화

                .csrf().disable() // CSRF 보안 비활성화

                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        /**
         *       SessionCreationPolicy.ALWAYS      - 스프링시큐리티가 항상 세션을 생성
         *       SessionCreationPolicy.IF_REQUIRED - 스프링시큐리티가 필요시 생성(기본)
         *       SessionCreationPolicy.NEVER       - 스프링시큐리티가 생성하지않지만 기존에 존재하면 사용
         *       SessionCreationPolicy.STATELESS   - 스프링시큐리티가 생성하지도않고 기존것을 사용하지도 않음
         */
                .and()
                .authorizeRequests()
                .antMatchers("/sign-api/sign-in","/sign-api/sign-up",
                        "/sign-api/exception").permitAll()
                .antMatchers(HttpMethod.GET, "/product/**").permitAll()
                .antMatchers("**exception**").permitAll()
                /**
                 * authorizeRequests() 애플리케이션에 들어오는 요청에 대한 사용 권한 체크
                 * antMatchers() antPattern을 통해 권한을 설정하는 역할
                 */


                .anyRequest().hasRole("ADMIN")

                .and()
                .exceptionHandling().accessDeniedHandler(new CustomAccessDeniedHandler())
                // 권한 확인 중 예외 발생할 경우 예외 전달
                .and()
                .exceptionHandling().authenticationEntryPoint(new CustomAuthenticationEntryPoint())
                // 인증 과정 중 예외 발생할 경우 예외 전달
                .and()
                .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class);
                // 필터 등록을 어느 필터 앞에 추가할 것인지 설정한다.
                // JwtAuthenticationFilter를 UsernamePasswordAuthenticationFilter 앞에 추가하겠다는 의미

                return httpSecurity.build();
    }

    /**
     * WebSecurity는 스프링 시큐리티 영향권 밖에 있어 인증, 인가가 적용 전에 동작하는 설정
     * 인증, 인가가 적용되지 않는 리소스 접근에만 사용된다.
     * ignoring 메소드를 사용해 인증과 인가를 무시하는 경로를 설정
     * @return
     */
    @Bean
    public WebSecurityCustomizer webSecurityCustomizer(){
        return (web) -> web.ignoring().antMatchers(
                "/v2/api-docs", "/swagger-resources/**", 
                "/swagger-ui.html","/webjars/**", "sign-api/exception");
    }

}

AccessDeniedHandler, AuthenticationEntryPoint

시큐리티 설정파일에서 인증 인가의 예외상황에서 발생하는 예외를 전달하는 클래스를 작성한다.
각각 AccessDeniedHandler 인터페이스, AuthenticationEntryPoint 인터페이스를 상속받아서 구현한다.
CustomAccessDeniedHandler

/**
 * 액세스 권한이 없는 리소스에 접근할 경우 발생하는 예외를 처리하는 클래스
 */
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {

    private final Logger LOGGER = LoggerFactory.getLogger(CustomAccessDeniedHandler.class);

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        LOGGER.info("[handle] 접근이 막혔을 경우 리다이렉트");
        response.sendRedirect("/sign-api/exception");
        // 접근이 막혔을 경우에 해당 경로로 리다이렉트되어 정의한 예외 메소드가 호출된다.
    }
}

response에서 리디렉트되어 정의한 메소드가 호출된다.
CustomAuthenticationEntryPoint

/**
 * 인증이 실패한 상황을 처리하는 클래스
 */
@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
    private final Logger LOGGER = LoggerFactory.getLogger(CustomAuthenticationEntryPoint.class);

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        ObjectMapper objectMapper = new ObjectMapper();
        LOGGER.info("[commence] 인증 실패로 reponse.sendError 발생");

        EntryPointErrorResponse entryPointErrorResponse = new EntryPointErrorResponse();
        entryPointErrorResponse.setMsg("인증이 실패하였습니다.");

        response.setStatus(404);
        response.setContentType("application/json");
        response.setCharacterEncoding("utf-8");
        response.getWriter().write(objectMapper.writeValueAsString(entryPointErrorResponse));

    }
}

예외처리를 위해 직접 response를 생성해서 클라이언트에게 응답한다.
response에 상태코드, 콘텐트 타입 등을 설정한 후 objectMapper를 사용해서 EntryPointErrorResponse 객체를 바디 값으로 파싱한다.
굳이 메시지를 설정할 필요가 없을 경우 인증실패 코드만 전달하는 것도 가능하다.

@Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
    response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
}

EntryPointErrorResponse

@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class EntryPointErrorResponse {
    private String msg;

}
profile
차근차근
post-custom-banner

0개의 댓글