diff --git a/spring/Spring Security/Spring Security.md b/spring/Spring Security/Spring Security.md index 375f77a..fadb720 100644 --- a/spring/Spring Security/Spring Security.md +++ b/spring/Spring Security/Spring Security.md @@ -20,26 +20,24 @@ - [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) + - [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 @@ -306,34 +304,205 @@ AbstractAuthenticationProcessingFilter通过如下流程来执行认证: 4. ApplicationEventPublisher将会发不InteractiveAuthenticationSuccessEvent事件 5. AuthenticationSuccessHandler将会被调用 -## 用户名/密码认证 -### Form Login -Spring Security为以表单形式提供的用户名和密码认证提供支持。 -#### 用户被重定向到登录页面的过程 -1. 用户发送了一个没有经过身份认证的请求到指定资源,并且待请求的资源对该用户来说是未授权的 -2. Spring Security中FilterSecurityInterceptor抛出AccessDeniedException,代表该未授权的请求被拒绝 -3. 因为该用户没有经过认证,故而ExceptionTransactionFilter发起了开始认证的过程,并且使用配置好的AuthenticationEntryPoint向登录页面发起了重定向。在大多数情况下AuthenticationEntryPoint都是LoginUrlAuthenticationEntryPoint -4. 浏览器接下来会请求重定向到的登陆页面 +## Username/Password Authentication +认证用户时最广泛使用的认证方式为用户名/密码认证。Spring Security对基于用户名和密码的认证提供了全面的支持。 -当用户名和密码提交后,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()); - // ... +@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); + } + } ``` -如果想要自定义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 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 @@ -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方法确保了所有用户对loginUrl,loginProcessingUrl,failureUrl都拥有访问权限。 + +### 从请求中读取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 -@Bean public SecurityFilterChain filterChain(HttpSecurity http) { http - // ... - .httpBasic(withDefaults()); - return http.build(); + .formLogin(withDefaults()); + // ... } ``` -### Digest Authentication(摘要认证,***不安全***) -在目前,不应该在现代应用程序中使用Digest Authentication,因为使用摘要认证时必须将password通过纯文本、加密或MD5的格式存储(MD5已经被证实不安全)。相对的,应该使用单向的密码散列(如bCrypt, PBKDF2, SCrypt)来存储认证凭证,但是这些都不被Digest Authentication所支持。 -> 摘要认证主要用来解决Basic Authentication中存在的问题,摘要认证确保了认证凭证在网络上不会以明文的方式传输。 -> 如果想要使用非https的方式并且最大限度的加强认证过程,那么可以考虑使用Digest Authentication。 +#### 非/login请求时,UsernamePassword并不执行认证校验逻辑 +> 当未认证用户访问受保护资源时,AuthorizationFilter会抛出AccessDenied异常,导致重定向到登录页面。 +> +> UsernamePasswordAuthenticationFilter当请求url并不为/login时,并不执行认证逻辑,而是交由后续AuthorizationFilter来校验当前用户是否由访问资源的权限。 +> +> UsernamePasswordAuthenticationFilter只是在用户发送/login请求执行登录时,才通过authenticationManager来执行认证逻辑。 -#### 摘要认证中的随机数 -摘要认证中的核心是随机数,该随机数的值由服务端产生,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 +#### HttpBasic +在使用httpBasic时,如果未经认证的用户请求受保护资源,会返回`WWW-Authenticate`的header,此时客户端应该在下次提交时在请求中添加用户名和密码。 + +在使用httpBasic时,应该在请求的`Authorization`header中添加username和password的内容,形式如下: +```http +Authorization: Basic ``` -需要为存储不安全的密码文本配置使用NoOpPasswordEncoder。可以通过如下方式来配置Digest Authentication。 -```java -@Autowired -UserDetailsService userDetailsService; +其中``应该通过base64进行加密 -DigestAuthenticationEntryPoint entryPoint() { - DigestAuthenticationEntryPoint result = new DigestAuthenticationEntryPoint(); - result.setRealmName("My App Relam"); - result.setKey("3028472b-da34-4501-bfd8-a355c42bdf92"); -} +#### Digest +目前应用中不应使用Digest。 -DigestAuthenticationFilter digestAuthenticationFilter() { - DigestAuthenticationFilter result = new DigestAuthenticationFilter(); - result.setUserDetailsService(userDetailsService); - result.setAuthenticationEntryPoint(entryPoint()); -} +### password storage -@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可以通过如下方式进行配置: +#### in-memory authentication +`InMemoryUserDetailsManager`实现了`UserDetailsService`,其将用户名和密码存储在内存中,并且提供了基于username和password的认证。 + +使用实例如下所示: ```java @Bean public UserDetailsService users() { @@ -421,8 +587,7 @@ public UserDetailsService users() { return new InMemoryUserDetailsManager(user, admin); } ``` -#### 内存中存储密码时使用defaultPasswordEncoder -***通过defaultPasswordEncoder来指定密码编码器时,无法防止通过反编译字节码来获取密码的攻击。*** +在上述示例中,需要手动对密码进行散列和编码处理,可以通过`User.withDefaultPasswordEncoder`来自动对传入密码进行编码处理: ```java @Bean public UserDetailsService users() { @@ -441,107 +606,81 @@ public UserDetailsService users() { return new InMemoryUserDetailsManager(user, admin); } ``` +#### jdbc authentication +`JdbcDaoImpl`实现了`UserDetailsService`,支持基于用户名和密码的认证,并且将密码存储在数据库中。 -### 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 -); +#### UserDetails +`UserDetails`由`UserDetailsService`返回,`DaoAuthenticationProvider`针对UserDetails及逆行校验,并且返回`Authentication`。Authentication是基于UserDetails进行构建的。 -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 -); +#### UserDetailsService +DaoAuthenticationProvider通过UserDetailsService来获取UserDetails信息,UserDetails信息中包含用户名、密码和用户其他信息。 -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 +可以通过自定义UserDetailsService的bean对象来自定义UserDetails的获取逻辑: ```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安全存储密码的过程。 +### 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 +``` +其中,第四部分通过(username,expirationTime,password,key),以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中传递给AuthenticationManager,ProviderManager实现了AuthenticationManager -2. ProviderManager被配置为使用DaoAuthenticationProvider -3. DaoAuthenticationProvider通过UserDetailsService来查找UserDetails -4. DaoAuthenticationProvider通过PasswordEncoder来验证UserDetails中的密码 -5. 当验证成功时,会返回UsernamePasswordAuthenticationToken类型的Authentication,并且返回的Authentication拥有一个主体为UserDetailsService返回的UserDetails -6. 返回的UsernamePasswordAuthenticationToken会在SecurityContextHolder中保存