Spring Security 认证源码超详细分析
认证(Authentication)是系统确认用户信息的重要途径,用户通过认证之后,系统才能明确用户的身份,进而才可以为该用户分配一定的权限,这个过程也叫授权(Authorization)。认证也是进入 Spring Security 框架的第一步。
- 搭建 Spring Security 入门案例
(1)引入依赖:
<!--Spring Boot父工程--><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>3.2.0</version></parent><dependencies><!--Web场景启动器--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!--Spring Security场景启动器--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency></dependencies>
(2)场景启动器:
package cn.xdf;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;/*** @author lscl* @version 1.0* @intro:*/
@SpringBootApplication
public class Test01Application {public static void main(String[] args) {SpringApplication.run(Test01Application.class, args);}
}
(3)编写一个Controller:
package cn.xdf.controller;import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;/*** @author lscl* @version 1.0* @intro:*/
@RestController
public class HelloController {@RequestMapping("/hello")public String hello(){return "Hello Spring Security!";}
}
在引入了Spring Security的场景启动器后,默认情况下所有的请求都将被Spring Security接管,都需要认证(登录)后才可以访问。
我们启动项目,访问:http://localhost:8080/hello,会发现请求被Spring Security拦截下来了,以下是Spring Security提供的登录页面,如图所示:
Spring Security提供的默认用户名为:user,默认密码会在Spring Boot项目启动时控制台中输出。
我们也可以在application.yml配置文件中对用户名和密码进行设定:
spring:security:user:name: xiaohuipassword: admin
3.1 认证架构组件分析
在 Spring Security 的认证整体架构中存在非常多的组件,这些组件在 Java 中都被一些接口或抽象类所描述,我们先了解这些组件的工作原理,然后学习它们的实现类有哪些,不同的实现类提供的功能又是如何。这样我们才能对 Spring Security 的认证整体流程有一个非常清晰的认识。
3.1.1 SecurityContextHolder
SecurityContextHolder 是 Spring Security 中保存认证信息(用户的身份信息)的核心组件,它负责管理当前请求的 Authentication 对象(保存用户信息),包括存储、清除等。如果 SecurityContextHolder 中存储了 Authentication 对象,那么 Spring Security 则认为当前请求已认证。
Authentication 对象是被包裹为 SecurityContext 对象存储在 SecurityContextHolder 中的,其关系如图所示:
下面的伪代码展示了 SecurityContext 的创建过程。
// 通过 SecurityContextHolder 创建 SecurityContext 对象
SecurityContext context = SecurityContextHolder.createEmptyContext(); // 创建用户认证信息
Authentication authentication =new TestingAuthenticationToken("username", "password", "ROLE_USER"); // 将用户认证信息存入 SecurityContext 中
context.setAuthentication(authentication);// 将 SecurityContext 设置到 SecurityContextHolder
SecurityContextHolder.setContext(context);
当用户认证成功后,Authentication 对象先被包裹成 SecurityContext 对象然后存储在 SecurityContextHolder 中。然后 Spring Security 会将存储在 SecurityContextHolder 中的 SecurityContext 对象存入 Session 中,名为 SPRING_SECURITY_CONTEXT,最后将 SecurityContexHolder 中的数据清空。
我们可以编写一个接口,来查看 Session 中存储的 Security 上下文:
@RequestMapping("/showAuthentication")
public SecurityContext showAuthentication(HttpSession session){// 获取 session 中存储的 Security 上下文SecurityContext loginUser = (SecurityContext) session.getAttribute("SPRING_SECURITY_CONTEXT");System.out.println(loginUser);return loginUser;
}
该接口响应的数据如下:
{"authentication": { // 该 SecurityContext 保存的 authentication对象"authorities": [], "details": {"remoteAddress": "0:0:0:0:0:0:0:1","sessionId": "597B5AC2A6EC50541A74D0CE779588BE"},"authenticated": true, "principal": {"password": null,"username": "xiaohui","authorities": [],"accountNonExpired": true,"accountNonLocked": true,"credentialsNonExpired": true,"enabled": true},"credentials": null,"name": "xiaohui"}
}
用户认证成功之后,每当有请求到来时,Spring Security 就会先从 Session 中取出用户数据(SecurityContext),保存到 SecurityContextHolder 中,因为该请求后续还要经过 Spring Security 的其他过滤器,这些过滤器中需要通过 SecurityContextHolder 中的 SecurityContext 数据来判断用户的认证状态。同时在请求结束时将 SecurityContextHolder 中的数据拿出来保存到 Session 中,然后再通过 FilterChainProxy 将 SecurityContextHolder 中的数据清空。
Tips:默认情况下,SecurityContextHolder 中的数据保存默认是通过 ThreadLocal 来实现的,使用 ThreadLocal 创建的变量只能被当前线程访问,不能被其他线程访问和修改,也就是用户数据和请求线程绑定在一起,这意味着 SecurityContext 对同一线程中的方法是可用的。
有些应用程序它们与线程的工作方式比较特殊,可能并不完全适合使用 ThreadLocal,那么可以在创建时用一个策略来配置 SecurityContextHolder 来指定如何存储 Security 上下文
3.1.2 AuthenticationManager
AuthenticationManager 接口是 Spring Security 负责认证的核心组件,它定义了处理认证请求的基本流程,用于接收一个封装了用户认证信息的 Authentication 对象(只包含用户名、密码等),并根据配置的认证策略对用户的身份进行验证。如果验证成功,它将返回一个已认证的 Authentication 对象(包含用户名、密码、权限、是否认证等完整信息)。
AuthenticationManager 接口如下:
public interface AuthenticationManager {// 对传递的身份验证对象(Authentication)进行身份验证,如果成功,则返回一个完全填充的身份验证对象(包括授予的权限)Authentication authenticate(Authentication authentication) throws AuthenticationException;
}
关于 AuthenticationManager 的实现,在官方中有如下定义:
(1)如果帐户被禁用,并且AuthenticationManager可以测试此状态,则必须抛出DisabledException。
(2)如果帐户被锁定并且AuthenticationManager可以测试帐户锁定,则必须抛出LockedException。
(3)如果提供了不正确的凭据,必须抛出BadCredentialsException。
AuthenticationManager 的默认实现是 ProviderManager,它内部维护了一个 AuthenticationProvider 的列表。这个列表中的每一个 AuthenticationProvider 都代表了一种特定的身份认证方式,当需要进行身份认证时,ProviderManager 会遍历这个列表,依次调用每个 AuthenticationProvider 的 authenticate 方法进行认证尝试。
ProviderManager 本身也可以配置一个 AuthenticationManager 作为 parent。这种父子关系的设置允许在子 ProviderManager 认证失败时,将认证请求转发给父ProviderManager 进行再次认证。
(1)AuthenticationProvider
通常,在 AuthenticationManager 中会存在一个或多个 AuthenticationProvider,AuthenticationManager 通过委托的方式将具体的认证逻辑交给 AuthenticationProvider 实现,这样我们就可以为某个 AuthenticationManager 提供多种认证方式,即提供多个 AuthenticationProvider,每个 AuthenticationProvider 都执行一种特定类型的认证,Spring Security 官方也提供了非常多的 AuthenticationProvider 子类,如:
- TestingAuthenticationProvider:用于测试的认证实现,它可以绕过实际的认证逻辑,直接返回一个预定义的 Authentication 对象。
- RememberMeAuthenticationProvider:用于完成“记住我”功能的认证实现,允许用户在成功登录后,在一定时间内无需再次输入用户名和密码即可自动登录系统。
- DaoAuthenticationProvider:用于用户名+密码的认证实现。通过集成数据库(如JDBC、JPA等)来实现用户认证。它依赖于UserDetailsService接口的实现类来从数据库中查询用户信息,并根据这些信息与用户提供的凭证(如用户名和密码)进行比对。
AuthenticationProvider 接口如下:
public interface AuthenticationProvider {/*** 对Authentication进行认证,如果身份验证失败,抛出:AuthenticationException。* 如过返回null,则代表当前AuthenticationProvider无法认证,继续使用下一个Provider进行认证*/Authentication authenticate(Authentication authentication) throws AuthenticationException;// 传入该authentication的类型(字节码对象),返回当前Provider是否支持对该authentication对象boolean supports(Class<?> authentication);
}
使用 AuthenticationProvider 来对请求认证,这也是官方推荐的模式。但这并非是强制性约束,因为在AuthenticationManager 接口中只有一个方法,即:Authentication authenticate(Authentication authentication) ,用户只需要实现该方法并完成相应的功能,即:传递一个封装了认证信息的 Authentication 然后对该用户进行认证,最终返回一个已认证的 Authentication 即可。具体如何实现认证过程,是借助 AuthenticationProvider 来完成认证还是自行实现认证细节全部由用户自定义。
默认情况下,Spring Security 将使用 AuthenticationProvider 的子类 AbstractUserDetailsAuthenticationProvider 类来进行认证处理,该类重写了 authenticate(…) 方法,在该方法中调用了retrieveUser(…) 方法作为具体认证流程的方法。但 AbstractUserDetailsAuthenticationProvider 自身并没有对 retrieveUser(…) 方法实现。而是最终交给了它的子类——DaoAuthenticationProvider 类。
下列是 AbstractUserDetailsAuthenticationProvider 的伪代码:
public Authentication authenticate(Authentication authentication) throws AuthenticationException {String username = determineUsername(authentication);// 先从缓存中获取UserDetails对象UserDetails user = this.userCache.getUserFromCache(username);// 调用retrieveUser方法查询到一个UserDetails对象user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);// 根据user来创建Authentication对象return createSuccessAuthentication(principalToReturn, authentication, user);
}// 抽象方法
protected abstract UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)throws AuthenticationException;// 将UserDetails对象转为Authentication对象
protected Authentication createSuccessAuthentication(Object principal, Authentication authentication,UserDetails user) {// 该Authentication对象是已经经过认证的(authenticated为true)UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(principal,authentication.getCredentials(),this.authoritiesMapper.mapAuthorities(user.getAuthorities()));result.setDetails(authentication.getDetails());return result;
}
DaoAuthenticationProvider 类对 retrieveUser(…) 方法做了实现,并交由 UserDetailsService 组件进行后续的认证流程处理。retrieveUser(…) 方法返回一个 UserDetails 对象(封装了用户信息),AbstractUserDetailsAuthenticationProvider 负责将 UserDetails 封装为一个合格的 Authentication 对象。然后请求回到 FilterChainProxy ,由 FilterChainProxy 处理后续流程,至此,Spring Security 认证流程结束。
下列是 DaoAuthenticationProvider 类的伪代码:
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication){UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);return loadedUser;
}
(2)UserDetailsService
UserDetailsService 作为 DaoAuthenticationProvider 的一个内部成员,用于加载用户信息并进行身份验证,通过用户名加载用户的详细信息,并将其封装为 UserDetails 对象返回,以供 Spring Security 进行后续的身份验证和授权操作。
Tips:需要注意的是,我们现在讨论的是 Spring Security 默认的认证流程。实际上,如果我们自定义了 AuthenticationManager 自身来实现认证流程,而非使用 AuthenticationProvider,那么 AuthenticationProvider、UserDetailsService 等组件将不会执行,除非我们自身使用到了这些组件。
UserDetailsService 提供了一个关键方法 loadUserByUsername(String username),通过用户名加载用户的详细信息,并将其封装为 UserDetails 对象返回,以供Spring Security 进行后续的身份验证和授权操作。
UserDetailsService 接口如下:
public interface UserDetailsService {// 根据用户名查询用户信息,该用户信息被封装为(UserDetails)UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
UserDetailsService 只提供了根据用户名查询用户信息(UserDetails)方法,UserDetailsManager 作为 UserDetailsService 的子接口还提供了一些管理 UserDetails 的方法,UserDetailsManager 接口如下:
public interface UserDetailsManager extends UserDetailsService {// 创建UserDetailsvoid createUser(UserDetails user);// 更新UserDetailsvoid updateUser(UserDetails user);// 根据用户名删除UserDetailsvoid deleteUser(String username);// 修改UserDetails密码void changePassword(String oldPassword, String newPassword);// 检查该UserDetails是否存在boolean userExists(String username);
}
大多数情况下,我们都是使用 UserDetailsManager 接口而非 UserDetailsService接口,Spring Security 的默认实现为 InMemoryUserDetailsManager,从名字我们也能看得出来,该类实现与 UserDetailsManager 接口。InMemoryUserDetailsManager 类将读取内存中的存储的用户信息与用户传递过来的用户信息进行比对。
InMemoryUserDetailsManager 如实现代码如下:
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {// 根据用户名获取对应的UserDetails对象(该UserDetails对象存储在内存中,服务器启动时就已经加载)UserDetails user = this.users.get(username.toLowerCase());if (user == null) {throw new UsernameNotFoundException(username);}// 将用户名、密码、权限等所有信息封装成一个User对象(UserDetails)返回return new User(user.getUsername(), user.getPassword(), user.isEnabled(), user.isAccountNonExpired(),user.isCredentialsNonExpired(), user.isAccountNonLocked(), user.getAuthorities());
}
(3)UserDetails
UserDetails 用于表示用户信息,它封装了用户的认证信息,包括用户名、密码、权限等。这个接口用于表示用户的具体信息。AuthenticationProvider 调用 UserDetailsService 的 loadUserByUsername 方法来加载用户信息,并将用户信息封装为一个实现了 UserDetails 接口的对象返回。
UserDetails 接口如下:
public interface UserDetails extends Serializable {// 返回用户的权限(不能为null)Collection<? extends GrantedAuthority> getAuthorities();// 返回用户的密码String getPassword();// 返回用户名(用户名不能为null)String getUsername();// 返回用户帐号是否过期(过期的帐户无法进行身份验证)boolean isAccountNonExpired();// 返回处于锁定状态或未锁定状态(被锁定的用户无法进行认证)boolean isAccountNonLocked();// 指示用户的凭证(密码)是否已过期(过期凭据阻止身份验证)boolean isCredentialsNonExpired();// 返回用户是否启用或禁用(禁用的用户无法进行认证)boolean isEnabled();
}
UserDetails 接口的默认实现为 User 类,在 InMemoryUserDetailsManager 中读取了内存中存储的用户信息,并将用户信息封装为一个 User 对象返回。
3.1.3 Authentication
Authentication 是 Spring Security 用于封装用户认证相关的核心组件,它包含了用户的身份信息、认证状态以及用户所具有的权限信息。
Authentication 对象包含如下信息:
- 身份信息:包含用户提交的凭据信息,如用户名和密码。我们可以通过 Authentication 对象获取该用户名和密码等信息。
- 权限信息:包含用户的权限信息,即 GrantedAuthority 集合。这些权限信息用于决定用户可以访问哪些资源或执行哪些操作。
- 认证状态:包含用户的认证状态,即用户是否已经被成功认证。这对 Spring Security 后续的执行流程影响非常重大。
Authentication 接口的源代码如下:
public interface Authentication extends Principal, Serializable {// 权限信息Collection<? extends GrantedAuthority> getAuthorities();// 密码Object getCredentials();// 有关认证的其他信息,由用户自行封装,默认为ip地址、sessionIdObject getDetails();// 用户信息(用户名)Object getPrincipal();// 是否认证boolean isAuthenticated();// 设置认证状态void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}
默认情况下,当 Spring Security 对用户进行认证时,首先会将用户的信息包装成一个 Authentication 对象,该对象仅包含一些基本信息(如用户名、密码等),之后 Spring Security 将交由 AuthenticationManager 中的 AuthenticationProvider 来对该 Authentication 进行认证处理,认证成功后将重置该对象的认证状态并为该对象赋值一些其他信息(如权限等信息),之后将该对象存入 SecurityContextHolder 中。
Authentication 接口在 Spring Security 中主要有两个作用。
(1)封装用户的信息:用于传递给 AuthenticationManager 进行认证处理。
(2)代表当前认证的用户:当用户认证成功后,Authentication 将存储我们认证的所有信息。
(1)Authentication-未认证
在 Spring Security 默认注册的16个过滤器中,其中有一个 UsernamePasswordAuthenticationFilter 过滤器,该过滤器主要负责处理基于表单的登录请求。当用户提交包含用户名和密码的表单时,该过滤器会拦截这些请求,并启动身份验证过程。
在 UsernamePasswordAuthenticationFilter 过滤器中,将表单提交的用户名和密码封装为了一个 UsernamePasswordAuthenticationToken 对象(Authentication的子类),该 Authentication 对象还未经过认证, 然后交由 AuthenticationManager 组件进行后续的认证流程。
UsernamePasswordAuthenticationFilter 的核心代码大致如下:
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)throws AuthenticationException {// 必须是post提交if (this.postOnly && !request.getMethod().equals("POST")) {throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());}// 获取表单的用户名String username = obtainUsername(request);username = (username != null) ? username : "";username = username.trim();// 获取表单的密码String password = obtainPassword(request);password = (password != null) ? password : "";// 将用户名和密码封装为一个Authentication对象,该Authentication是未经过认证的(authenticated为false)UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);// 设置Authentication的一些其他信息setDetails(request, authRequest);// 调用AuthenticationManager组件进行后续的认证操作,并返回一个已认证的Authentication对象return this.getAuthenticationManager().authenticate(authRequest);
}
(2)Authentication-已认证
Authentication 可以代表一个未认证的用户信息,也可以代表一个已经认证的用户信息。在 AbstractUserDetailsAuthenticationProvider 中,authenticate()方法用于获取一个 UserDetails 对象,并将 UserDetails 对象封装成一个已经通过认证的 Authentication 对象。
public Authentication authenticate(Authentication authentication) throws AuthenticationException {String username = determineUsername(authentication);// 先从缓存中获取UserDetails对象UserDetails user = this.userCache.getUserFromCache(username);// 调用retrieveUser方法查询到一个UserDetails对象user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);Object principalToReturn = user;if (this.forcePrincipalAsString) {// 用户名principalToReturn = user.getUsername();}// 根据UserDetails来创建Authentication对象return createSuccessAuthentication(principalToReturn, authentication, user);
}// 抽象方法
protected abstract UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)throws AuthenticationException;// 将UserDetails对象转为Authentication对象
protected Authentication createSuccessAuthentication(Object principal, Authentication authentication,UserDetails user) {// 该Authentication对象是已经经过认证的(authenticated为true)UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(// 用户名principal,// 密码authentication.getCredentials(), // 密码this.authoritiesMapper.mapAuthorities(user.getAuthorities()));// 设置Authentication的一些其他信息result.setDetails(authentication.getDetails());return result;
}
(3)GrantedAuthority
GrantedAuthority 是 Spring Security 用于存储用户权限的核心组件,通过 Authentication 的 getAuthorities() 方法可以获取到当前用户的所有 GrantedAuthority 实例,每个 GrantedAuthority 实例通常对应一个字符串值,例如 “ROLE_ADMIN” 或 “CAN_DELETE”,用于访问过程中的权限检查。
GrantedAuthority 接口信息如下:
public interface GrantedAuthority extends Serializable {// 返回权限字符串String getAuthority();
}
GrantedAuthority 是作为 Authentication 中的一部分存储在 Authentication 对象中的。因此,GrantedAuthority 自然也是由 AuthenticationProvider 的实例(AbstractUserDetailsAuthenticationProvider、DaoAuthenticationProvider、InMemoryUserDetailsManager等组件)进行封装。当用户信息验证成功后,AuthenticationProvider 会构造一个 Authentication 对象,并将用户的 GrantedAuthority 集合(权限信息)设置到该对象中。
GrantedAuthority 的默认实现为 SimpleGrantedAuthority。该类只有一个成员变量,用于存储该对象保存的字符串权限:
public final class SimpleGrantedAuthority implements GrantedAuthority {...public SimpleGrantedAuthority(String role) {Assert.hasText(role, "A granted authority textual representation is required");this.role = role;}@Overridepublic String getAuthority() {return this.role;}...
}
3.1.4 AbstractAuthenticationProcessingFilter
AbstractAuthenticationProcessingFilter 为处理认证请求提供了一个基础框架。这个抽象类主要用于处理 HTTP 请求,并将其转换为 Authentication 对象,然后提交给 AuthenticationManager 进行认证。这不就是我们前面介绍的 UsernamePasswordAuthenticationFilter 吗?没错,AbstractAuthenticationProcessingFilter 的默认实现就是 UsernamePasswordAuthenticationFilter 。
AbstractAuthenticationProcessingFilter 的工作流程如下:
- 1)当用户提交他们的凭证时,AbstractAuthenticationProcessingFilter 将会调用 attemptAuthentication(…)方法来进行后续的认证流程,该方法为抽象方法,具体执行流程需要参考子类的实现,默认情况下的子类为:UsernamePasswordAuthenticationFilter,该类会从 HttpServletRequest 中获取用户的信息并以此来创建一个需要认证的 Authentication(该对象还未经过认证)。创建的认证的类型以及具体认证的流程取决于 AbstractAuthenticationProcessingFilter 的子类。
- 2)一般情况下,Authentication 将被传入 AuthenticationManager,以进行认证(这取决于子类),返回一个已经认证成功的 Authentication 对象。
- 3)如果认证失败,AbstractAuthenticationProcessingFilter 将会做如下操作:
- (1)SecurityContextHolder 被清空。
- (2)RememberMeServices.loginFail 被调用,将调用 RememberMeServices 组件进行后续操作。
- (3)AuthenticationFailureHandler 被调用。将调用 AuthenticationFailureHandler 组件进行后续操作。
- 4)如果认证成功,AbstractAuthenticationProcessingFilter 将会做如下操作:
- (1)SessionAuthenticationStrategy 被通知有新的登录。将调用 SessionAuthenticationStrategy 组件进行后续操作。
- (2)SecurityContextHolder 将创建 SecurityContext,并将 Authentication 存入 SecurityContext,最后再将 SecurityContext 设置到 SecurityContextHolder 上。
- (3)RememberMeServices.loginSuccess 被调用。将调用 RememberMeServices 组件进行后续操作。
- (4)ApplicationEventPublisher 发布一个 InteractiveAuthenticationSuccessEvent 事件。
- (5)AuthenticationSuccessHandler 被调用。将调用 AuthenticationSuccessHandler 组件进行后续操作。
AbstractAuthenticationProcessingFilter 的核心源码大致如下:
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)throws IOException, ServletException {// 调用了自身的doFilterdoFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
}private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)throws IOException, ServletException {// 判断该请求是否需要进行后续的认证流程if (!requiresAuthentication(request, response)) {chain.doFilter(request, response);return;}try {// 调用attemptAuthentication方法进行认证,认证后返回一个Authentication对象Authentication authenticationResult = attemptAuthentication(request, response);this.sessionStrategy.onAuthentication(authenticationResult, request, response);// 认证成功if (this.continueChainBeforeSuccessfulAuthentication) {chain.doFilter(request, response);}// 执行认证成功的流程successfulAuthentication(request, response, chain, authenticationResult);}catch (InternalAuthenticationServiceException failed) {// 认证过程中内部问题导致的失败(例如代码异常等)this.logger.error("An internal error occurred while trying to authenticate the user.", failed);unsuccessfulAuthentication(request, response, failed);}catch (AuthenticationException ex) {// 认证失败(例如用户名、密码错误登)unsuccessfulAuthentication(request, response, ex);}
}
认证失败的大致流程:
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,AuthenticationException failed) throws IOException, ServletException {// 清空SecurityContextHolderSecurityContextHolder.clearContext();// 调用RememberMeServicesthis.rememberMeServices.loginFail(request, response);// 调用AuthenticationFailureHandler this.failureHandler.onAuthenticationFailure(request, response, failed);
}
认证成功的大致流程:
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,Authentication authResult) throws IOException, ServletException {// 创建SecurityContextSecurityContext context = SecurityContextHolder.createEmptyContext();// 将Authentication设置到SecurityContextcontext.setAuthentication(authResult);// 将SecurityContext设置到SecurityContextHolderSecurityContextHolder.setContext(context);// 调用RememberMeServices this.rememberMeServices.loginSuccess(request, response, authResult);if (this.eventPublisher != null) {// 调用ApplicationEventPublisher发布一个InteractiveAuthenticationSuccessEvent事件this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));}// 调用AuthenticationSuccessHandler this.successHandler.onAuthenticationSuccess(request, response, authResult);
}
(1)AuthenticationSuccessHandler
AuthenticationSuccessHandler 用于处理认证成功之后执行的流程。当一个请求在 AbstractAuthenticationProcessingFilter#attemptAuthentication方法认证成功后,将执行 successfulAuthentication(…) 方法进行后续的流程。在该方法中,使用了 AuthenticationSuccessHandler 组件来进行后续的流程。
AuthenticationSuccessHandler 接口如下:
public interface AuthenticationSuccessHandler {// 进行后续的认证处理(该Authentication是已经通过认证的)default void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, FilterChain chain,Authentication authentication) throws IOException, ServletException {onAuthenticationSuccess(request, response, authentication);chain.doFilter(request, response);}// 进行后续的认证处理(该Authentication是已经通过认证的)void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,Authentication authentication) throws IOException, ServletException;
}
默认情况下该接口的实现类为 SavedRequestAwareAuthenticationSuccessHandler,该类能够处理认证成功后的重定向逻辑。当用户尝试访问一个需要认证的资源时,如果用户还没有认证,ExceptionTranslationFilter 会将原始请求保存起来,并重定向用户到登录页面。一旦用户成功登录,SavedRequestAwareAuthenticationSuccessHandler 就会根据之前保存的请求来决定用户应该被重定向到哪个页面。
SavedRequestAwareAuthenticationSuccessHandler 处理请求的大致代码如下:
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,Authentication authentication) throws ServletException, IOException {// 获取在ExceptionTranslationFilter中保存的请求对象SavedRequest savedRequest = this.requestCache.getRequest(request, response);if (savedRequest == null) {// 说明是直接访问login请求,并没有上一个请求(那就重定向到默认的地址"/")super.onAuthenticationSuccess(request, response, authentication);return;}// 是否有设置登录成功后重定向的页面String targetUrlParameter = getTargetUrlParameter();if (isAlwaysUseDefaultTargetUrl()|| (targetUrlParameter != null && StringUtils.hasText(request.getParameter(targetUrlParameter)))) {this.requestCache.removeRequest(request, response);super.onAuthenticationSuccess(request, response, authentication);return;}clearAuthenticationAttributes(request);// 获取该对象中保存的路径String targetUrl = savedRequest.getRedirectUrl();// 重定向到该路径getRedirectStrategy().sendRedirect(request, response, targetUrl);
}
(2)AuthenticationFailureHandler
AuthenticationFailureHandler 用于处理用户认证失败的流程。当一个请求在 AbstractAuthenticationProcessingFilter#attemptAuthentication方法认证失败后(例如用户名或密码错误等),将执行 unsuccessfulAuthentication(…) 方法进行后续的流程。在该方法中,使用了 AuthenticationFailureHandler 组件来进行后续的流程。
AuthenticationFailureHandler 接口如下:
public interface AuthenticationFailureHandler {// 对认证失败的请求做处理void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,AuthenticationException exception) throws IOException, ServletException;
}
默认情况下该接口的实现类为 SimpleUrlAuthenticationFailureHandler,该类能够处理认证失败后的重定向逻辑,该类会根据配置执行相应的操作,如重定向到错误页面或返回 HTTP 错误状态码。
SimpleUrlAuthenticationFailureHandler 处理请求的大致代码如下:
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,AuthenticationException exception) throws IOException, ServletException {if (this.defaultFailureUrl == null) {// 如果没有设置登录失败的重定向页面那就重定向到一个失败的页面response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase());return;}// 将出现的异常保存起来(默认是保存到session中,key为:SPRING_SECURITY_LAST_EXCEPTION)saveException(request, exception);if (this.forwardToDestination) {this.logger.debug("Forwarding to " + this.defaultFailureUrl);request.getRequestDispatcher(this.defaultFailureUrl).forward(request, response);}else {// 默认重定向到/login页面进行重新登录this.redirectStrategy.sendRedirect(request, response, this.defaultFailureUrl);}
}
3.1.5 ExceptionTranslationFilter
ExceptionTranslationFilter 是 Spring Security 的核心过滤器中的其中一个,它位于过滤器链中的较后位置。其主要职责是处理认证和授权过程中抛出的异常,并将这些异常转化为适当的响应,例如重定向到登录页面或者返回一个 HTTP 状态码来表示访问被拒绝。
ExceptionTranslationFilter 过滤器主要的作用是针对认证和授权失败之后的处理:
- 认证失败处理:当用户尝试访问受保护的资源但未通过认证时,ExceptionTranslationFilter 会捕获相应的认证异常(AuthenticationException),并根据配置决定如何响应,比如重定向到登录页面。
- 授权失败处理:如果用户已经通过认证但没有足够的权限访问某些资源,ExceptionTranslationFilter 会捕获授权异常(AccessDeniedException),并采取适当的措施,如显示一个访问被拒绝的错误页面。
Tips:对于一般的认证异常(AuthenticationException),在 AbstractAuthenticationProcessingFilter#unsuccessfulAuthentication 已经处理了,ExceptionTranslationFilter 用户处理那些在认证过程中未处理的认证异常。
其代码大致如下:
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)throws IOException, ServletException {try {chain.doFilter(request, response);}catch (IOException ex) {throw ex;}catch (Exception ex) {// 获取认证/授权过程中的SpringSecurityException异常Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(ex);RuntimeException securityException = (AuthenticationException) this.throwableAnalyzer.getFirstThrowableOfType(AuthenticationException.class, causeChain);if (securityException == null) {securityException = (AccessDeniedException) this.throwableAnalyzer.getFirstThrowableOfType(AccessDeniedException.class, causeChain);}...// 进行后续处理 handleSpringSecurityException(request, response, chain, securityException);}}
// 处理SpringSecurityException异常
private void handleSpringSecurityException(HttpServletRequest request, HttpServletResponse response,FilterChain chain, RuntimeException exception) throws IOException, ServletException {if (exception instanceof AuthenticationException) {// 处理认证异常handleAuthenticationException(request, response, chain, (AuthenticationException) exception);}else if (exception instanceof AccessDeniedException) {// 处理授权异常(权限不足)handleAccessDeniedException(request, response, chain, (AccessDeniedException) exception);}
}
某个请求不具备当前资源的访问权限时,在后续的授权过程中就会出现异常,这个异常将被 ExceptionTranslationFilter 捕获到,它首先检查当前请求是否具备访问权限。如果没有,它会检查是否需要认证,并决定是否应该启动认证过程。handleAuthenticationException(…) 和 handleAccessDeniedException(…)方法中都调用了 sendStartAuthentication(…) 方法,该方法正是处理认证或授权失败的后续流程的核心逻辑,代码大致如下:
protected void sendStartAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, AuthenticationException reason) throws ServletException, IOException {// 创建了一个SecurityContextSecurityContext context = SecurityContextHolder.createEmptyContext();// 并将SecurityContext设置到SecurityContextHolder中SecurityContextHolder.setContext(context);// 将当前请求保存到requestCache中,等到认证成功后需要重定向到这个请求来this.requestCache.saveRequest(request, response);// 交由AuthenticationEntryPoint组件进行后续流程this.authenticationEntryPoint.commence(request, response, reason);
}
1)AuthenticationEntryPoint
AuthenticationEntryPoint 接口用于处理未认证的 HTTP 请求,通常由 ExceptionTranslationFilter 调用。当一个用户尝试访问受保护的资源但尚未通过认证时,Spring Security 会调用 AuthenticationEntryPoint 来处理这种情况。
AuthenticationEntryPoint 接口如下:
public interface AuthenticationEntryPoint {// 根据需要来做出响应给前端void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException)throws IOException, ServletException;
}
Spring Security 默认采用 DelegatingAuthenticationEntryPoint 作为实现类来处理未登录的请求,在该类中存在一个 AuthenticationEntryPoint 的Map集合,key 为 RequestMatcher 类型,值为 AuthenticationEntryPoint,也就是说一个 RequestMatcher 对应一个 AuthenticationEntryPoint。实际上后续的逻辑将委派给这些 AuthenticationEntryPoint。
默认情况下最终将交由 LoginUrlAuthenticationEntryPoint 来处理未认证的 HTTP请求,该类将未认证的请求重定向到/login请求中。
2)RequestMatcher
RequestMatcher 接口用于对当前请求进行判断,之后返回一个布尔值。也就是说 RequestMatcher 代表一种特定的请求,AuthenticationEntryPoint 代表一种特定的处理方式。如默认情况下 Spring Security 加载两个 RequestMatcher ,分别为 AndRequestMatcher 和 OrRequestMatcher。
DelegatingAuthenticationEntryPoint 的核心代码大致如下:
// 自身的所有AuthenticationEntryPoint(每一种AuthenticationEntryPoint代表一种特定的处理方式)
private final LinkedHashMap<RequestMatcher, AuthenticationEntryPoint> entryPoints;// 默认的
private AuthenticationEntryPoint defaultEntryPoint;@Override
public void commence(HttpServletRequest request, HttpServletResponse response,AuthenticationException authException) throws IOException, ServletException {// 迭代所有的RequestMatcher,查看当前的RequestMatcher是否支持处理当前请求for (RequestMatcher requestMatcher : this.entryPoints.keySet()) {// 判断当前的requestMatcher是否支持处理当前的请求,if (requestMatcher.matches(request)) {// 如果支持,则获取requestMatcher对应的AuthenticationEntryPointAuthenticationEntryPoint entryPoint = this.entryPoints.get(requestMatcher);// 调用AuthenticationEntryPoint来处理后续流程entryPoint.commence(request, response, authException);return;}}// 没有合适的AuthenticationEntryPoint就采用默认的this.defaultEntryPoint.commence(request, response, authException);
}
3.2 对默认的认证源码分析
3.2.1 Spring Security 认证流程分析
了解完 Spring Security 中的重要组件后,我们开始对 Spring Security 的认证流程做一个整体的分析。Spring Security 认证流程图如下图所示。
(1)浏览器发送请求最先来到 AbstractAuthenticationProcessingFilter,将执行该过滤器的 doFilter(…)方法,在 doFilter(…)方法中调用了attemptAuthentication(…)方法。
(2)UsernamePasswordAuthenticationFilter 作为 AbstractAuthenticationProcessingFilter 的实现类,实现了attemptAuthentication(…)方法,并在方法中调用了AuthenticationManager 接口的 authenticate(…)方法。
(3)ProviderManager 作为 AuthenticationManager 的实现类,实现了 authenticate(…)方法。在方法中将任务委派给了 AuthenticationProvider 接口。
(4)AbstractUserDetailsAuthenticationProvider(抽象类)实现了 AuthenticationProvider 接口的 authenticate(…)方法,并在方法中调用了 retrieveUser(…)方法。
(5)DaoAuthenticationProvider 实现了 AbstractUserDetailsAuthenticationProvider 中的 retrieveUser(…)方法,并调用了 UserDetailsService 的 loadUserByUsername(…)方法。
(6)UserDetailsManager 作为 UserDetailsService 的子接口规范了许多管理用户信息方法,最终 InMemoryUserDetailsManager 实现了 UserDetailsManager 接口,重写了 loadUserByUsername(…)方法。
3.2.1 Spring Security 认证源码分析
(1)我们之前在 SpringBootWebSecurityConfiguration 配置类中,查看到该配置类往 SpringIOC 容器中注入了一个 SecurityFilterChain(实现类为DefaultSecurityFilterChain) ,并添加了一些默认配置,如下。
@Configuration(proxyBeanMethods = false) // 标注当前是配置类
@ConditionalOnWebApplication(type = Type.SERVLET) // 当前运行是是Servlet容器
class SpringBootWebSecurityConfiguration {// 如果当前IOC容器没有WebSecurityConfigurerAdapter和SecurityFilterChain对象才会生效@Configuration(proxyBeanMethods = false)@ConditionalOnDefaultWebSecuritystatic class SecurityFilterChainConfiguration {@Bean@Order(SecurityProperties.BASIC_AUTH_ORDER)SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {http.authorizeHttpRequests( // 开启认证(requests) -> requests.anyRequest() // 所有请求.authenticated() // 都必须认证);http.formLogin(withDefaults()); // 可以表单认证http.httpBasic(withDefaults()); // 可以httpBasic认证return http.build(); // 返回的是DefaultSecurityFilterChain对象}}...
}
(2)我们点开 formLogin() 方法,在 FormLoginConfigurer 类中配置了 UsernamePasswordAuthenticationFilter 过滤器。
public HttpSecurity formLogin(Customizer<FormLoginConfigurer<HttpSecurity>> formLoginCustomizer) throws Exception {// 配置了一个FormLoginConfigurer类formLoginCustomizer.customize(getOrApply(new FormLoginConfigurer<>()));return HttpSecurity.this;
}// 打开FormLoginConfigurer,发现创建了一个UsernamePasswordAuthenticationFilter过滤器
public FormLoginConfigurer() {super(new UsernamePasswordAuthenticationFilter(), null);usernameParameter("username");passwordParameter("password");
}
(3)UsernamePasswordAuthenticationFilter 并没有重写父类的 doFilter 方法,但是父类的 doFilter 中调用了 attemptAuthentication 方法,所以我们直接查看 UsernamePasswordAuthenticationFilter 的 attemptAuthentication 即可。
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)throws AuthenticationException {if (this.postOnly && !request.getMethod().equals("POST")) {throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());}// 获取提交的用户名String username = obtainUsername(request);username = (username != null) ? username : "";username = username.trim();// 获取提交的密码String password = obtainPassword(request);password = (password != null) ? password : "";// 创建了一个UsernamePasswordAuthenticationToken(用户凭证)UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);// Allow subclasses to set the "details" propertysetDetails(request, authRequest);// 获取认证管理器,调用认证管理器中的 authenticate 方法进行认证return this.getAuthenticationManager().authenticate(authRequest);
}
(4)UsernamePasswordAuthenticationFilter 默认使用的认证管理器为 ProviderManager ,然后调用了 AbstractUserDetailsAuthenticationProvider 的 authenticate 方法进行认证。
@Overridepublic Authentication authenticate(Authentication authentication) throws AuthenticationException {Class<? extends Authentication> toTest = authentication.getClass();AuthenticationException lastException = null;AuthenticationException parentException = null;Authentication result = null;Authentication parentResult = null;int currentPosition = 0;int size = this.providers.size();// 判断当前的AuthenticationProvider是否支持对当前Authentication认证。for (AuthenticationProvider provider : getProviders()) {if (!provider.supports(toTest)) {continue;}....try {// 调用AuthenticationProvider的authenticate方法进行认证result = provider.authenticate(authentication);if (result != null) {copyDetails(authentication, result);break;}}....}....}
(5)在 AbstractUserDetailsAuthenticationProvider 类中调用了 retrieveUser 方法进行认证
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,() -> this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports","Only UsernamePasswordAuthenticationToken is supported"));String username = determineUsername(authentication);boolean cacheWasUsed = true;// 先从内置的缓存里面查询用户信息UserDetails user = this.userCache.getUserFromCache(username);if (user == null) {cacheWasUsed = false;try {// 如果缓存为null,再调用后续的流程去查询用户信息user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);}catch (UsernameNotFoundException ex) {if (!this.hideUserNotFoundExceptions) {throw ex;}// 用户名错误则抛出BadCredentialsException异常,该异常是一个AuthenticationException类型的异常(说明认证失败)throw new BadCredentialsException(...);}}try {this.preAuthenticationChecks.check(user);/* user: 系统里面查询到的用户信息authentication: 用户输入的(前端传递的)用户信息对比用户输入的和系统里面查询到的用户信息,如果对比失败则抛出BadCredentialsException异常*/additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);}catch (AuthenticationException ex) {if (!cacheWasUsed) {throw ex;}cacheWasUsed = false;// 如果认证失败,再认证一次,有可能上一次是缓存还没更新user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);this.preAuthenticationChecks.check(user);additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);}...if (!cacheWasUsed) {// 将user存入缓存this.userCache.putUserInCache(user);}Object principalToReturn = user;if (this.forcePrincipalAsString) {principalToReturn = user.getUsername();}// 根据user来创建一个Authenticationreturn createSuccessAuthentication(principalToReturn, authentication, user);
}
(6)最终来到了 DaoAuthenticationProvider 类调用该类的 retrieveUser方法,在方法中调用了 UserDetailsService 的 loadUserByUsername 方法进行认证:
@Override
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)throws AuthenticationException {....try {// 调用 InMemoryUserDetailsManager 的loadUserByUsername方法进行认证UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);if (loadedUser == null) {throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");}return loadedUser;}....
}
(7)最终调用的是 InMemoryUserDetailsManager 类的 loadUserByUsername 方法
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {// 读取配置文件里面的用户信息UserDetails user = this.users.get(username.toLowerCase());if (user == null) {// 用户名错误则抛出一个异常,该异常继承与AuthenticationException,说明是一个认证异常throw new UsernameNotFoundException(username);}// 封装成一个 UserDetailsreturn new User(user.getUsername(), user.getPassword(), user.isEnabled(), user.isAccountNonExpired(),user.isCredentialsNonExpired(), user.isAccountNonLocked(), user.getAuthorities());
}
(8)在 DaoAuthenticationProvider 还提供了 additionalAuthenticationChecks(…) 方法,该方法用于对比用户的其他信息,如:密码。
protected void additionalAuthenticationChecks(UserDetails userDetails,UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {// 如果用户输入的密码为null则抛出BadCredentialsException异常if (authentication.getCredentials() == null) {throw new BadCredentialsException(this.messages.getMessage("..."));}// 获取用户输入的密码String presentedPassword = authentication.getCredentials().toString();// 将用户输入的密码与系统查询到的密码对比if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {// 如果不一致也抛出BadCredentialsException异常throw new BadCredentialsException(this.messages.getMessage("..."));}
}
3.3 自动配置类
2.2.1 SpringBootWebSecurityConfiguration
默认情况下,当 SpringBoot 工程启动后,在 SpringBootWebSecurityConfiguration 配置类中向 SpringIOC 容器中注入了一个 SecurityFilterChain,该 SecurityFilterChain 的默认实现为 DefaultSecurityFilterChain,源码如下:
@Configuration(proxyBeanMethods = false) // 标注当前是配置类
@ConditionalOnWebApplication(type = Type.SERVLET) // 当前运行是是Servlet容器
class SpringBootWebSecurityConfiguration {// 如果当前IOC容器没有SecurityFilterChain对象才会生效@Configuration(proxyBeanMethods = false)@ConditionalOnDefaultWebSecuritystatic class SecurityFilterChainConfiguration {@Bean@Order(SecurityProperties.BASIC_AUTH_ORDER)SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {http.authorizeHttpRequests( // 开启认证(requests) -> requests.anyRequest() // 所有请求.authenticated() // 都必须认证);http.formLogin(withDefaults()); // 可以表单认证http.httpBasic(withDefaults()); // 可以httpBasic认证return http.build(); // 返回的是DefaultSecurityFilterChain对象}}@Configuration(proxyBeanMethods = false)// 当前IOC容器必须没有一个名为"springSecurityFilterChain"的Bean时才会生效@ConditionalOnMissingBean(name = BeanIds.SPRING_SECURITY_FILTER_CHAIN)@ConditionalOnClass(EnableWebSecurity.class)@EnableWebSecuritystatic class WebSecurityEnablerConfiguration {}
}
而在 WebSecurityConfiguration 配置类中,SpringBoot 会向 SpringIOC 容器中注入一个名字为 springSecurityFilerChain 的过滤器,该过滤器就是 FilterChainProxy 的实例对象,该 FilterChainProxy 中默认包含了一个 SecurityFilerChain(实现类为 DefaultSecurityFilterChain,该类就是在 SpringBootWebSecurityConfiguration 自动配置类中注入的那个类),该 SecurityFilerChain 中默认包含16个过滤器,这些过滤器也是 Spring Security 的核心过滤器。
我们可以翻开源码如图所示:
默认的 SecurityFilterChain 会接收客户端发送的所有请求并进入 Spring Security 的认证流程,这就是为什么在引入了 Spring Security 的场景启动器后,没有任何配置情况下,请求会被拦截的核心原因。
3.3.1 UserDetailServiceAutoConfigutation
关于 InMemoryUserDetailsManager 的配置在 UserDetailsServiceAutoConfiguration 自动配置类中,如下:
@AutoConfiguration
@ConditionalOnClass(AuthenticationManager.class)
@ConditionalOnMissingClass({ "org.springframework.security.oauth2.client.registration.ClientRegistrationRepository","org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector","org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository" })
@ConditionalOnBean(ObjectPostProcessor.class)
/*当IOC容器中没有AuthenticationManager、AuthenticationProvider、UserDetailsService、AuthenticationManagerResolver等Bean时才会生效
*/
@ConditionalOnMissingBean(value = { AuthenticationManager.class, AuthenticationProvider.class, UserDetailsService.class,AuthenticationManagerResolver.class }, type = "org.springframework.security.oauth2.jwt.JwtDecoder")
public class UserDetailsServiceAutoConfiguration {// 默认的密码前缀private static final String NOOP_PASSWORD_PREFIX = "{noop}";private static final Pattern PASSWORD_ALGORITHM_PATTERN = Pattern.compile("^\\{.+}.*$");private static final Log logger = LogFactory.getLog(UserDetailsServiceAutoConfiguration.class);@Beanpublic InMemoryUserDetailsManager inMemoryUserDetailsManager(// 从IOC容器中获取SecurityProperties、ObjectProviderSecurityProperties properties,ObjectProvider<PasswordEncoder> passwordEncoder) {// 获取SecurityProperties中配置的用户信息SecurityProperties.User user = properties.getUser();// 获取SecurityProperties中配置的权限信息List<String> roles = user.getRoles();return new InMemoryUserDetailsManager(User.withUsername(user.getName()).password(getOrDeducePassword(user, passwordEncoder.getIfAvailable())).roles(StringUtils.toStringArray(roles)).build());}// 返回加密后的密码(前提有设置加密组件)private String getOrDeducePassword(SecurityProperties.User user, PasswordEncoder encoder) {String password = user.getPassword();if (user.isPasswordGenerated()) {logger.warn(...);}if (encoder != null || PASSWORD_ALGORITHM_PATTERN.matcher(password).matches()) {return password;}return NOOP_PASSWORD_PREFIX + password;}
}
SecurityProperties 的属性配置类代码如下:
// 该属性配置类的配置前缀
@ConfigurationProperties(prefix = "spring.security")
public class SecurityProperties {.....private final Filter filter = new Filter();private final User user = new User();....public static class User {// 默认的用户名private String name = "user";// 默认的密码private String password = UUID.randomUUID().toString();// 默认的权限(空)private List<String> roles = new ArrayList<>();private boolean passwordGenerated = true;....}
}