authenticate() 메서드는 인증 개체를 수신하고 반환합니다. authenticate() 메서드 내부에 사용자 지정 인증 로직을 모두 구현할 수 있습니다.
AuthenticationProvider 인터페이스의 두 번째 방법은 지원(Class<?> Authentication)입니다. 현재 AuthenticationProvider가 제공된 Authentication 개체의 유형을 지원하는 경우 true를 반환하도록 이 방법을 구현합니다.
CORS (Cross-Origin Resource Sharing, 교차 출처 리소스 공유)
CORS는 웹 리소스가 다른 도메인의 리소스에 접근할 수 있도록 허용하는 메커니즘이다. 예를 들어, 도메인 A에서 도메인 B의 API를 호출하려면 CORS 정책을 설정해야 할 수 있다.
CSRF (Cross-Site Request Forgery, 사이트 간 요청 위조)
CSRF는 공격자가 사용자의 세션을 이용해 악의적인 작업을 수행하는 공격 방법입니다. 사용자가 로그인한 상태에서 공격자가 준비한 페이지를 열면, 그 페이지에서는 사용자의 권한으로 서버에 요청을 보낼 수 있습니다.
https://github.com/eazybytes/springsecurity6/tree/3.1.2/section6/bank-app-ui
CORS는 브라우저 클라이언트에서 실행되는 스크립트가 다른 원본의 리소스와 상호 작용할 수 있도록 하는 프로토콜입니다.
예를 들어 UI 앱이 다른 도메인에서 실행 중인 API 호출을 원할 경우 CORS로 인해 기본적으로 차단됩니다. 대부분의 브라우저에서 도입한 WsC의 사양입니다.
따라서 CORS는 보안 문제/공격이 아니라 서로 다른 원본 간의 데이터/통신 공유를 중단하기 위해 브라우저가 제공하는 기본 보호 기능입니다.
서버에 배치된 웹 APP UI가 다른 서버에 배치된 REST 서비스와 통신을 시도하는 유효한 시나리오가 있다면 @CrossOrigin 주석을 사용하여 이러한 종류의 통신을 허용할 수 있습니다. @CrossOrigin을 사용하면 모든 도메인의 클라이언트가 API를 사용할 수 있습니다.
웹 앱 내의 모든 컨트롤러에 @CrossOrigin 주석을 언급하는 대신 아래와 같은 Spring Security를 사용하여 CORS 관련 구성을 전역적으로 정의할 수 있습니다.
import java.util.Collections;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import jakarta.servlet.http.HttpServletRequest;
@Configuration
public class ProjectSecurityConfig {
@Bean
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
// http://localhost:4200 에 대한 CORS 허용
http.cors(corsCustomizer -> corsCustomizer.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;
}
}))
// csrf 비활성화 (사이트 요청 위조) ---> csrf 토큰이 없어도 서버는 응답
// 스프링에서는 csrf 기본은 활성화 (보안 목적) ---> csrf 토큰을 url에 포함해야 서버는 응답
.csrf((csrf) -> csrf.disable())
.authorizeHttpRequests((requests)->requests
.requestMatchers("/myAccount","/myBalance","/myLoans","/myCards", "/user").authenticated()
.requestMatchers("/notices","/contact","/register").permitAll())
.formLogin(Customizer.withDefaults())
.httpBasic(Customizer.withDefaults());
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
status: 401 Unauthorized 확인
일반적으로 CSRF 또는 XSRF(Cross-Site Request Fuggement) 공격은 사용자의 명시적인 동의 없이 웹 어플리케이션에서 동작을 수행하는 것을 목적으로 하며, 일반적으로 사용자의 압흔을 직접적으로 훔치는 것이 아니라 사용자를 이용하여 자신의 의지 없이 동작을 수행하는 것을 목적으로 합니다.
netflix.com 웹사이트와 공격자의 웹사이트 evil.com 를 사용하고 있다고 생각합니다
1단계 : Netfilx.com 에 대한 넷플릭스 사용자 로그인과 넷플릭스의 백엔드 서버는 Netflix.com 도메인 이름에 대해 브라우저에 저장할 쿠키를 제공합니다
--->사용자가 자격 증명을 제출하고 Netflix.com 에 로그인합니다
<---넷플릭스 서버는 Netflix.com 도메인 이름을 기반으로 쿠키를 만들고 사용자 브라우저에 저장합니다.
2단계 : 같은 넷플릭스 사용자가 브라우저의 다른 탭에서 evil.com 웹사이트를 엽니다.
--->사용자가 evil.com 에 호스팅된 악의적인 블로그/사이트에 액세스했습니다
<---evil.com 은 넷플릭스 계정의 이메일을 변경하기 위한 악성 링크가 내장된 웹 페이지를 반환합니다. 그러나 링크는 아이폰에서 "90%" OFF와 같은 텍스트로 나타납니다.
3단계 : 사용자가 Netflix.com 에 요청하는 악성 링크를 유혹하여 클릭하였고, 동일한 브라우저에 이미 존재하는 로그인 쿠키와 동일한 도메인 Netflix.com 에 이메일 변경 요청이 이루어지므로 Netflix.com 의 백엔드 서버는 요청이 온 곳을 구별할 수 없습니다. 따라서 여기서 evil.com 은 Netflix.com UI 페이지에서 오는 것처럼 요청을 위조하였습니다.
--->사용자는 아래와 같은 내용이 있는 evil.com 의 링크를 클릭합니다
<---쾅!! 넷플릭스 계정 이메일이 바뀌었네요.
CSRF 공격을 물리치기 위해서는 응용 프로그램의 사용자 인터페이스를 통해 HTTP 요청이 합법적으로 생성되었는지 여부를 판단할 수 있는 방법이 필요합니다. 이를 달성하기 위한 가장 좋은 방법은 CSRF 토큰을 통해서입니다.
CSRF 토큰은 CSRF 공격을 방지하기 위해 사용되는 안전한 랜덤 토큰입니다. 토큰은 사용자 세션마다 고유해야 하며 추측하기 어렵도록 큰 랜덤 값이어야 합니다.
이것이 어떻게 CSRF 공격을 해결하는지 이전 넷플릭스의 예를 다시 들어보겠습니다.
1단계 : Netflix.com 에 대한 넷플릭스 사용자 로그인과 넷플릭스의 백엔드 서버는 이 특정 사용자 세션에 대해 무작위로 생성된 고유 CSRF 토큰과 함께 도메인 이름 Netflix.com 에 대해 브라우저에 저장할 쿠키를 제공합니다.
CSRF 토큰은 세션 쿠키에 노출되지 않도록 HTML 양식의 숨겨진 매개변수 내에 삽입됩니다.
2단계 : 같은 넷플릭스 사용자가 브라우저의 다른 탭에서 evil.com 웹사이트를 엽니다.
3단계 : 사용자가 Netflix.com 에 요청하는 악성 링크를 유혹하여 클릭하였습니다. 그리고 동일한 브라우저에 이미 존재하는 로그인 쿠키와 이메일 변경 요청이 Netflix.com 에 이루어졌기 때문에 이번에는 Netflix.com 백엔드 서버가 쿠키와 함께 CSRF 토큰을 기대합니다. CSRF 토큰은 로그인 작업 중에 생성된 초기 값과 동일해야 합니다.
CSRF 토큰은 애플리케이션 서버가 최종 사용 요청이 동일한 App UI에서 오는지 여부를 확인하는 데 사용됩니다. 애플리케이션 서버는 CSRF 토큰이 테스트와 일치하지 않으면 요청을 거부합니다.
기본적으로 Spring Security는 웹 애플리케이션에 구현된 CSRF 솔루션이 없는 경우 오류 403으로 모든 HTTP POST, PUT, DELETE, PATCH 작업을 차단합니다. Spring Security에서 제공하는 CSRF 보호를 비활성화하여 이 기본 동작을 변경할 수 있습니다.
✅ 추가 해야 하는 코드!!!
// 핸들러는 CSRF 토큰을 요청 속성(attribute)으로 설정하는 역할
CsrfTokenRequestAttributeHandler requestHandler = new CsrfTokenRequestAttributeHandler();
// CSRF 토큰의 이름을 "_csrf"로 설정. 즉, 요청이 들어올 때 "_csrf"라는 이름의 요청 속성(attribute)에 CSRF 토큰 값이 저장
requestHandler.setCsrfRequestAttributeName("_csrf");
// CSRF 활성화 (조건)
.csrf((csrf) -> csrf.csrfTokenRequestHandler(requestHandler) // CSRF 토큰을 요청 핸들러에 설정
.ignoringRequestMatchers("/contact", "/register") // 특정 URL 경로(/contact, /register)에 대해서는 CSRF 검증을 무시하도록 설정
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()))// CSRF 토큰을 어떻게 저장할지 설정하는 부분 (쿠키를 사용하며, HttpOnly 속성을 false로 설정 -> JavaScript에서 쿠키에 접근)
전체 코드
package com.example.springsecurity65.config;
import java.util.Collections;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import jakarta.servlet.http.HttpServletRequest;
@Configuration
public class ProjectSecurityConfig {
@Bean
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
// 핸들러는 CSRF 토큰을 요청 속성(attribute)으로 설정하는 역할
CsrfTokenRequestAttributeHandler requestHandler = new CsrfTokenRequestAttributeHandler();
// CSRF 토큰의 이름을 "_csrf"로 설정. 즉, 요청이 들어올 때 "_csrf"라는 이름의 요청 속성(attribute)에 CSRF 토큰 값이 저장
requestHandler.setCsrfRequestAttributeName("_csrf");
// http://localhost:4200 에 대한 CORS 허용
http.cors(corsCustomizer -> corsCustomizer.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;
}
}))
// csrf 비활성화 (사이트 요청 위조) ---> csrf 토큰이 없어도 서버는 응답
// 스프링에서는 csrf 기본은 활성화 (보안 목적) ---> csrf 토큰을 url에 포함해야 서버는 응답
//.csrf((csrf) -> csrf.disable()) ---> CSRF 비활성화
// CSRF 활성화 (조건)
.csrf((csrf) -> csrf.csrfTokenRequestHandler(requestHandler) // CSRF 토큰을 요청 핸들러에 설정
.ignoringRequestMatchers("/contact", "/register") // 특정 URL 경로(/contact, /register)에 대해서는 CSRF 검증을 무시하도록 설정
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()))// CSRF 토큰을 어떻게 저장할지 설정하는 부분 (쿠키를 사용하며, HttpOnly 속성을 false로 설정 -> JavaScript에서 쿠키에 접근)
.authorizeHttpRequests((requests)->requests
.requestMatchers("/myAccount","/myBalance","/myLoans","/myCards", "/user").authenticated()
.requestMatchers("/notices","/contact","/register").permitAll())
.formLogin(Customizer.withDefaults())
.httpBasic(Customizer.withDefaults());
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
"/regist", "/contact" 에 대한 접근 성공
일반적으로 CSRF 토큰은 클라이언트와 서버 간의 요청에서 보안을 강화하기 위해 사용됩니다. 이 필터를 통해 서버는 응답 헤더에 CSRF 토큰 정보를 담아 클라이언트에게 전달하게 됩니다. 클라이언트는 이 토큰을 사용하여 이후의 요청에서 서버로 보내면, 서버는 이 토큰을 검증하여 요청이 유효한지 판단하게 됩니다.
✅ 코드 추가 !!!
✅✅✅
http.securityContext((context) -> context
.requireExplicitSave(false))
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.ALWAYS))
// CSRF 활성화 (조건)
.csrf((csrf) -> csrf.csrfTokenRequestHandler(requestHandler) // CSRF 토큰을 요청 핸들러에 설정
.ignoringRequestMatchers("/contact", "/register") // 특정 URL 경로(/contact, /register)에 대해서는 CSRF 검증을 무시하도록 설정
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()))// CSRF 토큰을 어떻게 저장할지 설정하는 부분 (쿠키를 사용하며, HttpOnly 속성을 false로 설정 -> JavaScript에서 쿠키에 접근)
✅✅✅ // CsrfCookieFilter 필터( 이 필터는 요청마다 한 번씩 실행되는 OncePerRequestFilter를 상속받아 구현) 추가 (Basic 인증이 처리된 후에 이 필터가 실행)
.addFilterAfter(new CsrfCookieFilter(), BasicAuthenticationFilter.class)
전체 코드
package com.example.springsecurity65.config;
import java.util.Collections;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import com.example.springsecurity65.filter.CsrfCookieFilter;
import jakarta.servlet.http.HttpServletRequest;
@Configuration
public class ProjectSecurityConfig {
@Bean
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
// 핸들러는 CSRF 토큰을 요청 속성(attribute)으로 설정하는 역할
CsrfTokenRequestAttributeHandler requestHandler = new CsrfTokenRequestAttributeHandler();
// CSRF 토큰의 이름을 "_csrf"로 설정. 즉, 요청이 들어올 때 "_csrf"라는 이름의 요청 속성(attribute)에 CSRF 토큰 값이 저장
requestHandler.setCsrfRequestAttributeName("_csrf");
// requireExplicitSave(false)는 보안 컨텍스트가 명시적으로 저장되어야 하는지 여부를 설정
http.securityContext((context) -> context
// false로 설정하면, 명시적으로 저장하지 않아도 됨.
.requireExplicitSave(false))
// sessionCreationPolicy(SessionCreationPolicy.ALWAYS)는 항상 새로운 세션을 생성하도록 설정합니다.
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.ALWAYS))
// http://localhost:4200 에 대한 CORS 허용
.cors(corsCustomizer -> corsCustomizer.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;
}
}))
// csrf 비활성화 (사이트 요청 위조) ---> csrf 토큰이 없어도 서버는 응답
// 스프링에서는 csrf 기본은 활성화 (보안 목적) ---> csrf 토큰을 url에 포함해야 서버는 응답
//.csrf((csrf) -> csrf.disable()) ---> CSRF 비활성화
// CSRF 활성화 (조건)
.csrf((csrf) -> csrf.csrfTokenRequestHandler(requestHandler) // CSRF 토큰을 요청 핸들러에 설정
.ignoringRequestMatchers("/contact", "/register") // 특정 URL 경로(/contact, /register)에 대해서는 CSRF 검증을 무시하도록 설정
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()))// CSRF 토큰을 어떻게 저장할지 설정하는 부분 (쿠키를 사용하며, HttpOnly 속성을 false로 설정 -> JavaScript에서 쿠키에 접근)
// CsrfCookieFilter 필터( 이 필터는 요청마다 한 번씩 실행되는 OncePerRequestFilter를 상속받아 구현) 추가 (Basic 인증이 처리된 후에 이 필터가 실행)
.addFilterAfter(new CsrfCookieFilter(), BasicAuthenticationFilter.class)
.authorizeHttpRequests((requests)->requests
.requestMatchers("/myAccount","/myBalance","/myLoans","/myCards", "/user").authenticated()
.requestMatchers("/notices","/contact","/register").permitAll())
.formLogin(Customizer.withDefaults())
.httpBasic(Customizer.withDefaults());
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
sessionCreationPolicy(SessionCreationPolicy.ALWAYS)는 항상 새로운 세션을 생성하도록 설정합니다. 이 설정은 다음과 같은 경우에 유용할 수 있습니다:
1.사용자가 로그인할 때마다 새로운 세션을 생성하려는 경우
2.사용자가 애플리케이션에 접근할 때마다 세션 정보를 새로 생성하려는 경우
기본적으로, Spring Security는 필요할 때만 세션을 생성합니다. 하지만 이 설정을 ALWAYS로 하면, 요청이 들어올 때마다 새로운 세션을 생성하게 됩니다. 이것은 특별한 요구 사항이 있을 때 유용하게 사용될 수 있습니다.
package com.example.springsecurity65.filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
public class CsrfCookieFilter extends OncePerRequestFilter {
// doFilterInternal 메서드를 오버라이드하여 실제 필터의 로직을 구현
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// CsrfToken 클래스의 인스턴스를 요청 속성에서 가져오기 (현재 요청에 대한 CSRF 토큰 정보를 가져오기)
CsrfToken csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
// 응답 헤더에 CSRF 토큰을 설정
if(null != csrfToken.getHeaderName()){
// CSRF 토큰의 이름(헤더 이름)과 값을 응답 헤더에 설정 -> 클라이언트 측에서 이 헤더를 읽어 CSRF 토큰을 알 수 있게 됨.
response.setHeader(csrfToken.getHeaderName(), csrfToken.getToken());
}
// 다음 필터로 요청과 응답을 전달
filterChain.doFilter(request, response);
}
}