Files
rikako-note/spring/Spring Security/Spring Security.md
2024-03-17 20:37:24 +08:00

737 lines
34 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

- [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)
- [Username/Password Authentication](#usernamepassword-authentication)
- [发布AuthenticationManager bean](#发布authenticationmanager-bean)
- [自定义AuthenticationManager](#自定义authenticationmanager)
- [从请求中读取Username/Password](#从请求中读取usernamepassword)
- [Form](#form)
- [非/login请求时UsernamePassword并不执行认证校验逻辑](#非login请求时usernamepassword并不执行认证校验逻辑)
- [HttpBasic](#httpbasic)
- [Digest](#digest)
- [password storage](#password-storage)
- [in-memory authentication](#in-memory-authentication)
- [jdbc authentication](#jdbc-authentication)
- [UserDetails](#userdetails)
- [UserDetailsService](#userdetailsservice)
- [RememberMe](#rememberme)
- [hash based token](#hash-based-token)
- [将token持久化到数据库中](#将token持久化到数据库中)
- [RememberMe的接口及其实现](#rememberme的接口及其实现)
- [TokenBasedRememberMeService](#tokenbasedremembermeservice)
# 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会通过BCtyptHash解密算法来对密码的存储进行保护
## 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支持FilterChainProxyFilterChainProxy是一个由Spring Security提供的特殊FilterFilterChainProxy通过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中包含有SecurityContextHolderSecurityContextHolder负责将当前线程和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将会被调用
## Username/Password Authentication
认证用户时最广泛使用的认证方式为用户名/密码认证。Spring Security对基于用户名和密码的认证提供了全面的支持。
可以通过如下方式来配置用户名和密码认证:
```java
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().authenticated()
)
.httpBasic(Customizer.withDefaults())
.formLogin(Customizer.withDefaults());
return http.build();
}
@Bean
public UserDetailsService userDetailsService() {
UserDetails userDetails = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("USER")
.build();
return new InMemoryUserDetailsManager(userDetails);
}
}
```
上述示例向SecurityFilterChain中自动注册了InMemoryUserDetailsService并且向默认的AuthenticationManager中注册了DaoUserAuthenticationProvider并且启用了FormLogin和HttpBasic的认证。
### 发布AuthenticationManager bean
当想要自定义认证过程时可以将AuthenticationManager发布为bean对象示例如下
```java
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/login").permitAll()
.anyRequest().authenticated()
);
return http.build();
}
@Bean
public AuthenticationManager authenticationManager(
UserDetailsService userDetailsService,
PasswordEncoder passwordEncoder) {
DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
authenticationProvider.setUserDetailsService(userDetailsService);
authenticationProvider.setPasswordEncoder(passwordEncoder);
return new ProviderManager(authenticationProvider);
}
@Bean
public UserDetailsService userDetailsService() {
UserDetails userDetails = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("USER")
.build();
return new InMemoryUserDetailsManager(userDetails);
}
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
}
```
> 在上述示例中没有指定formLogin或是httpBasic来执行身份认证并且针对login请求其权限校验为permitAll故而login请求的实际身份认证工作由自定义的loginController来执行
当添加了上述Configuration类后可以指定自定义的Controller类
```java
@RestController
public class LoginController {
private final AuthenticationManager authenticationManager;
public LoginController(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
@PostMapping("/login")
public ResponseEntity<Void> login(@RequestBody LoginRequest loginRequest) {
Authentication authenticationRequest =
UsernamePasswordAuthenticationToken.unauthenticated(loginRequest.username(), loginRequest.password());
Authentication authenticationResponse =
this.authenticationManager.authenticate(authenticationRequest);
// ...
}
public record LoginRequest(String username, String password) {
}
}
```
### 自定义AuthenticationManager
默认情况下SpringSecurity会在内部构建一个AuthenticationManager该AuthenticationManager由DaoAuthenticationProvider组成用于对username/password进行认证。
为了实现AuthenticationMananger的自定义可以发布一个AuthenticationManager bean对象spring security会使用发布的bean对象。
```java
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/login").permitAll()
.anyRequest().authenticated()
)
.httpBasic(Customizer.withDefaults())
.formLogin(Customizer.withDefaults());
return http.build();
}
@Bean
public AuthenticationManager authenticationManager(
UserDetailsService userDetailsService,
PasswordEncoder passwordEncoder) {
DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
authenticationProvider.setUserDetailsService(userDetailsService);
authenticationProvider.setPasswordEncoder(passwordEncoder);
ProviderManager providerManager = new ProviderManager(authenticationProvider);
providerManager.setEraseCredentialsAfterAuthentication(false);
// provider为最常用的AuthenticationManager的实现类
return providerManager;
}
@Bean
public UserDetailsService userDetailsService() {
UserDetails userDetails = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("USER")
.build();
return new InMemoryUserDetailsManager(userDetails);
}
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
}
```
除了发布自定义的AuthenticaitonMananger之外还可以通过修改AuthenticationManagerBuilder bean对象来实现。
AuthenticationManagerBuilder被发布为bean对象并且用于构建Spring Security的全局AuthenticationManager可以按如下方式来自定义
```java
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// ...
return http.build();
}
@Bean
public UserDetailsService userDetailsService() {
// Return a UserDetailsService that caches users
// ...
}
@Autowired
public void configure(AuthenticationManagerBuilder builder) {
builder.eraseCredentials(false);
}
}
```
如果想要自定义登录页,可以通过如下方式:
```java
public SecurityFilterChain filterChain(HttpSecurity http) {
http
.formLogin(form -> form
.loginPage("/login")
.permitAll()
);
// ...
}
```
并且在使用spring mvc时需要将/login和页面模板映射
```java
@Controller
class LoginController {
@GetMapping("/login")
String login() {
return "login";
}
}
```
> 上述permitAll方法确保了所有用户对loginUrlloginProcessingUrlfailureUrl都拥有访问权限。
### 从请求中读取Username/Password
Spring Security提供如下方式来从请求中读取Username和password
- Form
- Basic
- Digest
#### Form
在http用户名和密码被作为http form的形式x-www-form-urlencoded被传递时Spring Security支持获取form形式的username和password。
默认情况下Spring Security form login被开启但是如果在应用手动配置了SecurityFilterChain那么必须显式调用`formLogin`来指定。
```java
public SecurityFilterChain filterChain(HttpSecurity http) {
http
.formLogin(withDefaults());
// ...
}
```
#### 非/login请求时UsernamePassword并不执行认证校验逻辑
> 当未认证用户访问受保护资源时AuthorizationFilter会抛出AccessDenied异常导致重定向到登录页面。
>
> UsernamePasswordAuthenticationFilter当请求url并不为/login时并不执行认证逻辑而是交由后续AuthorizationFilter来校验当前用户是否由访问资源的权限。
>
> UsernamePasswordAuthenticationFilter只是在用户发送/login请求执行登录时才通过authenticationManager来执行认证逻辑。
#### HttpBasic
在使用httpBasic时如果未经认证的用户请求受保护资源会返回`WWW-Authenticate`的header此时客户端应该在下次提交时在请求中添加用户名和密码。
在使用httpBasic时应该在请求的`Authorization`header中添加username和password的内容形式如下
```http
Authorization: Basic <credentials>
```
其中`<credentials>`应该通过base64进行加密
#### Digest
目前应用中不应使用Digest。
### password storage
#### in-memory authentication
`InMemoryUserDetailsManager`实现了`UserDetailsService`其将用户名和密码存储在内存中并且提供了基于username和password的认证。
使用实例如下所示:
```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);
}
```
在上述示例中,需要手动对密码进行散列和编码处理,可以通过`User.withDefaultPasswordEncoder`来自动对传入密码进行编码处理:
```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
`JdbcDaoImpl`实现了`UserDetailsService`,支持基于用户名和密码的认证,并且将密码存储在数据库中。
#### UserDetails
`UserDetails``UserDetailsService`返回,`DaoAuthenticationProvider`针对UserDetails及逆行校验并且返回`Authentication`。Authentication是基于UserDetails进行构建的。
#### UserDetailsService
DaoAuthenticationProvider通过UserDetailsService来获取UserDetails信息UserDetails信息中包含用户名、密码和用户其他信息。
可以通过自定义UserDetailsService的bean对象来自定义UserDetails的获取逻辑
```java
@Bean
CustomUserDetailsService customUserDetailsService() {
return new CustomUserDetailsService();
}
```
### RememberMe
rememberMe要求在多个session之间应用能够记住用户的身份。其通常是由cookie实现的如果指定cookie在后续请求中被检测到那么会进行自动登录。
Spring Security提供了两种Remember Me实现一种是基于hash来保存token另一种则是通过数据库来保存token。
#### hash based token
该方案通过hash来实现有效的rememberMe策略在认证成功后如下cookie会被发送给客户端
```http
base64(username + ":" + expirationTime + ":" + algorithmName + ":"
+algorithmHex(username + ":" + expirationTime + ":" password + ":" + key))
username: As identifiable to the UserDetailsService
password: That matches the one in the retrieved UserDetails
expirationTime: The date and time when the remember-me token expires, expressed in milliseconds
key: A private key to prevent modification of the remember-me token
algorithmName: The algorithm used to generate and to verify the remember-me token signature
```
其中第四部分通过usernameexpirationTimepasswordkey以algorithmName算法生成。
> 当该cookie重新被发送到服务端时服务端会取出cookie的前三部分并通过UserDetailService查询出password该password为获取的UserDetails中的password为编码后的密码按照cookie中指定的算法结合存储在服务端的private key重新计算出第四部分并且将计算结果和第四部分进行比较。
>
> 该算法可以保证cookie并没有被篡改因为password和private key都存储在服务端如果手动修改username或是过期时间那么都会导致服务端计算的第四部分结果不匹配。
但是使用该hash-based方法时存在安全隐患因为所有捕获到该token的agent都可以使用该token访问用户资源直到token过期。
在用户意识到token已经被泄露时可以通过修改密码来使该token无效因为修改密码后第四部分的计算结果会发生变化导致旧token失效。
如果想要开启rememberMe可以通过如下方式开启
```java
http
.authorizeHttpRequests(authorize -> {
authorize.requestMatchers("/login").permitAll()
.anyRequest().authenticated();
})
.formLogin(Customizer.withDefaults())
.httpBasic(Customizer.withDefaults())
.rememberMe(Customizer.withDefaults());
return http.build();
```
#### 将token持久化到数据库中
为了在多次session之间记住用户的身份信息可以通过将token存储到数据库中从而记住用户的身份。
#### RememberMe的接口及其实现
remember-me将会由`UsernamePasswordAuthenticationFilter`来使用与rememberMe相关的hook位于`RememberMeServices`接口中并且将会在适当的时机被调用。如下展示了rememberMe相关的接口定义
```java
Authentication autoLogin(HttpServletRequest request, HttpServletResponse response);
void loginFail(HttpServletRequest request, HttpServletResponse response);
void loginSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication successfulAuthentication);
```
`AbstractAuthenticationProcessingFilter`只会调用loginSuccess或loginFail方法`autoLogin`方法将会由`RememberMeAuthenticationFilter`进行调用,当`SecurityContextHolder`不包含`Authentication`对象时就会调用autoLogin方法。
#### TokenBasedRememberMeService
TokenBasedRemeberMeService支持上述基于hash的rememberMe方法。`TokenBasedRememberMeServices`会生成`RememberMeAuthenticationToken`该token将会由`RememberMeAuthenticationProvider`进行验证。
为了`TokenBasedRememberMeServices`生成的token必须能被`RememberMeAuthenticationToken`正确的校验,必须相同的`key`必须能够在两者之间进行共享。
默认情况下TokenBasedRememberMeService使用`SHA-256`来对token进行编码。如果想要针对`TokenBasedRememberMeServices`进行自定义可以发布自己的bean
```java
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http, RememberMeServices rememberMeServices) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().authenticated()
)
.rememberMe((remember) -> remember
.rememberMeServices(rememberMeServices)
);
return http.build();
}
@Bean
RememberMeServices rememberMeServices(UserDetailsService userDetailsService) {
RememberMeTokenAlgorithm encodingAlgorithm = RememberMeTokenAlgorithm.SHA256;
TokenBasedRememberMeServices rememberMe = new TokenBasedRememberMeServices(myKey, userDetailsService, encodingAlgorithm);
// 修改编码算法
rememberMe.setMatchingAlgorithm(RememberMeTokenAlgorithm.MD5);
return rememberMe;
}
```
为了启用rememberMe需要在应用上下文中包含如下bean
```java
@Bean
RememberMeAuthenticationFilter rememberMeFilter() {
RememberMeAuthenticationFilter rememberMeFilter = new RememberMeAuthenticationFilter();
rememberMeFilter.setRememberMeServices(rememberMeServices());
rememberMeFilter.setAuthenticationManager(theAuthenticationManager);
return rememberMeFilter;
}
@Bean
TokenBasedRememberMeServices rememberMeServices() {
TokenBasedRememberMeServices rememberMeServices = new TokenBasedRememberMeServices();
rememberMeServices.setUserDetailsService(myUserDetailsService);
rememberMeServices.setKey("springRocks");
return rememberMeServices;
}
@Bean
RememberMeAuthenticationProvider rememberMeAuthenticationProvider() {
RememberMeAuthenticationProvider rememberMeAuthenticationProvider = new RememberMeAuthenticationProvider();
rememberMeAuthenticationProvider.setKey("springRocks");
return rememberMeAuthenticationProvider;
}
```