수업 내용 정리
[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(): 해당 필터에서 필터 체인의 다음 필터로 이동