阅读spring security文档

This commit is contained in:
asahi
2024-03-17 20:22:24 +08:00
parent 35d39bcea0
commit 553931005d

View File

@@ -20,26 +20,24 @@
- [AuthenticationProvider](#authenticationprovider) - [AuthenticationProvider](#authenticationprovider)
- [AuthenticationEntryPoint](#authenticationentrypoint) - [AuthenticationEntryPoint](#authenticationentrypoint)
- [AbstractAuthenticationProcessingFilter](#abstractauthenticationprocessingfilter) - [AbstractAuthenticationProcessingFilter](#abstractauthenticationprocessingfilter)
- [用户名/密码认证](#用户名密码认证) - [Username/Password Authentication](#usernamepassword-authentication)
- [Form Login](#form-login) - [发布AuthenticationManager bean](#发布authenticationmanager-bean)
- [用户被重定向到登录页面的过程](#用户被重定向到登录页面的过程) - [自定义AuthenticationManager](#自定义authenticationmanager)
- [认证用户名和密码过程](#认证用户名和密码过程) - [从请求中读取Username/Password](#从请求中读取usernamepassword)
- [基本Authentication](#基本authentication) - [Form](#form)
- [基本Authentication的认证流程](#基本authentication的认证流程) - [非/login请求时UsernamePassword并不执行认证校验逻辑](#非login请求时usernamepassword并不执行认证校验逻辑)
- [Digest Authentication(摘要认证,***不安全***)](#digest-authentication摘要认证不安全) - [HttpBasic](#httpbasic)
- [摘要认证中的随机数](#摘要认证中的随机数) - [Digest](#digest)
- [密码存储方式](#密码存储方式) - [password storage](#password-storage)
- [内存中存储密码](#内存中存储密码) - [in-memory authentication](#in-memory-authentication)
- [内存中存储密码时使用defaultPasswordEncoder](#内存中存储密码时使用defaultpasswordencoder) - [jdbc authentication](#jdbc-authentication)
- [JDBC Authentication](#jdbc-authentication)
- [User Schema](#user--schema)
- [Group Schema](#group-schema)
- [配置Datasource](#配置datasource)
- [创建JdbcUserDetailsManager Bean对象](#创建jdbcuserdetailsmanager-bean对象)
- [UserDetails](#userdetails) - [UserDetails](#userdetails)
- [UserDetailsService](#userdetailsservice) - [UserDetailsService](#userdetailsservice)
- [PasswordEncoder](#passwordencoder) - [RememberMe](#rememberme)
- [DaoAuthenticationProvider](#daoauthenticationprovider) - [hash based token](#hash-based-token)
- [将token持久化到数据库中](#将token持久化到数据库中)
- [RememberMe的接口及其实现](#rememberme的接口及其实现)
- [TokenBasedRememberMeService](#tokenbasedremembermeservice)
# Spring Security # Spring Security
@@ -306,34 +304,205 @@ AbstractAuthenticationProcessingFilter通过如下流程来执行认证
4. ApplicationEventPublisher将会发不InteractiveAuthenticationSuccessEvent事件 4. ApplicationEventPublisher将会发不InteractiveAuthenticationSuccessEvent事件
5. AuthenticationSuccessHandler将会被调用 5. AuthenticationSuccessHandler将会被调用
## 用户名/密码认证 ## Username/Password Authentication
### Form Login 认证用户时最广泛使用的认证方式为用户名/密码认证。Spring Security对基于用户名和密码的认证提供了全面的支持。
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 ```java
// 显式指定form login的配置 @Configuration
@Bean @EnableWebSecurity
public SecurityFilterChain filterChain(HttpSecurity http) { public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http http
.formLogin(withDefaults()); .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);
}
} }
``` ```
如果想要自定义login form page可以使用如下配置 上述示例向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 ```java
public SecurityFilterChain filterChain(HttpSecurity http) { public SecurityFilterChain filterChain(HttpSecurity http) {
http http
@@ -344,67 +513,64 @@ public SecurityFilterChain filterChain(HttpSecurity http) {
// ... // ...
} }
``` ```
### 基本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。 并且在使用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 ```java
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) { public SecurityFilterChain filterChain(HttpSecurity http) {
http http
.formLogin(withDefaults());
// ... // ...
.httpBasic(withDefaults());
return http.build();
} }
``` ```
### Digest Authentication(摘要认证,***不安全***) #### 非/login请求时UsernamePassword并不执行认证校验逻辑
在目前不应该在现代应用程序中使用Digest Authentication因为使用摘要认证时必须将password通过纯文本、加密或MD5的格式存储MD5已经被证实不安全。相对的应该使用单向的密码散列如bCrypt, PBKDF2, SCrypt来存储认证凭证但是这些都不被Digest Authentication所支持 > 当未认证用户访问受保护资源时AuthorizationFilter会抛出AccessDenied异常导致重定向到登录页面
> 摘要认证主要用来解决Basic Authentication中存在的问题摘要认证确保了认证凭证在网络上不会以明文的方式传输。 >
> 如果想要使用非https的方式并且最大限度的加强认证过程那么可以考虑使用Digest Authentication > UsernamePasswordAuthenticationFilter当请求url并不为/login时并不执行认证逻辑而是交由后续AuthorizationFilter来校验当前用户是否由访问资源的权限
>
> UsernamePasswordAuthenticationFilter只是在用户发送/login请求执行登录时才通过authenticationManager来执行认证逻辑。
#### 摘要认证中的随机数 #### HttpBasic
摘要认证中的核心是随机数该随机数的值由服务端产生Spring Security中随机数次啊用如下格式 在使用httpBasic时如果未经认证的用户请求受保护资源会返回`WWW-Authenticate`的header此时客户端应该在下次提交时在请求中添加用户名和密码。
```txt
base64(expirationTime + ":" + md5Hex(expirationTime + ":" + key)) 在使用httpBasic时应该在请求的`Authorization`header中添加username和password的内容形式如下
expirationTime: The date and time when the nonce expires, expressed in milliseconds ```http
key: A private key to prevent modification of the nonce token Authorization: Basic <credentials>
``` ```
需要为存储不安全的密码文本配置使用NoOpPasswordEncoder。可以通过如下方式来配置Digest Authentication。 其中`<credentials>`应该通过base64进行加密
```java
@Autowired
UserDetailsService userDetailsService;
DigestAuthenticationEntryPoint entryPoint() { #### Digest
DigestAuthenticationEntryPoint result = new DigestAuthenticationEntryPoint(); 目前应用中不应使用Digest。
result.setRealmName("My App Relam");
result.setKey("3028472b-da34-4501-bfd8-a355c42bdf92");
}
DigestAuthenticationFilter digestAuthenticationFilter() { ### password storage
DigestAuthenticationFilter result = new DigestAuthenticationFilter();
result.setUserDetailsService(userDetailsService);
result.setAuthenticationEntryPoint(entryPoint());
}
@Bean #### in-memory authentication
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { `InMemoryUserDetailsManager`实现了`UserDetailsService`其将用户名和密码存储在内存中并且提供了基于username和password的认证。
http
// ... 使用实例如下所示:
.exceptionHandling(e -> e.authenticationEntryPoint(authenticationEntryPoint()))
.addFilterBefore(digestFilter());
return http.build();
}
```
## 密码存储方式
### 内存中存储密码
Spring Security中InMemoryUserDetailsManager实现了UserDetailsService用于向基于存储在内存中的密码认证提供支持。
InMemoryUserDetailsManager通过实现UserDetailsManager接口来提供对UserDetails的管理。基于UserDetails的认证主要用来接受基于用户名/密码的认证。
InMemoryUserDetailsManager可以通过如下方式进行配置
```java ```java
@Bean @Bean
public UserDetailsService users() { public UserDetailsService users() {
@@ -421,8 +587,7 @@ public UserDetailsService users() {
return new InMemoryUserDetailsManager(user, admin); return new InMemoryUserDetailsManager(user, admin);
} }
``` ```
#### 内存中存储密码时使用defaultPasswordEncoder 在上述示例中,需要手动对密码进行散列和编码处理,可以通过`User.withDefaultPasswordEncoder`来自动对传入密码进行编码处理:
***通过defaultPasswordEncoder来指定密码编码器时无法防止通过反编译字节码来获取密码的攻击。***
```java ```java
@Bean @Bean
public UserDetailsService users() { public UserDetailsService users() {
@@ -441,107 +606,81 @@ public UserDetailsService users() {
return new InMemoryUserDetailsManager(user, admin); return new InMemoryUserDetailsManager(user, admin);
} }
``` ```
#### jdbc authentication
`JdbcDaoImpl`实现了`UserDetailsService`,支持基于用户名和密码的认证,并且将密码存储在数据库中。
### JDBC Authentication #### UserDetails
Spring Security的JdbcDaoImpl实现了UserDetailsService来基于username/password的认证提供从jdbc获取密码的支持。JdbcUserDetailsManager继承了JdbcDaoImpl来通过DetailsManager的接口提供对UserDetails的管理。 `UserDetails``UserDetailsService`返回,`DaoAuthenticationProvider`针对UserDetails及逆行校验并且返回`Authentication`。Authentication是基于UserDetails进行构建的。
Spring Security为基于jdbc的认证提供了默认的查询语句。
#### User Schema
JdbcDaoImpl需要数据表来导入密码、账户状态和用户的一系列权限。JdbcDaoImpl默认需要的schema如下
```sql
# 创建用户表和权限表,并且将用户表和权限表之间用外键关联
# 用户表需要提供usernamepassword、用户状态
# 权限表需要提供用户名和权限名称
create table users(
username varchar_ignorecase(50) not null primary key,
password varchar_ignorecase(500) not null,
enabled boolean not null
);
create table authorities ( #### UserDetailsService
username varchar_ignorecase(50) not null, DaoAuthenticationProvider通过UserDetailsService来获取UserDetails信息UserDetails信息中包含用户名、密码和用户其他信息。
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 ( 可以通过自定义UserDetailsService的bean对象来自定义UserDetails的获取逻辑
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 ```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一起认证的信息。对于UserDetailsServiceSpring Security提供了in-memory和jdbc两种实现形式。
可以通过自定义UserDetailsService类bean对象的方式来自定义认证过程。
```java
// 自定义UserDetailsService的bean对象
@Bean @Bean
CustomUserDetailsService customUserDetailsService() { CustomUserDetailsService customUserDetailsService() {
return new CustomUserDetailsService(); return new CustomUserDetailsService();
} }
``` ```
### PasswordEncoder ### RememberMe
Spring Security支持PasswordEncoder来安全的存储密码。可以通过自定义PasswordEncoder类的bean对象的形式来自定义Spring Security安全存储密码的过程 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`必须能够在两者之间进行共享。
### DaoAuthenticationProvider
DaoAuthenticationProvider是AuthenticationProvider的一个实现类通过调用UserDetailsService和PasswordEncoder来认证用户名和密码。
Spring Security中DaoAuthenticationProvider的工作流程
1. authentication filter会读取username和password并且将其封装到UsernamePasswordAuthenticationToken中传递给AuthenticationManagerProviderManager实现了AuthenticationManager
2. ProviderManager被配置为使用DaoAuthenticationProvider
3. DaoAuthenticationProvider通过UserDetailsService来查找UserDetails
4. DaoAuthenticationProvider通过PasswordEncoder来验证UserDetails中的密码
5. 当验证成功时会返回UsernamePasswordAuthenticationToken类型的Authentication并且返回的Authentication拥有一个主体为UserDetailsService返回的UserDetails
6. 返回的UsernamePasswordAuthenticationToken会在SecurityContextHolder中保存