548 lines
30 KiB
Markdown
548 lines
30 KiB
Markdown
- [Spring Security](#spring-security)
|
||
- [Spring Security简介](#spring-security简介)
|
||
- [Spring Security自动配置](#spring-security自动配置)
|
||
- [Spring Security结构](#spring-security结构)
|
||
- [DelegatingFilterProxy](#delegatingfilterproxy)
|
||
- [FilterChainProxy](#filterchainproxy)
|
||
- [SecurityFilterChain](#securityfilterchain)
|
||
- [SecurityFilters](#securityfilters)
|
||
- [添加自定义filter到SecurityFilterChain](#添加自定义filter到securityfilterchain)
|
||
- [处理Security异常](#处理security异常)
|
||
- [在多次认证之间保存request](#在多次认证之间保存request)
|
||
- [RequestCache](#requestcache)
|
||
- [Spring Security身份认证架构](#spring-security身份认证架构)
|
||
- [SecurityContextHolder](#securitycontextholder)
|
||
- [SecurityContext](#securitycontext)
|
||
- [Authentication](#authentication)
|
||
- [GrantedAuthority](#grantedauthority)
|
||
- [AuthenticationManager](#authenticationmanager)
|
||
- [ProviderManager](#providermanager)
|
||
- [AuthenticationProvider](#authenticationprovider)
|
||
- [AuthenticationEntryPoint](#authenticationentrypoint)
|
||
- [AbstractAuthenticationProcessingFilter](#abstractauthenticationprocessingfilter)
|
||
- [用户名/密码认证](#用户名密码认证)
|
||
- [Form Login](#form-login)
|
||
- [用户被重定向到登录页面的过程](#用户被重定向到登录页面的过程)
|
||
- [认证用户名和密码过程](#认证用户名和密码过程)
|
||
- [基本Authentication](#基本authentication)
|
||
- [基本Authentication的认证流程](#基本authentication的认证流程)
|
||
- [Digest Authentication(摘要认证,***不安全***)](#digest-authentication摘要认证不安全)
|
||
- [摘要认证中的随机数](#摘要认证中的随机数)
|
||
- [密码存储方式](#密码存储方式)
|
||
- [内存中存储密码](#内存中存储密码)
|
||
- [内存中存储密码时使用defaultPasswordEncoder](#内存中存储密码时使用defaultpasswordencoder)
|
||
- [JDBC Authentication](#jdbc-authentication)
|
||
- [User Schema](#user--schema)
|
||
- [Group Schema](#group-schema)
|
||
- [配置Datasource](#配置datasource)
|
||
- [创建JdbcUserDetailsManager Bean对象](#创建jdbcuserdetailsmanager-bean对象)
|
||
- [UserDetails](#userdetails)
|
||
- [UserDetailsService](#userdetailsservice)
|
||
- [PasswordEncoder](#passwordencoder)
|
||
- [DaoAuthenticationProvider](#daoauthenticationprovider)
|
||
|
||
|
||
# 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中。
|
||
|
||
### 添加自定义filter到SecurityFilterChain
|
||
大多数情况下,默认的security是足够的,但是,也允许向security filter chain中添加自定义过滤器。
|
||
|
||
如下是一个自定义过滤器的示例,示例中自定义filter会在认证后校验用户对于请求的租户是否拥有权限:
|
||
```java
|
||
import java.io.IOException;
|
||
|
||
import jakarta.servlet.Filter;
|
||
import jakarta.servlet.FilterChain;
|
||
import jakarta.servlet.ServletException;
|
||
import jakarta.servlet.ServletRequest;
|
||
import jakarta.servlet.ServletResponse;
|
||
import jakarta.servlet.http.HttpServletRequest;
|
||
import jakarta.servlet.http.HttpServletResponse;
|
||
|
||
import org.springframework.security.access.AccessDeniedException;
|
||
|
||
public class TenantFilter implements Filter {
|
||
|
||
@Override
|
||
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
|
||
HttpServletRequest request = (HttpServletRequest) servletRequest;
|
||
HttpServletResponse response = (HttpServletResponse) servletResponse;
|
||
|
||
String tenantId = request.getHeader("X-Tenant-Id"); //(1)
|
||
boolean hasAccess = isUserAllowed(tenantId); //(2)
|
||
if (hasAccess) {
|
||
filterChain.doFilter(request, response); //(3)
|
||
return;
|
||
}
|
||
throw new AccessDeniedException("Access denied"); //(4)
|
||
}
|
||
|
||
}
|
||
```
|
||
如下是自定义过滤器链的流程:
|
||
1. 步骤从请求header中获取了tenantId
|
||
2. 步骤校验了用户对于该tenant id代表的租户资源是否拥有访问权限
|
||
3. 只有当用户对租户资源拥有访问权限时,才会继续调用filterChain中剩余的部分
|
||
4. 如果用户没有访问权限,将会抛出AccessDeniedException
|
||
|
||
在定义完自定义过滤器后,还需要将自定义filter添加到Security Filter Chain中,添加代码如下所示例:
|
||
```java
|
||
@Bean
|
||
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||
http
|
||
// ...
|
||
.addFilterBefore(new TenantFilter(), AuthorizationFilter.class);
|
||
return http.build();
|
||
}
|
||
```
|
||
> 由于权限校验操作在身份认证之后,故而将TenantFilter加到AuthorizationFilter之前可以保证在执行TenantFilter时,所有的身份认证操作都已经执行完成。
|
||
|
||
> 在向SecurityFilterChain注册filter时,不应该将自定义filter注册为bean,因为spring会将注册为bean的filter对象都添加到内置容器中,这会造成注册为bean的filter被调用两次:一次被Spring Seurity调用,一次被container调用。
|
||
|
||
|
||
|
||
### 处理Security异常
|
||
ExceptionTranslationFilter将`AccessDeniedException`和`AuthenticationException`翻译为http response。
|
||
|
||
> ExceptionTranslationFilter被插入到FilterChainProxy中,作为SecurityFilterChain中的一个filter。
|
||
> 如果应用程序没有抛出AccessDeniedException或AuthenticationException,那么ExceptionTranslationFilter并不会做任何事情。
|
||
|
||
ExceptionTranslationFilter的处理流程:
|
||
1. ExceptionTranslationFilter首先会调用`filterChain.doFilter`来执行后续的逻辑
|
||
2. 在执行后续请求时,如果抛出了AuthenticationException,那么则会开启认证流程
|
||
1. 清空SecurityContextHolder
|
||
2. 保存HttpServletRequest,当认证通过时,还能重新发送原始请求
|
||
3. `AuthenticationEntryPoint`用于重新请求用户的凭证,其可能会重定向到登录页面或是发送 `www-authenticate`header
|
||
3. 在执行后续请求时,如果抛出了AccessDeniedException,那么会调用`AccessDeniedHandler`用于处理访问拒绝。
|
||
4. 如果执行后续逻辑时,抛出了非`AuthenticationException`和`AccessDeniedException`的异常,那么异常将会被重新抛出,ExceptionTranslationFilter并不会针对其进行处理
|
||
|
||
```java
|
||
// ExceptionTranslationFilter的伪代码
|
||
try {
|
||
filterChain.doFilter(request, response);
|
||
} catch (AccessDeniedException | AuthenticationException ex) {
|
||
if (!authenticated || ex instanceof AuthenticationException) {
|
||
startAuthentication();
|
||
} else {
|
||
accessDenied();
|
||
}
|
||
}
|
||
```
|
||
|
||
### 在多次认证之间保存request
|
||
如果用户在发送请求访问需要认证的资源时,尚未经过身份认证,那么需要将本次请求保存起来,并且对用户进行认证,待用户认证通过之后,再重新执行保存的原始请求。在Spring Security中,是通过RequestCache来实现保存请求的。
|
||
|
||
#### RequestCache
|
||
HttpServletRequest被保存在RequestCache中,当用户经过重新认证后,RequestCache将会用于重新发送原始请求。
|
||
|
||
在Spring Security中,在sendStartAuthentication时会像requestCache中存储request,并且重新执行认证流程,认证通过后,将会重新发送原始请求。
|
||
|
||
RequestCacheAwareFilter负责请求的重新发送。
|
||
|
||
默认情况下,会使用`HttpSessionRequestCache`,如下代码展示了HttpSessionRequestCache的自定义使用:
|
||
```java
|
||
@Bean
|
||
DefaultSecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
|
||
HttpSessionRequestCache requestCache = new HttpSessionRequestCache();
|
||
// 匹配本次request时,需要确保请求url参数中具有continue参数
|
||
requestCache.setMatchingRequestParameterName("continue");
|
||
http
|
||
// ...
|
||
.requestCache((cache) -> cache
|
||
.requestCache(requestCache)
|
||
);
|
||
return http.build();
|
||
}
|
||
```
|
||
|
||
由于在认证失败时存储原始请求会占用内存,如果想要禁止在抛出AuthenticationException后,保存原始请求到RequestCache的行为,可以为SecurityFilterChain指定`NullRequestCache`:
|
||
```java
|
||
@Bean
|
||
SecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
|
||
RequestCache nullRequestCache = new NullRequestCache();
|
||
http
|
||
// ...
|
||
.requestCache((cache) -> cache
|
||
.requestCache(nullRequestCache)
|
||
);
|
||
return http.build();
|
||
}
|
||
```
|
||
|
||
## Spring Security身份认证架构
|
||
### SecurityContextHolder
|
||
SecurityContextHolder用于存储用户的认证信息,是Spring Security身份认证认证模型的核心,SecurityContextHolder中包含有SecurityContextHolder,SecurityContextHolder负责将当前线程和SecurityContext相关联。
|
||
|
||
如果想要将用户表示为已经通过身份认证,最简单的方式是向SecurityContextHolder中设置值,示例如下:
|
||
|
||
```java
|
||
SecurityContext context = SecurityContextHolder.createEmptyContext(); // 1
|
||
Authentication authentication =
|
||
new TestingAuthenticationToken("username", "password", "ROLE_USER"); //2
|
||
context.setAuthentication(authentication);
|
||
|
||
SecurityContextHolder.setContext(context); //3
|
||
```
|
||
|
||
上述流程如下所示:
|
||
1. 创建了一个空的SecurityContext。在最开始,应该通过createEmptyContext方法创建一个空的SecurityContext,而不是使用`getContext().setAuthentication(authentication)`,从而避免在多线程下的竞争场景
|
||
2. 第二步中,创建了一个Authentication对象,Spring Security不关注向SecurityContext中塞入了哪种类型的Authentication实现
|
||
3. 在第三步中,将SecurityContext设置到了SecurityContextHolder中,spring使用设置的SecurityContext信息进行身份认证
|
||
|
||
如果后续操作想要访问当前已认证用户的信息,可以访问SecurityContextHolder。
|
||
|
||
```java
|
||
SecurityContext context = SecurityContextHolder.getContext();
|
||
Authentication authentication = context.getAuthentication();
|
||
String username = authentication.getName();
|
||
Object principal = authentication.getPrincipal();
|
||
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
|
||
```
|
||
|
||
在默认情况下,`SecurityContextHolder`使用ThreadLocal来存SecurityContext,故而SecurityContext在同一线程内可以共享。在SecurityContextHolder中使用ThreadLocal是安全的,不会发生当前请求完成而SecurityContext未被清理的情况,Spring的FilterChainProxy保证SecurityContext会被清理。
|
||
|
||
在一些场景下,可能不希望使用ThreadLocal来存储ContextHolder,例如在swing程序下,可能希望整个java虚拟机中的线程都共享同一个security context。可以在启动时为SecurityContextHolder指定一个strategy,该strategy决定context应该如何被存储。
|
||
|
||
对于应用程序内所有线程共享的security context,可以使用`SecurityContextHolder.MODE_GLOBAL`策略。
|
||
|
||
### SecurityContext
|
||
Security Context用于存储用户的身份认证信息,从SecurityContextHolder中获取。
|
||
|
||
### Authentication
|
||
Authentication在Spring Security中具有两种使用场景:
|
||
- 在用户尚未被认证的场景,向`AuthenticationManager`提供用于身份认证的凭证
|
||
- 在用户已经被认证的场景,Authentication用于代表当前已经通过认证的用户,此时可以通过SecurityContext来获取用户认证信息
|
||
|
||
Authentication包含有如下信息:
|
||
- 主体(principal):用于标识用户,当通过username/password进行认证时,其通常是UserDetails类的实例
|
||
- 凭据(credentials):通常是password,在许多场景下为了保证凭证不会被泄露,在用户通过认证后,凭证将会被清空
|
||
- 权限(authorities):代表用户被授予的权限,为`GrantedAuthority`的实例集合
|
||
|
||
### GrantedAuthority
|
||
GrantedAuthority是用户被授予的权限。
|
||
|
||
可以通过`Authentication.getAuthorities()`方法来获取用户被授予的权限集合,该方法会返回`GrantedAuthority`的集合。
|
||
|
||
### AuthenticationManager
|
||
AuthenticationManager定义了Security Filters如何执行认证。
|
||
|
||
AuthenticationManager会对传递给其的Authentication执行认证操作,如果认证成功,会返回一个被完全填充的Authetication对象。
|
||
|
||
在AuthenticationManager的authenticate方法返回Authentication对象之后,调用AuthenticationManager的controller会将返回的Authentication对象设置到SecurityContextHolder中。
|
||
|
||
AuthenticationManager的实现可以是任何类,最通用的实现类是`ProviderManager`.
|
||
|
||
### ProviderManager
|
||
ProviderManager是最常用的AuthenticationManager实现类,ProviderMananger将认证工作委托给了一个`AuthenticationProvider`的实例集合。在集合中的每一个AuthenticationProvider,都可以决定当前认证工作是认证成功、认证失败还是将认证工作交由集合中的后续AuthenticationProvider决定。
|
||
|
||
如果AuthenticationProvider集合中没有任何AuthenticationProvider可以决定当前认证工作是成功或是失败,那么当前认证工作会失败,并且抛出`ProviderNotFoundException`异常。该异常代表当前的认证方式没有被支持。
|
||
|
||
> 在实践中,每个AuthenticationProvider都明确其应该负责的认证方式,例如一个AuthenticationProvider负责用户名/密码形式的登录,另一个AuthenticationProvider负责saml形式的登录。故而,可以在支持多种认证方式的情况下,只通过一个AuthenticationManager bean对象来进行整合多种认证方式。
|
||
|
||
对于ProviderManager,可以配置一个parent ProviderManager,当本级没有AuthenticationProvider能够完成认证工作时,将会将认证工作委托给父级manager。
|
||
|
||
默认情况下,ProviderManager将会在认证成功后,清空任任何成功登录后返回Authenticaiton对象的凭证信息。
|
||
|
||
### AuthenticationProvider
|
||
AuthenticationProvider负责执行特定类型的认证工作,可以将多个AuthenticationProvider对象注入到同一个ProviderMananger中。
|
||
|
||
例如,DaoAuthenticationProvider支持基于用户名/密码的认证,JwtAuthenticationManager则是支持基于jwt形式的认证。
|
||
|
||
### AuthenticationEntryPoint
|
||
AuthenticationEntryPoint用于服务端向用户请求凭证(例如,重定向到登录页面,向客户端发送`WWW-Authenticate`响应等。)
|
||
|
||
一些场景下,用户在请求资源时会主动发送在请求中包含凭据。在这些场景下,spring security不用向客户返回相应来要求客户提供凭证。
|
||
|
||
在其他场景下,客户端可能在未认证的情况下发送请求来访问资源,这时,可以使用`AuthenticationEntryPoint`的实现来向客户请求凭证。`AuthenticationEntryPoint`的实现可以重定向到登录页,或是向客户端发送`WWW-Authenticate`响应,或是采用其他方式来向客户端请求凭据。
|
||
|
||
### AbstractAuthenticationProcessingFilter
|
||
`AbstractAuthenticationProcessingFilter`用于认证用户的凭据。在凭据可以被认证之前,spring security通过会通过`AuthentciationEntryPoint`来向客户请求凭据。
|
||
|
||
AbstractAuthenticationProcessingFilter通过如下流程来执行认证:
|
||
1. 当用户提交凭据时,AbstractAuthenticationProcessingFilter会根据将要被认证的request来创建一个Authentication对象,创建Authentication的类型基于AbstractAuthenticationProcessingFilter的继承类。例如,UsernamePasswordAuthenticationFilter将会基于request中提交的username和password创建一个UsernamePasswordAuthenticationToken
|
||
2. 接下来,Authentication将会被传递给AuthenticationMananger用于认证
|
||
3. 如果认证失败,那么
|
||
1. SecurityContextHolder将会被清空
|
||
2. RememberMeServices.loginFail方法将会被调用,如果remeberMe没有被调用,那么将不会执行任何操作
|
||
3. AuthenticationFailureHandler将会被调用
|
||
4. 如果认证成功:
|
||
1. 将会通知SessionAuthenticaitonStrategy将会被提示有一个新的登录
|
||
2. Authentication将会被设置到SecurityContextHolder中。如果想要在多个请求之前保存context,需要手动调用`SecurityContextRepository#saveContext`方法,调用后,SecurityContext可以在后续的请求中自动被设置
|
||
3. 调用RememberMeServices.loginSuccess方法,如果没有设置rememberMe,那么不会执行任何操作
|
||
4. ApplicationEventPublisher将会发不InteractiveAuthenticationSuccessEvent事件
|
||
5. 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()
|
||
);
|
||
// ...
|
||
}
|
||
```
|
||
### 基本Authentication
|
||
#### 基本Authentication的认证流程
|
||
1. 用户向私有资源发送未认证请求,其中对私有资源的访问并没有被授权
|
||
2. SpringSecurity的FilterSecurityInterceptor表明该未认证的请求被拒绝访问,抛出AccessDeniedException
|
||
3. 由于该请求没有经过身份认证,故而ExceptionTranslationFilter启动身份认证,被配置好的AuthenticationEntryPoint是一个BasicAuthenticationEntryPoint类的实例,该实例会发送WWW-Authentication的header。RequestCache通常是一个NullRequestCache,不会保存任何的http request请求,因为客户端能够重新发送其原来发送过的请求。
|
||
4. 当客户端获取到WWW-Authentication的header,客户端会知道其接下来会通过username和password重新尝试,重新发送http请求。
|
||
|
||
默认情况下,basic authentication是被开启的。但是,如果有任何基于基于servlet的配置被提供,那么必须通过如下方式显式开启basic authentication。
|
||
```java
|
||
@Bean
|
||
public SecurityFilterChain filterChain(HttpSecurity http) {
|
||
http
|
||
// ...
|
||
.httpBasic(withDefaults());
|
||
return http.build();
|
||
}
|
||
```
|
||
|
||
### Digest Authentication(摘要认证,***不安全***)
|
||
在目前,不应该在现代应用程序中使用Digest Authentication,因为使用摘要认证时必须将password通过纯文本、加密或MD5的格式存储(MD5已经被证实不安全)。相对的,应该使用单向的密码散列(如bCrypt, PBKDF2, SCrypt)来存储认证凭证,但是这些都不被Digest Authentication所支持。
|
||
> 摘要认证主要用来解决Basic Authentication中存在的问题,摘要认证确保了认证凭证在网络上不会以明文的方式传输。
|
||
> 如果想要使用非https的方式并且最大限度的加强认证过程,那么可以考虑使用Digest Authentication。
|
||
|
||
#### 摘要认证中的随机数
|
||
摘要认证中的核心是随机数,该随机数的值由服务端产生,Spring Security中随机数次啊用如下格式:
|
||
```txt
|
||
base64(expirationTime + ":" + md5Hex(expirationTime + ":" + key))
|
||
expirationTime: The date and time when the nonce expires, expressed in milliseconds
|
||
key: A private key to prevent modification of the nonce token
|
||
```
|
||
需要为存储不安全的密码文本配置使用NoOpPasswordEncoder。可以通过如下方式来配置Digest Authentication。
|
||
```java
|
||
@Autowired
|
||
UserDetailsService userDetailsService;
|
||
|
||
DigestAuthenticationEntryPoint entryPoint() {
|
||
DigestAuthenticationEntryPoint result = new DigestAuthenticationEntryPoint();
|
||
result.setRealmName("My App Relam");
|
||
result.setKey("3028472b-da34-4501-bfd8-a355c42bdf92");
|
||
}
|
||
|
||
DigestAuthenticationFilter digestAuthenticationFilter() {
|
||
DigestAuthenticationFilter result = new DigestAuthenticationFilter();
|
||
result.setUserDetailsService(userDetailsService);
|
||
result.setAuthenticationEntryPoint(entryPoint());
|
||
}
|
||
|
||
@Bean
|
||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||
http
|
||
// ...
|
||
.exceptionHandling(e -> e.authenticationEntryPoint(authenticationEntryPoint()))
|
||
.addFilterBefore(digestFilter());
|
||
return http.build();
|
||
}
|
||
```
|
||
## 密码存储方式
|
||
### 内存中存储密码
|
||
Spring Security中InMemoryUserDetailsManager实现了UserDetailsService,用于向基于存储在内存中的密码认证提供支持。
|
||
InMemoryUserDetailsManager通过实现UserDetailsManager接口来提供对UserDetails的管理。基于UserDetails的认证主要用来接受基于用户名/密码的认证。
|
||
InMemoryUserDetailsManager可以通过如下方式进行配置:
|
||
```java
|
||
@Bean
|
||
public UserDetailsService users() {
|
||
UserDetails user = User.builder()
|
||
.username("user")
|
||
.password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
|
||
.roles("USER")
|
||
.build();
|
||
UserDetails admin = User.builder()
|
||
.username("admin")
|
||
.password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
|
||
.roles("USER", "ADMIN")
|
||
.build();
|
||
return new InMemoryUserDetailsManager(user, admin);
|
||
}
|
||
```
|
||
#### 内存中存储密码时使用defaultPasswordEncoder
|
||
***通过defaultPasswordEncoder来指定密码编码器时,无法防止通过反编译字节码来获取密码的攻击。***
|
||
```java
|
||
@Bean
|
||
public UserDetailsService users() {
|
||
// The builder will ensure the passwords are encoded before saving in memory
|
||
UserBuilder users = User.withDefaultPasswordEncoder();
|
||
UserDetails user = users
|
||
.username("user")
|
||
.password("password")
|
||
.roles("USER")
|
||
.build();
|
||
UserDetails admin = users
|
||
.username("admin")
|
||
.password("password")
|
||
.roles("USER", "ADMIN")
|
||
.build();
|
||
return new InMemoryUserDetailsManager(user, admin);
|
||
}
|
||
```
|
||
|
||
### JDBC Authentication
|
||
Spring Security的JdbcDaoImpl实现了UserDetailsService来基于username/password的认证提供从jdbc获取密码的支持。JdbcUserDetailsManager继承了JdbcDaoImpl来通过DetailsManager的接口提供对UserDetails的管理。
|
||
Spring Security为基于jdbc的认证提供了默认的查询语句。
|
||
#### User Schema
|
||
JdbcDaoImpl需要数据表来导入密码、账户状态和用户的一系列权限。JdbcDaoImpl默认需要的schema如下:
|
||
```sql
|
||
# 创建用户表和权限表,并且将用户表和权限表之间用外键关联
|
||
# 用户表需要提供username、password、用户状态
|
||
# 权限表需要提供用户名和权限名称
|
||
create table users(
|
||
username varchar_ignorecase(50) not null primary key,
|
||
password varchar_ignorecase(500) not null,
|
||
enabled boolean not null
|
||
);
|
||
|
||
create table authorities (
|
||
username varchar_ignorecase(50) not null,
|
||
authority varchar_ignorecase(50) not null,
|
||
constraint fk_authorities_users foreign key(username) references users(username)
|
||
);
|
||
create unique index ix_auth_username on authorities (username,authority);
|
||
```
|
||
#### Group Schema
|
||
如果你的程序中使用了Group,那么还额外需要一张group的表,默认如下:
|
||
```sql
|
||
# 如果要为group配置权限,需要引入三张表,group表,权限表和group_member表
|
||
create table groups (
|
||
id bigint auto_increment primary key,
|
||
group_name varchar_ignorecase(50) not null
|
||
);
|
||
|
||
create table group_authorities (
|
||
group_id bigint not null,
|
||
authority varchar(50) not null,
|
||
constraint fk_group_authorities_group foreign key(group_id) references groups(id)
|
||
);
|
||
|
||
create table group_members (
|
||
id bigint auto_increment primary key,
|
||
username varchar(50) not null,
|
||
group_id bigint not null,
|
||
constraint fk_group_members_group foreign key(group_id) references groups(id)
|
||
);
|
||
```
|
||
|
||
#### 配置Datasource
|
||
```java
|
||
// 生产环境时,应该通过对外部数据库的连接来建立数据源
|
||
@Bean
|
||
DataSource dataSource() {
|
||
return new EmbeddedDatabaseBuilder()
|
||
.setType(H2)
|
||
.addScript(JdbcDaoImpl.DEFAULT_USER_SCHEMA_DDL_LOCATION)
|
||
.build();
|
||
}
|
||
```
|
||
#### 创建JdbcUserDetailsManager Bean对象
|
||
```java
|
||
@Bean
|
||
UserDetailsManager users(DataSource dataSource) {
|
||
UserDetails user = User.builder()
|
||
.username("user")
|
||
.password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
|
||
.roles("USER")
|
||
.build();
|
||
UserDetails admin = User.builder()
|
||
.username("admin")
|
||
.password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
|
||
.roles("USER", "ADMIN")
|
||
.build();
|
||
JdbcUserDetailsManager users = new JdbcUserDetailsManager(dataSource);
|
||
users.createUser(user);
|
||
users.createUser(admin);
|
||
return users;
|
||
}
|
||
```
|
||
### UserDetails
|
||
UserDetails是通过UserDetailsService返回的。DaoAuthenticationProvider对UserrDetails进行验证并且返回Authentication.
|
||
|
||
### UserDetailsService
|
||
UserDetailsService被DaoAuthenticationProvider调用,用来获取username、password和其他随着password/username一起认证的信息。对于UserDetailsService,Spring Security提供了in-memory和jdbc两种实现形式。
|
||
可以通过自定义UserDetailsService类bean对象的方式来自定义认证过程。
|
||
```java
|
||
// 自定义UserDetailsService的bean对象
|
||
@Bean
|
||
CustomUserDetailsService customUserDetailsService() {
|
||
return new CustomUserDetailsService();
|
||
}
|
||
```
|
||
|
||
### PasswordEncoder
|
||
Spring Security支持PasswordEncoder来安全的存储密码。可以通过自定义PasswordEncoder类的bean对象的形式来自定义Spring Security安全存储密码的过程。
|
||
|
||
### DaoAuthenticationProvider
|
||
DaoAuthenticationProvider是AuthenticationProvider的一个实现类,通过调用UserDetailsService和PasswordEncoder来认证用户名和密码。
|
||
Spring Security中DaoAuthenticationProvider的工作流程:
|
||
1. authentication filter会读取username和password并且将其封装到UsernamePasswordAuthenticationToken中传递给AuthenticationManager,ProviderManager实现了AuthenticationManager
|
||
2. ProviderManager被配置为使用DaoAuthenticationProvider
|
||
3. DaoAuthenticationProvider通过UserDetailsService来查找UserDetails
|
||
4. DaoAuthenticationProvider通过PasswordEncoder来验证UserDetails中的密码
|
||
5. 当验证成功时,会返回UsernamePasswordAuthenticationToken类型的Authentication,并且返回的Authentication拥有一个主体为UserDetailsService返回的UserDetails
|
||
6. 返回的UsernamePasswordAuthenticationToken会在SecurityContextHolder中保存
|
||
|