ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Udemy] Spring Security 예외 처리, CORs, CSRF
    수업 내용 정리 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(): 해당 필터에서 필터 체인의 다음 필터로 이동

    '수업 내용 정리' 카테고리의 다른 글

    [Udemy] Spring Security 사용자 정의, JWT  (0) 2024.11.23
    [Udemy] Spring Security Basic  (1) 2024.11.21
    Argo cd  (3) 2024.10.17
    [Udemy] Twitter4j & Kafka  (0) 2024.09.23
    docker 설정  (0) 2024.08.26