SecurityBuilder의 build메소드를 호출하게 되면 필드로 등록된 SecurityConfigure들이 loop를 돌면서
init configue메소드를 내부적으로 호출하면서 초기화가 진행이 된다.
이 과정은 SpringBootApplication.run -> refreshContext 에서 @Configuration으로 등록된 HttpSecurityConfiguration , WebSecurityConfiguration을 통해서 초기화 과정중에 다 자동으로 일어나게 되는일이다.
public final class HttpSecurity extends AbstractConfiguredSecurityBuilder<DefaultSecurityFilterChain, HttpSecurity>
implements SecurityBuilder<DefaultSecurityFilterChain>, HttpSecurityBuilder<HttpSecurity> {
예를들면 HttpSecurity는 SecrurityBuilder의 구현체인데 AbstractConfiguredSecurityBuilder가 상속하는 AbstractSecurityBuilder 안에 build메소드가 존재한다.
dobuild를 호출하게 되면
init configure 메소드가 호출되게 된다.
SecurityBuiler는 여러개의 구현체를 가지고 있다. 그중 WebSecurity , HttpSecurity를 살펴본다.
HttpSecurityConfigruation에서 기본으로 HttpSecurity하나를 bean으로 제공한다.
apply에서 HttpSecurity#configurers list에 추가한 SecurityConfigurer 설정들을 init, configure 메소드를 호출하면서
Filter, Authentication Provider , Authentication Manager 등등을 초기화 해준다.
FilterChainProxy가 SecurityFilterChain을 리스트로 들고 있다.
WebSecurityConfiguration 에서 WebSecurity 객체를 만들고 build할 Configurer들을 WebSecurity에게 apply 해준다.
근데 Configurer 가 왠만하면 존재하지 않는다.
또한 FilterChainProxy를 만드는것이 주된 목적이다.
HttpSecurityConfiguration에서는 HttpSecurity를 생성하게 되는데 @Bean("prototype")으로 여러개를 생성할 수 있다.
사용자가 Custom하게 HttpSecurity 관련 설정을 만들지 않는 경우 WebSecurityConfiguration이 먼저 Bean생성을 위해 호출되지만 사용자가 Custom 하게 HttpSecurity 관련 설정을 할경우 HttpSecurityConfiguration을 통해 기본적인 SecurityConfigurer들을 HttpSecurity에 apply하여 사용자가 추가적으로 SecurityConfigurer등을 추가할수 있게 주입해준다.
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
class SpringBootWebSecurityConfiguration {
/**
* The default configuration for web security. It relies on Spring Security's
* content-negotiation strategy to determine what sort of authentication to use. If
* the user specifies their own {@code WebSecurityConfigurerAdapter} or
* {@link SecurityFilterChain} bean, this will back-off completely and the users
* should specify all the bits that they want to configure as part of the custom
* security configuration.
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnDefaultWebSecurity
static class SecurityFilterChainConfiguration {
@Bean
@Order(SecurityProperties.BASIC_AUTH_ORDER)
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().authenticated();
http.formLogin();
http.httpBasic();
return http.build();
}
}
Custom HttpSecurity설정이 없다면 기본적인 SecurityFilterChain을 만들어주게된다. 위에서 만든 HttpSecurity를 주입해준다. 추가적으로 formLogin()은 FormLoginConfigurer 를 추가하고 httpbasic도 자신의 Configurer를 추가하게 된다.
http.build로 들어가게 되면 이제 HttpSecurity configuerers 에 추가된 configurer들의 init , configure 메소드를 호출하게 된다. 제일 마지막으로 perfombuild 를 호출하게 된다.
init 과 configurer 호출을 통해 filter들이 만들어진것이다. FilterChain을 만들어서 return하게 되면 하나의 SecurityFilterChain을 bean으로 생성완료인것이다.
만들어진 filterchain을 WebSecurityConfiguration에서 주입받아 저장하게 된다.
주입받은 FilterChain을 WebSecurity에게 전달해준다. 그다음 build가 호출되어 init configrue performbuild를 호출하게 된다. 하지만 WebSecuirty 설정클래스들이 deprecated 되었기 때문에 init configure는 아무것도 하지않는다.
performBuild 메소드 안에서 만든 HttpSecurity에서 만든 securityFilterChain을 통해 FilterChainProxy를 만들게 된다.springSecurityFilterChain으로 Filter 빈이 하나 생성이 된다.
CustomSecurityConfigurer 만들기
@EnableWebSecurity
public class SecurityConfig {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().authenticated();
http.formLogin();
http.apply(new CustomSecurityConfigurer().setFlag(false));
return http.build();
}
}
기본으로 만들어진 HttpSecurity를 주입받아서 우리의 CustomConfigurer를 추가하는것이다.
@EnableWebSecurity에 @Configuration이 들어있어서 Bean을 만들 수 있다.
public class CustomSecurityConfigurer extends AbstractHttpConfigurer<CustomSecurityConfigurer, HttpSecurity> {
private boolean isSecure;
@Override
public void init(HttpSecurity builder) throws Exception {
super.init(builder);
System.out.println("init method started...");
}
@Override
public void configure(HttpSecurity builder) throws Exception {
super.configure(builder);
System.out.println("configure method started...");
if(isSecure) {
System.out.println("https is required");
} else {
System.out.println("https is optional");
}
}
public CustomSecurityConfigurer setFlag(boolean isSecure) {
this.isSecure = isSecure;
return this;
}
}
동일하게 init ,configure 메소드를 가지는 것을 확인한다. 초기화 과정에서 Configurer 리스트들에 포함되어 초기화 과정을 거치게 될 것 이다.
자동설정에 의한 초기화 과정 이해
ImporterSelector = 조건(DispatcherServlet.class 를 로딩했는지 -> 왜냐면 그래야 MVC가 동작할테니) 에 따라 설정클래스를 가져오게 된다. 위 예제에서는 WebMvc 설정을 로드함
SecurityFilterAutoConfiguration = DelegatingFilterProxy 는 FilterChainProxy에게 사용자의 요청을 위임해주는 빈인데 그것을 등록하는 과정을 거친다.
WebMvcSecurityConfiguration = ArgumentResolver들을 생성한다. (csrftAR, 등등)
그중 AuthenticaitonPricipalArguementResolver는 메소드에 인자에 @AuthenticationPrincipal 가 앞에 있으면 인증 받은 principal 를 바인딩 해준다.
이런 값 바인딩해주는 기능들은 MVC Controller단에서 쓰이기에WebMvcSecurityConfiguration 에서 생성하는것이다.
HttpSecurityConfiguration = HttpSecurity빈을 생성하는 scope가 prototype이다.
위에서 언급한 공통설정클래스(*Configurer 클래스들) 과 필터 생성및초기화
예를들면 FormLoginConfigurer 는 초기화시 UsernamePassword~Filter를 만들게 된다. 이런과정이 HttpSecurity에서 일어나는것이다.
SpringBootWebSecurityConfiguration에서 기본적인 인증을 강제하기위해 위에서만든 HttpSecurity를 주입받고 추가적인 인증 작업후 SecurityFilterChain 빈을 반환한다.
WebSecuirty#addSecurityFilterChainBuilder 메소드에서 람다로 HttpSecurity 가 만든 DefaultFilterChain을 인자로 넣어준다.
WebSecurity 안의 securityFilterChainBuilders에서 실제 앱에 적용할 SecurityFilterChain을 들고 있다가 WebSecurity의 perfomBuild 메소드가 호출되면 FilterChainProxy에 그 목록들을 적용시킨다.
그래야 FilterChainProxy가 요청이 들어왔을때 적용할 필터들을 사용할 수 있게 된다.
위에서 만든것처럼 우리가 SecurityFilterChain을 빈으로 만들면 기본 SpringBootWebSecurityConfiguration인 요놈이 따로 동작하지 않는다.
빨간줄 Annotation에서 SecurityFilterChain 이 Bean으로 등록이 되어있는가? 를 체크해보고 없는 경우 SpringSecurity에서 default로 하나만들어준다.
AuthenticationEntryPoint 이해
기본적으로 form , httpbasic login 을 제공한다. ExceptionHandlingConfigurer가 인증예외가 발생했을때 어떤 Authentication EntryPoint로 가야할지 알려주게되고 모든 EntryPoint설정을 담당하고 있다.
원래는 form, httpbasic 을 기본설정으로 가져가니 ExceptionHandlingConfigurer의 defaultEntryPointMappings 에 2개가 존재해야하는데 설정으로 form httpbasic을 제거하면 0개가 존재하게 된다.
이때 AuthenticationEntryPoint를 따로 커스텀하게 만들지 않았으면 Http403ForbiddenEntryPoint로 가게 되어 403 WhitePage가 뜨게 되고
@Configuration(proxyBeanMethods = false)
public class SecurityConfig {
@Bean
@Order(SecurityProperties.BASIC_AUTH_ORDER)
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().authenticated();
http.formLogin();
// http.basic();
http.exceptionHandling().authenticationEntryPoint(new CustomAuthenticationEntryPoint());
return http.build();
}
}
다음과 같이 기본으로 만들어져서 주입된 HttpSecurity 에 formLogin 메소드를 호출하면 FormLoginConfigurer 가 apply에 추가되어 init -> configure -> performBuild를 통해 필터를 만들어낸다.
init 과정에서 FormLoginConfigurer 의 authenticationEntryPoint에 LoginUrlAuthenticationEntryPoint를 주입시키고 doBuild 과정에서 exceptionHandlingConfigurer의 defaultEntryPointMappings 에 추가 된다.
마찬가지로 http.basic() 를 호출하게 되면 위와 동일하게 관련 필터를 만든다.
이 둘이 defaultEntryPointMapping에 저장되어 ExceptionTranslationFilter로 전달이 된다. 나중에 인증 예외가 발생했을시 처리할 수 있도록
3개의 설정이 다 주석해제해도 CustomEntryPoint가 최우선으로 적용되기때문에 localhost:8080 에 들어가도 formlogin, basic 둘다 적용이 안된다.
잘보면 Exception 과 인증이 관련되어있는것을 알 수 있다. spring의 예외처리 하는 부분에서 해당 AuthenticationEntryPoint를 등록해줘야 인증이 안된 요청이 왔을시 예외처리하여 인증을 요구 할 수 있는 것이다.
시큐리티 인증 및 인가 흐름 요약
DelegatingFilterProxy = 사용자의 요청을 처음 받는것은 was(tomcat) 의 servlet container이다.
was의 filter는 spring의 di,aop 기능들이 존재하지 않는다.
그래서 사용자의 요청을 SecurtyFilterChain(Spring의 모든 기능을 사용할 수 있는 영역)에게 넘겨주는 역할을 담당한다.
AuthenticationFilter 가 로그인 요청시 가장 먼저 거치는 filter이다. userid, password로 Authentication 객체를 만든다.
AuthenticationManager = 인증 처리를 실질적으로 할수 있는 클래스를 찾아서 위임하는 역할이다.
AuthenticationProvider = 인증에 성공 및 실패를 실질적으로 구분하는 곳이다. db에 해당 userid가 존재하는지 UserDetailService를 통해서 확인한다.
존재하면 해당 유저의 정보를 UserDetails 로 만들어줘서 반환한다.
id가 존재하는 것이 검증이 된것이고 이제 password를 PasswordEncoder를 활용하여 검증한다.
실패하면 AuthenticationFilter로 가서 예외처리를 하게되고
성공하면 인증된 Authentication 객체를 다시 만들어줘서 위로 전달하고 SecurityContext에 저장한다.
시나리오
처음 root url 로 요청이 들어오면 FilterChainProxy에서 모든 Filter들을 다 거치게 된다. 이때 ExceptionTranslationFilter를 거치고 바로 다음으로 FitlerSecurityInterceptor로 전달한다.
여기서 아직 인증을 하지 않은 AnonymousUser임으로(인증이 안된것으로 간주한다.)
AccessDecisionVoter 에서 인가에러를 발생시킨다. AccessDeniedException을 던진다.
ExceptionTranslationFilter에서 해당 인가에러를 catch 문에 잡는다. AccessDeniedException으로 떨어지지만 더 들어가면 isAnonymous를 확인하여 다시 인증 에러로 바꿔준다.
SecurtyContext를 초기화 하고 LoginAuthenticationEntryPoint를 통해서 login하도록 돌려보낸다.
그러면 다시 FilterChainProxy로 오게되고 이번에는 UsernamePasswordAuthenticationFilter에서 이번엔 /login 요청임으로 인증 처리를 하게 된다.
아까는 FilterChainProxy에서 /login 요청이 아니라 / 요청이었으므로 그냥 넘어갔었다.
AuthenticationManager의 구현체인 ProviderManager에서 DaoAUthenticationProvider를 통해 id 는 db에서 UserDetailService에서 가져오고 pw도 맞는지 확인한다. 맞으면 Authentication 객체를 새로 만들고
AbstractAuthetnicationProcessingFilter(UsernamePasswordAuthenticationFilter가 상속해서 구현한다.)
로 돌아와서 SecurtyContext에 저장을 하게 된다.
다시 ExceptionTranslationFilter -> AccesDecisionVoter로 가는데 이번엔 ACCESS_GRANTED로 승인하게 되어 MVC 레이어로 진입이 가능해진다.
HTTP Basic 인증
1. 클라이언트는 인증정보 없이 서버로 접속을 시도한다.
2. 서버가 클라이언트에게 인증 요구를 보낼 때 401 Unauthorized 응답과 함께 WWW-Authenticate 헤더를 기술해서 realm(보안영역) 과 Basic 인증방법을 보냄
3. 클라이언트가 서버로 접속할 때 Base64 로 username과 password 를 인코딩하고 Authorization 헤더에 담아서 요청함
4. 성공적으로 완료되면 정상적인 상태코드를 반환한다.
HttpBasicConfigurer 를 통해 BasicAuthenticationFilter를 만들어낸다.
BasicAuthenticationConverter 를 사용해서 요청 헤더에 기술된 인증정보의 유효성을 체크
인증이 성공하면 SecurityContext에 인증된 Authentication 저장
실패하면 다시 인증할 수 있게 BasicAuthenticationEntryPoint 호출
세션을 사용하는 경우 매요청마다 인증과정을 거치게 된다. httpSession에 저장하게 되는것은 SecurityContextImpl을 저장하게 된다. 이 안에 인증된 Authentication 객체가 존재한다.
UsernamePasswordAuthenticationToken이 Authentication 객체이다.
Cors (Cross Origin Resource Sharing)
HTTP 헤더를 사용하여 ,한 출처(도메인)에서 실행중인 웹 앱이 다른 출처(도메인)의 선택한 자원에 접근할 수 있는 권한을 부여하도록 브라우저에 알려주는 체제
웹 앱이 리소스가 자신의 도메인과 다를 때 브라우저는 요청 헤더에 Origin 필드에 요청 출처를 담아
교차 출처HTTP 요청을 실행한다. 이때 출처를 비교하는 로직은 브라우저단에서 구현되어있다.
두개의 출처를 비교할때 URL 구성요소의 Protocol, Host, Port 3가지만 동일한지 확인한다.
서버에서 Access-Control-Allow-Origin 을 통해 허용하면 브라우저도 허용하게 할수있다.
Simple Request
Preflight(예비 요청) 과정없이 바로 서버에게 본요청을 한후 서버가 Access-Control-Allow-Origin를 전송하면 브라우저에서 CORS 정책을 비교하게 된다.
간단한만큼 제약사항이 존재한다. 이를 만족해야 Simple Request로 날아간다.
1. GET, POST, HEAD 중 한가지 메소드를 사용해야함
2. Custom Header는 허용되지 않는다.
3. json 은 안된다.
PreFlight Request
브라우저가 한번에 요청을 보내지 않고 예비 요청(preflight)를 먼저 보내본다. 이때 OPTIONS 메소드가 사용된다.
브라우저 스스로가 이 요청이 안전한 요청인지 확인하는과정이다.
예비 요청에서 본요청이 어떤 메소드를 사용할 것인지 알려준다.
url 말고 여러 제한을 걸수 있다.
인증이 필요한 경우 요청을 보내는 client (javascript)에서는 credential:include 받는 서버쪽에서는 true 를 해줘야한다.
CorsConfigurer
Spring security 필터 체인에 CorsFilter를 추가한다.
corsFilter 빈이 없으면 CosrConfigurationSource 빈을 사용
CosrConfiguration 빈이 없고 Spring MVC가 클래스 경로에 있으면 HandlerMappingIntrospector 사용
CorsFilter
CORS preflight 를 처리하고 본 요청,simple request를 가로채고 제공된 CorsConfigurationSource를 통해 일치된 정책에 따라 CORS 응답헤더와 같은 응답을 업데이트하기 위한 필터이다.
Spring MVC Java 구성과 Spring MVC XML네임스페이스에서 CORS를구성하는 대안이라 볼수 있다.= @CorsOrign
@Configuration(proxyBeanMethods = false)
public class SecurityConfig {
@Bean
@Order(SecurityProperties.BASIC_AUTH_ORDER)
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().permitAll();
http.cors().configurationSource(corsConfigurationSource());
return http.build();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.addAllowedOrigin("*");
configuration.addAllowedMethod("*");
configuration.addAllowedHeader("*");
// configuration.setAllowCredentials(true);
configuration.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
CorsConfigurationSource는 인터페이스이다. 이중 구현체인 UrlBasedCorsConfigurationSource를 사용한다. 어떤 url 경로에 해당 Configuration을 적용할것인지 결정할 수 있다.
setAllowedCredential(true) 인 경우 addAllowedOrigin에 *을 쓸수 없고 명시적으로 허용할 url 을 적어줘야한다.
'WEB > Security' 카테고리의 다른 글
OAuth 2.0 권한부여 유형 (0) | 2022.11.17 |
---|---|
OAuth 2.0 용어 이해 (0) | 2022.11.16 |
앱 설명 (0) | 2022.06.08 |
Lesson 33,34 - Integration testing for Spring Security implementations - Part 1,2 (0) | 2022.05.22 |
Lesson 29 - Using permissions (0) | 2022.05.20 |