정수원님의 강의 스프링 시큐리티 완전 정복 [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")인 경우
