163 lines
11 KiB
Markdown
163 lines
11 KiB
Markdown
# Spring Security
|
||
## Spring Security简介
|
||
Spring Security作为一个安全框架,向使用者提供了用户认证、授权、常规攻击保护等功能。
|
||
## Spring Security自动配置
|
||
默认情况下,在引入Spring Security的启动器依赖之后,Spring Boot自动配置会做如下工作:
|
||
- 启用Spring Security的默认配置,创建一个的bean对象,bean对象名称为“springSecurityFilterChain”,bean对象类型为SecurityFilterChain,实现了Filter。该bean对象为负责应用中所有与安全相关的操作(例如验证提交的username和password,保护应用的url等)
|
||
- 创建一个UserDetailsService的bean对象,并且产生一个为”user“的username和一个随机产生的password,随机产生的password会输出在console日志中
|
||
- 将名为springSecurityFilterChain的bean对象注册到servlet容器中,用来对每个servlet请求进行处理
|
||
> Spring Security会通过Spring Security会通过BCtypt(Hash解密算法)来对密码的存储进行保护
|
||
|
||
## Spring Security结构
|
||
### DelegatingFilterProxy
|
||
Spring提供了Filter的一个实现类DelegatingFilterProxy,其将Servlet容器的生命周期和Spring的ApplicationContext连接在一起。
|
||
***DelegatingFilterProxy会通过标准的servlet容器机制被注册,但是将所有工作都委托给Spring容器中实现了Filter的bean对象***。
|
||
> DelegatingFilterProxy会在ApplicationContext中查找实现了Filter的bean对象,并且调用该bean对象的doFilter方法
|
||
> ```java
|
||
> public void doFilter(ServletRequest request,
|
||
> ServletResponse response, FilterChain chain) {
|
||
> // Lazily get Filter that was registered as a Spring Bean
|
||
> // For the example in DelegatingFilterProxy delegate is an instance of Bean Filter0
|
||
> Filter delegate = getFilterBean(someBeanName);
|
||
> // delegate work to the Spring Bean
|
||
> delegate.doFilter(request, response);
|
||
> }
|
||
> ```
|
||
|
||
### FilterChainProxy
|
||
Spring Security支持FilterChainProxy,FilterChainProxy是一个由Spring Security提供的特殊Filter,FilterChainProxy通过SecurityFilterChain允许向多个Filters委托工作。
|
||
***FilterChainProxy是一个bean对象,通过被包含在DelegatingFilterProxy中。***
|
||
|
||
### SecurityFilterChain
|
||
SecurityFilterChain通常被FilterChainProxy使用,用来决定在该次请求中调用那些Spring Security Filters。
|
||
|
||
### SecurityFilters
|
||
Security Filters通过SecurityFilterChain API被插入到FilterChainProxy中。
|
||
### 处理Security异常
|
||
ExceptionTranslationFilter将认证异常和权限异常翻译为http response。
|
||
> ExceptionTranslationFilter被插入到FilterChainProxy中,作为SecurityFilterChain中的一个。
|
||
> 如果应用程序没有抛出AccessDeniedException或AuthenticationException,那么ExceptionTranslationFilter并不会做任何事情。
|
||
|
||
```java
|
||
// ExceptionTranslationFilter的伪代码
|
||
try {
|
||
filterChain.doFilter(request, response);
|
||
} catch (AccessDeniedException | AuthenticationException ex) {
|
||
if (!authenticated || ex instanceof AuthenticationException) {
|
||
startAuthentication();
|
||
} else {
|
||
accessDenied();
|
||
}
|
||
}
|
||
```
|
||
|
||
## Spring Security身份认证的结构
|
||
### SecurityContextHolder
|
||
Spring Security身份认证的核心模型,SecurityContextHolder包含有SecurityContext。
|
||
> Spring Security将被认证用户的详细信息(details)存储在SecurityContextHolder中。Spring Security不在乎该SecurityContextHolder是如何被填充的,只要该SecurityContextHolder有值,那么其将会被用作当前已经被认证的用户。
|
||
|
||
> ***将用户标识为已认证的最简单的方法是为该用户设置SecurityContextHolder。***
|
||
|
||
```java
|
||
// 设置SecurityContextHolder
|
||
SecurityContext context = SecurityContextHolder.createEmptyContext();
|
||
Authentication authentication =
|
||
new TestingAuthenticationToken("username", "password", "ROLE_USER");
|
||
context.setAuthentication(authentication);
|
||
|
||
SecurityContextHolder.setContext(context);
|
||
```
|
||
默认情况下,SecurityContextHolder通过ThreadLocal来存储SecurityContext,故而SecurityContext对于位于同一线程之下的方法来说都可以访问。
|
||
> 使用ThreadLocal来存储SecurityContext是相当安全的,如果想要在该已认证主体的请求被处理完成之后清除SecurityContext,Spring Security中的FilterChainProxy会保证该SecurityContext被清除。
|
||
|
||
### SecurityContext
|
||
SecurityContext从SecurityContextHolder中获得,SecurityContext中含有Authentication对象。
|
||
|
||
### Authentication
|
||
Authentication在Spring Security中具有两个目的:
|
||
- 作为AuthenticationManager的输入,用于提供待认证用户的认证凭证。当用于该场景下时,isAuthenticated()方法返回值应该为false
|
||
- 代表当前已经认证过的用户。当前的Authentication可以从SecurityContext中获取,而默认情况下SecurityContext是存储在ThreadLocal中的
|
||
|
||
Authentication含有如下部分:
|
||
- 主体(principal):用于标识用户,当通过username/password进行认证时,其通常是UserDetails类的实例
|
||
- 凭据(credentials):通常是password,在许多场景下凭据会在用户认证成功之后被清空,为了保证凭据不会被泄露
|
||
- 权限(authorities):该GrantedAuthority集合是用户被授予的高层次许可。许可通常是用户角色或者作用域范围。
|
||
|
||
### GrantedAuthority
|
||
GrantedAuthority是用户被授予的高层次许可,譬如用户角色或者作用域范围。
|
||
GrantedAuthority可以通过Authentication.getAuthorities()方法来获得,该方法会返回一个GrantedAuthentication的集合。每个GrantedAuthentication都是一项被授予该用户的权限。
|
||
|
||
### AuthenticationManager
|
||
AuthenticationManager的API定义了Security Filters如何来执行身份认证。对于身份认证返回的Authentication,会被调用AuthenticationManager的controller设置到SecurityContextHolder中。
|
||
> AuthenticationManager的实现可以是任何类,但是最通用的实现仍然是ProviderManager
|
||
|
||
### ProviderManager
|
||
ProviderManager是AuthenticationManager的最通用实现。ProviderManager将工作委托给一系列AuthenticationProvider。
|
||
> 对于每个ProviderManager,都可以决定将该认证标识为成功、失败,或者将认证工作委托给下游AuthenticationProvider。
|
||
> 如果所有的AuthenticationProvider都没有将该认证标识为成功或者失败,那么整个认证流程失败,并且抛出ProviderNotFoundException异常。
|
||
> ProviderNotFoundException是一个特殊的AuthenticationException,该异常代表对传入Authentication的类型并没有配置该类型的ProviderManager
|
||
|
||
> 在实践中,每个AuthenticationProvider知道如何处理一种特定类型的Authentication
|
||
|
||
默认情况下,ProviderManager在认证请求成功后会尝试清除返回的Authentication对象中任何敏感的凭据信息,这将会保证password等敏感信息保存时间尽可能地短,减少泄露的风险。
|
||
|
||
### AuthenticationProvider
|
||
复数个AuthenticationProvider可以被注入到ProviderManager中,每个AuthenticationProvider可以负责一种专门类型的认证,例如DaoAuthenticationProvider负责username/password认证,JwtAuthenticationProvider负责jwt token的认证。
|
||
|
||
### AuthenticationEntryPoint
|
||
AuthenticationEntryPoint用来发送一个Http Response,用来向客户端请求认证凭据。
|
||
某些情况下,客户端在请求资源时会主动在请求中包含凭据,如username/password等。在这种情况下,服务端并不需要再专门发送Http Response来向客户端请求认证凭据。
|
||
> AuthenticationEntryPoint用来向客户端请求认证凭据,AuthenticationEntryPoint的实现可能会执行一个从定向操作,将请求重定向到一个登录页面用于获取凭据,并且返回一个WWW-Authentication Header。
|
||
|
||
### AbstractAuthenticationProcessingFilter
|
||
该类作为base filter用来对用户的凭据进行认证。再凭据被认证之前,Spring Security通常会通过AuthenticationEntryPoint向客户端请求认证凭据。
|
||
之后,AbstractAuthenticationProcessingFilter会对提交的任何认证请求进行认证。
|
||
#### 认证流程
|
||
1. 当用户提交认证凭据之后,AbstractAuthenticationProcesingFilter会根据HttpServletRequest对象创建一个Authentication对象用于认证,该Authentication的类型取决于AbstractAuthenticationProcessingFilter的子类类型。例如,UsernamePasswordAuthenticationFilter会通过提交request中的username和password创建UsernamePasswordAuthenticationToken。
|
||
2. 然后将构建产生的Authentication对象传入到AuthenticationManager中,进行认证
|
||
3. 如果认证失败,那么会失败,SecurityContextHolder会被清空,RememberMeService.logFail方法将会被调用,AuthenticationFailureHandler也会被调用
|
||
4. 如果认证成功,那么SessionAuthenticationStrategy将会收到登录的通知
|
||
5. 该Authentication对象再认证成功之后将会被设置到SecurityContextHolder中
|
||
6. RemeberMeService.logSuccess将会被调用
|
||
7. ApplicationEventPublisher发布InteractiveAuthenticationSuccessEvent.
|
||
8. AuthenticationSuccessHandler被调用
|
||
|
||
## 用户名/密码认证
|
||
### Form Login
|
||
Spring Security为以表单形式提供的用户名和密码认证提供支持。
|
||
#### 用户被重定向到登录页面的过程
|
||
1. 用户发送了一个没有经过身份认证的请求到指定资源,并且待请求的资源对该用户来说是未授权的
|
||
2. Spring Security中FilterSecurityInterceptor抛出AccessDeniedException,代表该未授权的请求被拒绝
|
||
3. 因为该用户没有经过认证,故而ExceptionTransactionFilter发起了开始认证的过程,并且使用配置好的AuthenticationEntryPoint向登录页面发起了重定向。在大多数情况下AuthenticationEntryPoint都是LoginUrlAuthenticationEntryPoint
|
||
4. 浏览器接下来会请求重定向到的登陆页面
|
||
|
||
当用户名和密码提交后,UsernamePasswordAuthenticationFilter会对username和password进行认证。UsernamePasswordAuthenticationFilter继承了AbstractAuthenticationProcessingFilter。
|
||
|
||
#### 认证用户名和密码过程
|
||
1. 当用户提交了其用户名和密码之后,UsernamePasswordAuthenticationFilter会创建一个UsernamePasswordAuthenticationToken
|
||
2. 创建的UsernamePasswordAuthenticationToken会传入AuthenticationManager中进行认证
|
||
3. 如果认证失败,那么SecurityContextHolder会被清除,RememberMeService.logFailure和AuthenticationFailureHandler会被调用
|
||
4. 如果认证成功,那么SessionAuthenticationStrategy将会收到登录的通知,RemeberMeService.logSuccess和AuthenticationSuccessHandler会被调用,ApplicationEventPublisher发布InteractiveAuthenticationSuccessEvent
|
||
|
||
Spring Security Form Login默认情况下是开启的,但是,一旦任何基于servlet的配置被提供,那么基于表单的login也必须要显式指定。
|
||
```java
|
||
// 显式指定form login的配置
|
||
@Bean
|
||
public SecurityFilterChain filterChain(HttpSecurity http) {
|
||
http
|
||
.formLogin(withDefaults());
|
||
// ...
|
||
}
|
||
```
|
||
如果想要自定义login form page,可以使用如下配置
|
||
```java
|
||
public SecurityFilterChain filterChain(HttpSecurity http) {
|
||
http
|
||
.formLogin(form -> form
|
||
.loginPage("/login")
|
||
.permitAll()
|
||
);
|
||
// ...
|
||
}
|
||
```
|