-
[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