Files
rikako-note/spring/Spring Security/Spring Security.md
2024-03-15 23:14:26 +08:00

30 KiB
Raw Blame History

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方法

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会在认证后校验用户对于请求的租户是否拥有权限

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中添加代码如下所示例

@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将AccessDeniedExceptionAuthenticationException翻译为http response。

ExceptionTranslationFilter被插入到FilterChainProxy中作为SecurityFilterChain中的一个filter。
如果应用程序没有抛出AccessDeniedException或AuthenticationException那么ExceptionTranslationFilter并不会做任何事情。

ExceptionTranslationFilter的处理流程

  1. ExceptionTranslationFilter首先会调用filterChain.doFilter来执行后续的逻辑
  2. 在执行后续请求时如果抛出了AuthenticationException那么则会开启认证流程
    1. 清空SecurityContextHolder
    2. 保存HttpServletRequest当认证通过时还能重新发送原始请求
    3. AuthenticationEntryPoint用于重新请求用户的凭证,其可能会重定向到登录页面或是发送 www-authenticateheader
  3. 在执行后续请求时如果抛出了AccessDeniedException那么会调用AccessDeniedHandler用于处理访问拒绝。
  4. 如果执行后续逻辑时,抛出了非AuthenticationExceptionAccessDeniedException的异常那么异常将会被重新抛出ExceptionTranslationFilter并不会针对其进行处理
// 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的自定义使用

@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

@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中设置值示例如下

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。

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也必须要显式指定。

// 显式指定form login的配置
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
	http
		.formLogin(withDefaults());
	// ...
}

如果想要自定义login form page可以使用如下配置

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。

@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中随机数次啊用如下格式

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。

@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可以通过如下方式进行配置

@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来指定密码编码器时无法防止通过反编译字节码来获取密码的攻击。

@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如下

# 创建用户表和权限表,并且将用户表和权限表之间用外键关联
#	用户表需要提供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 (
	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的表默认如下

# 如果要为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

//  生产环境时,应该通过对外部数据库的连接来建立数据源
@Bean
DataSource dataSource() {
	return new EmbeddedDatabaseBuilder()
		.setType(H2)
		.addScript(JdbcDaoImpl.DEFAULT_USER_SCHEMA_DDL_LOCATION)
		.build();
}

创建JdbcUserDetailsManager Bean对象

@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对象的方式来自定义认证过程。

// 自定义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中传递给AuthenticationManagerProviderManager实现了AuthenticationManager
  2. ProviderManager被配置为使用DaoAuthenticationProvider
  3. DaoAuthenticationProvider通过UserDetailsService来查找UserDetails
  4. DaoAuthenticationProvider通过PasswordEncoder来验证UserDetails中的密码
  5. 当验证成功时会返回UsernamePasswordAuthenticationToken类型的Authentication并且返回的Authentication拥有一个主体为UserDetailsService返回的UserDetails
  6. 返回的UsernamePasswordAuthenticationToken会在SecurityContextHolder中保存