수업 내용 정리

[Udemy] Spring Security 예외 처리, CORs, CSRF

헨헨7 2024. 11. 22. 13:17

* 해당 강의에 대한 정리 글입니다.

https://www.udemy.com/course/spring-security-6-jwt-oauth2-korean/?couponCode=CPSALEBRAND24

1. SecurityConfig - HTTPS 프로토콜 허용

@Bean
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
    http.requiresChannel(rcc -> rcc.anyRequest().requiresSecure()) // HTTPS 트래픽만 허용
        // ... 이후 로직
    return http.build();
}
  • http.requiresChannel(rcc -> rcc.anyRequest().requiresSecure()): HTTPS 트래픽만 허용(InSecure(): HTTP 프로토콜만 허용), 해당 설정 없을 시 둘 다 허용

 

2. Custom Exception

ExceptionTranslationFilter

  • 수신된 예외의 유형을 확인하고 관련 구현을 호출
  • AuthenticationException -> AuthenticationEntryPoint
  • AccessDeniedException -> AccessDeniedHandler
  • doFilter()에서 예외 처리하며, 예외가 발생하지 않을 경우 필터 체인 내부의 다음 필터를 호출
    예외가 발생할 경우, handleSpringSecurityException()에서 예외 유형을 확인하고 Exception 호출
  • 각 Exception() 메서드 내부에서 로직(sendStartAuthentication)에 따라 예외 처리 수행

AuthenticationException(인증 예외)

  • 401 status 반환(인증되지 않음)
  • BadCredentialsException, UsernameNotFoundException

AccessDeniedException

  • 403 status(Forbidden)
  • 사용자나 클라이언트 애플리케이션이 올바르게 인증되었지만 보안된 API에 접근할 수 있는 권한이나 역할이 없음

CustomBasicAuthenticationEntryPoint.java

public class CustomBasicAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException)
            throws IOException, ServletException {
        response.setHeader("system-error-reason", "Authentication failed"); // 사용자 정의 로직 사용 가능
        response.sendStatus(HttpStatus.UNAUTHORIZED.value());
        response.setContentType("application/json;charset=UTF-8");
        
        String jsonResponse = 
            String.format("<jsonFormat>", <object>);
        response.getWriter().write(jsonResponse);
    }
}
  • 설정 후 SecurityConfig.java에서 http.httpBasic(hbc -> hbc.authenticationEntryPoint(new CustomBasicAuthenticationEntryPoint())); 지정하거나
  • http.exceptionHandling(ehc -> ehc.authenticationEntryPoint(new CustomBasicAuthenticationEntryPoint())); 으로 전역 설정 가능

CustomAccessDeniedHandler.java

public class CustomAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException)
            throws IOException, ServletException {
        /*
        사용자 정의 populate dynamic value 사용 가능...
        */
        response.setHeader("system-error-reason", "Authentication failed"); // 사용자 정의 로직 사용 가능
        response.sendStatus(HttpStatus.FORBIDDEN.value());
        response.setContentType("application/json;charset=UTF-8");
        
        String jsonResponse = 
            String.format("<jsonFormat>", <object>); // 여기서 value construct
        response.getWriter().write(jsonResponse);
    }
}

 

SecurityConfig.java

@Bean
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
    http.requiresChannel(rcc -> rcc.anyRequest().requiresSecure()) // HTTPS 트래픽만 허용
        .csrf(csrfConfig -> csrfConfig.disable())
        .authorizeHttpRequiest((requests) -> requests
        .requestMatchers("/myAccount", "/myBalance", "/myLoans", "/myCards").authenticated()
        .requestMatchers("/notices", "/contact", "/error", "/register").permitAll());

    http.formLogin(withDefaults());
    http.http.httpBasic(hbc -> hbc.authenticationEntryPoint(new CustomBasicAuthenticationEntryPoint()));
    http.exceptionHandling(ehc -> ehc.accessDeniedHandler(new CustomAccessDeniedHandler())
        //.accessDeniedPage("/denied")); // 최종 경로 리다이렉션 하는 경우

    return http.build();
}
  • exceptionHandling에 의해 리다이렉션되는 경우, 해당 경로에 해당하는 MVC 경로가 구성되어 있어야 한다.
    accessDeniedHandler와 accessDeniedPage를 한번에 정의하는 것 비추 -> REST API를 지원하는 애플리케이션에서만 엄격하게 적용되므로 별도의 ui 애플리케이션이 백엔드 로직을 사용하려 한다면 accessDeniedHandler만 정의하는 것이 합리적
  • ui와 백엔드 로직이 멀티 애플리케이션(Spring MVC ...)처럼 구축된 경우 둘 다 사용하여 리다이렉션한다.

타임아웃 설정

  • application.properties 설정

 

3. 동시 세션 관리

SecurityConfig.java

@Bean
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
    http.sessionManageMent(smc -> smc.invalidSessionUrl("/invalidSession")
            .maximumSessions(1).maxSessionsPreventsLogin(true)) // 최대 세션 수 지정
        .requiresChannel(rcc -> rcc.anyRequest().requiresSecure()) // HTTPS 트래픽만 허용
        
        // ... 이후 로직

    return http.build();
}
  • maximunSessions() 메서드: 클라이언트 애플리케이션의 최대 세션 수 지정 -> 세션 수를 초과하는 경우 이전에 유지하던 세션이 동시 로그인으로 인해 만료됨
  • maxSessionsPreventsLogin(): Dafault(false)인 경우 이전에 유지하던 세션이 만료되고 새로 로그인한 세션이 사용자 인증 허가
    true인 경우 이전 세션이 유지되고 새로 로그인하는 경우 유효성 검사를 통해 사용자 인증 절차를 수행하지 않음
    * maxSessionsPreventsLogin(true).expiredUrl()을 통해 세션이 만료된 경우 최종 사용자를 리다이렉션 할 수 있음
  • 세션 하이재킹: URL 내부에 세션 ID가 존재하는 경우, 쿠키에서 세션 ID를 저장하는 경우
    -> HTTPS 프로토콜 사용
    -> 세션 ID 타임아웃을 짧게 유지

4. CORs

  • origin: scheme, domain, port
  • CORs 에러: 두 개의 서로 다른 오리진에 배포된 두 개의 다른 애플리케이션이 통신을 통해 리소스를 공유하려고 할 때 CORs 정책에 따라 다른 오리진을 가진 애플리케이션 간의 통신이 차단되면서 발생하는 문제
  • @CrossOrigin(origins = "http://localhost:4200") 등 어노테이션을 통해 해결

SecurityConfig.java

public class ProjectSecurityConfig {

    @Bean
    SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
        CsrfTokenRequestAttributeHandler csrfTokenRequestAttributeHandler = new CsrfTokenRequestAttributeHandler();
        http.cors(corsConfig -> corsConfig.configurationSource(new CorsConfigurationSource() {
            @Override
            public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
                CorsConfiguration config = new CorsConfiguration();
                config.setAllowedOrigins(Collections.singletonList("http://localhost:4200"));
                config.setAllowedMethods(Collections.singletonList("*"));
                config.setAllowCredentials(true);
                config.setAllowedHeaders(Collections.singletonList("*"));
                config.setMaxAge(3600L);
                return config;
            }
        }))
  • setAllowOrigins(): 트래픽을 수락하고자 하는 출처 목록 제공
  • setAllowMethod(): 특정 HTTP 메소드에 대해서만 트래픽 허용 가능
  • setAllowCredentials(): 브라우저가 백엔드 api에 요청을 보낼 때 자격 증명이나 적용 가능한 쿠키를 전송할 수 있도록 설정
  • setAllowedHeaders(): ui 애플리케이션이나 다른 출처에서 백엔드가 수락할 수 있는 헤더 목록 정의
  • setMaxAge(): 설정 유지 시간(캐시) 지정
  • preflight 요청: 실제 api 요청 전에 브라우저가 백엔드 서버로 보낼 요청(CORs 관련 설정 탐색)

 

5. CSRF attack

  • CSRF 공격: 해커가 사용자의 명시적인 동의 없이 웹 애플리케이션에서 사용자를 대신하여 작업을 수행하려고 시도
  • 백엔드 서버가 요청이 원래 웹사이트에서 온 것인지 타 웹사이트에서 온 것인지 인지할 수 있어야 함
  • CSRF 토큰 솔루션: 사용자 세션마다 고유한 토큰 값을 쿠키로 ui 애플리케이션에 전달, RequestHeader 혹은 RequestBody에서 토큰을 받았는 지 식별

SecurityConfig.java

@Configuration
@Profile("!prod")
public class ProjectSecurityConfig {

    @Bean
    SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
        CsrfTokenRequestAttributeHandler csrfTokenRequestAttributeHandler = new CsrfTokenRequestAttributeHandler();
        http.securityContext(contextConfig -> contextConfig.requireExplicitSave(false))
                .sessionManagement(sessionConfig -> sessionConfig.sessionCreationPolicy(SessionCreationPolicy.ALWAYS))
                // .cors 설정...
                .csrf(csrfConfig -> csrfConfig.csrfTokenRequestHandler(csrfTokenRequestAttributeHandler)
                        .ignoringRequestMatchers( "/contact","/register")
                        .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()))
                .addFilterAfter(new CsrfCookieFilter(), BasicAuthenticationFilter.class)
        // 이후 설정...
        return http.build();
    }
  • csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()): CSRF 토큰을 쿠키로 저장, 내부적으로 해당 객체를 설정하고 cookieHttpOnly를 false로 지정 -> ui 프레임워크나 자바스크립트 코드가 쿠키 값을 읽을 수 있도록 하기 위함(true인 경우 쿠키는 브라우저에서만 읽을 수 있어 해당 값을 수동으로 읽을 수 없음)
  • addFilterAfter(): 기본 인증 필터 실행 후 CsrfCookieFilter()가 실행되도록 함
  • CsrfTokenRequestAttributeHandler: 로그인 작업 후 요청 내부와 쿠키로 토큰 값을 전송할 때 AttributeHandler가 토큰 값을 읽어서 _csrf 속성으로 요청 내부에 추가
  • SessionCreationPolicy.ALWAYS: 항상 세션 생성 요청

CsrfCookieFileter.java

public class CsrfCookieFilter extends OncePerRequestFilter {


    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        CsrfToken csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
        // Render the token value to a cookie by causing the deferred token to be loaded
        csrfToken.getToken();
        filterChain.doFilter(request, response);
    }
}
  • OncePerRequestFilter: 확장하는 필터가 있을 때마다 라이브러리가 요청의 일부로 필터를 한 번만 실행하도록 관리하며 불필요하게 실행하지 않음
  • getToken(): 실제 토큰 생성 및 쿠키로 전송 처리
  • filterChain.doFilter(): 해당 필터에서 필터 체인의 다음 필터로 이동