정수원님의 강의 스프링 시큐리티 완전 정복 [6.x 개정판] 보면서 공부한 내용입니다.
// 요청 받을 url만 명시
Origin: https:// security.io
Simple Request 조건 만족
Simple Request
💡 제약 사항
- GET, POST, HEAD 중 한 가지 METHOD를 사용해야 한다.
- 헤더는 Accept, Accept-Language, Content-Language, Content-Type, DPR, Downlink, Save-Data, Viewport-Width Width 만 가능하다
- Content-type 은 application/x-www-form-urlencoded, multipart/form-data, text/plain 만 가능하다
// 예비 요청 시 포함
Access-Control-Request-Headers: content-type
Access-Control-Request-Method: GET
💡 OPTIONS
클라이언트가 서버로부터 요청을 했을 때 해당 서버가 받은 요청에 대해서 지원하는 HTTP 메소드가 어떤 것인지 미리 탐색을 하고자 할 때 사용ex) 서버가 지원하는 HTTP 메소드가 GET 방식만 지원한다하는데 클라이언트가 POST 방식으로 보내면 실패하므로 어떤 방식을 지원하는지 미리 확인하는 것이 OPTIONS다
💡 CORS가 먼저 처리되도록 CorsFilter을 사용하여 Spring Security와 통합할 수 있다
사전 요청
본 요청
💡 쿠키는 브라우저에서 자동으로 요청하기 때문에 쿠키를 토큰으로 만드는 것은 보안에 위험하다
@Bean
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
http.csrf(Customizer.withDefaults()); // 별도 설정없이 활성화 상태로 초기화 된다
return http.build();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/csrf").permitAll()
.anyRequest().authenticated())
.formLogin(Customizer.withDefaults())
// .csrf(csrf -> csrf.disable()) // csrf 전체 비활성화
.csrf(csrf -> csrf.ignoringRequestMatchers("/csrf"))
// /csrf url만 기능 비활성화
;
return http.build();
}
@Bean
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
HttpSessionCsrfTokenRepository repository = new HttpSessionCsrfTokenRepository();
http.csrf(csrf -> csrf.csrfTokenRepository(repository));
return http.build();
}
@Bean
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
CookieCsrfTokenRepository repository = new CookieCsrfTokenRepository();
// 첫 번째 방법
http.csrf(csrf -> csrf.csrfTokenRepository(repository));
// 두 번째 방법
// http.csrf(csrf -> csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()));
return http.build();
}
@Bean
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
XorCsrfTokenRequestAttributeHandler csrfTokenHandler = new XorCsrfTokenRequestAttributeHandler();
http.csrf(csrf -> csrf.csrfTokenRequestHandler(csrfTokenHandler));
return http.build();
}
@Bean
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
XorCsrfTokenRequestAttributeHandler handler = new XorCsrfTokenRequestAttributeHandler();
handler.setCsrfRequestAttributeName(null);
// 바로 CSRF 토큰을 모든 요청마다 가져온다
http.csrf(csrf -> csrf
.csrfTokenRequestHandler(handler));
return http.build();
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// 지연된 객체 => request 에 저장됨
DeferredCsrfToken deferredCsrfToken = this.tokenRepository.loadDeferredToken(request, response);
request.setAttribute(DeferredCsrfToken.class.getName(), deferredCsrfToken);
this.requestHandler.handle(request, response, deferredCsrfToken::get);
// false 인 경우
// GET 방식인 경우 => CSRF 기능 실행 X
if (!this.requireCsrfProtectionMatcher.matches(request)) {
if (this.logger.isTraceEnabled()) {
this.logger.trace("Did not protect against CSRF since request did not match "
+ this.requireCsrfProtectionMatcher);
}
filterChain.doFilter(request, response);
return;
}
// POST, DELETE 등 방식인 경우 => CSRF 기능 실행 O
CsrfToken csrfToken = deferredCsrfToken.get();
String actualToken = this.requestHandler.resolveCsrfTokenValue(request, csrfToken);
if (!equalsConstantTime(csrfToken.getToken(), actualToken)) {
boolean missingToken = deferredCsrfToken.isGenerated();
this.logger
.debug(LogMessage.of(() -> "Invalid CSRF token found for " + UrlUtils.buildFullRequestUrl(request)));
AccessDeniedException exception = (!missingToken) ? new InvalidCsrfTokenException(csrfToken, actualToken)
: new MissingCsrfTokenException(actualToken);
this.accessDeniedHandler.handle(request, response, exception);
return;
}
filterChain.doFilter(request, response);
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// 쿠키로 저장
CookieCsrfTokenRepository csrfTokenRepository = new CookieCsrfTokenRepository();
// 기본적인 쿠키 이름
// static final String DEFAULT_CSRF_COOKIE_NAME = "XSRF-TOKEN";
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/csrf").permitAll()
.anyRequest().authenticated())
.formLogin(Customizer.withDefaults())
.csrf(csrf -> csrf.csrfTokenRepository(csrfTokenRepository))
;
return http.build();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// 쿠키로 저장
CookieCsrfTokenRepository csrfTokenRepository = new CookieCsrfTokenRepository();
// 기본적인 쿠키 이름
// static final String DEFAULT_CSRF_COOKIE_NAME = "XSRF-TOKEN";
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/csrf").permitAll()
.anyRequest().authenticated())
.formLogin(Customizer.withDefaults())
// XSRF-TOKEN은 http 통신에서만 사용할 수 있으므로 withHttpOnlyFalse를 활용하여 토큰을 스크립트에서 읽을 수 있도록 설정
.csrf(csrf -> csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()))
;
return http.build();
}
@GetMapping("/csrfToken")
public String csrfToken(HttpServletRequest request) {
// request에서 토큰의 이름(CsrfToken.class.getName())과 문자열(_csrf)로
// 지연된 객체가 저장됐으므로 참조해서 가져오기
CsrfToken csrfToken1 = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
CsrfToken csrfToken2 = (CsrfToken) request.getAttribute("_csrf");
String token = csrfToken1.getToken();
return token;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/csrf","/csrfToken").permitAll()
.anyRequest().authenticated())
.formLogin(Customizer.withDefaults())
// XSRF-TOKEN은 http 통신에서만 사용할 수 있으므로 withHttpOnlyFalse를 활용하여 토큰을 스크립트에서 읽을 수 있도록 설정
// .csrf(csrf -> csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()))
;
return http.build();
}
사용자가 A 사이트에 방문하면 해당 서비스는 사용자에게 쿠키를 발급한다. 해당 쿠키는 세션 쿠키로 브라우저에 저장된다. 그렇기에 사용자가 A 사이트에서 작업을 진행하면 세션쿠키가 자동적으로 브라우저에 전달된다. 다만, 사용자가 A 사이트에서 B 사이트로 이동한 후 어떤 작업을 진행 후 다시 A 사이트로 이동한 경우에는 세션 쿠키가 브라우저에 전송되지 않는다.
💡 Top Level Navigation이란?
사용자가 링크(<'a'>)를 클릭하거나 window.location.replace , 302 리다이렉트 등의 이동
동일 사이트에서는 쿠키를 전송하지만 사용자가 B 사이트에서 쿠키를 발급한 A 사이트로 재이동한 경우 Top Level Navigation 또는 GET 방식 인 경우에는 쿠키를 전송하지만 POST, DELETE와 같은 서버의 자원이 변하는 요청에는 쿠키를 전송하지 않는다
// 의존성 추가
implementation group: 'org.springframework.session', name: 'spring-session-core', version: '3.2.1'
@Configuration
@EnableSpringHttpSession
public class HttpSessionConfig {
// 쿠키 설정
@Bean
public CookieSerializer cookieSerializer() {
DefaultCookieSerializer serializer = new DefaultCookieSerializer(); // 객체 생성
serializer.setUseHttpOnlyCookie(true); // http 통신에서만 사용
serializer.setUseSecureCookie(true); // 보안 쿠키 사용
serializer.setSameSite("None"); // SameSite 속성 설정
return serializer;
}
// 설정한 쿠키를 저장할 수 있는 Session Repository
@Bean
public SessionRepository<MapSession> sessionRepository() {
return new MapSessionRepository(new ConcurrentHashMap<>())
}
}
SameSite("None")인 경우
SameSite("Strict")인 경우
SameSite("Lax")인 경우