Spring Security 认证源码超详细分析

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;....}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/413208.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

项目实战--Sa-Token详细方案笔记

Sa-Token权限系统设计 一、前言二、认证授权的概念三、Sa-Token简介3.1 Sa-Token使用方式3.2 踢人下线3.3 全局异常处理3.4 二级认证3.5 同端互斥登录3.6 Http Basic/Digest 认证3.6.1 HttpBasic认证3.6.2 Http Digest 认证 四、Sa-Token授权&#xff08;鉴权&#xff09;4.1 权…

详说 类和对象

类怎么定义 类是什么呢&#xff1f;类就是我们上篇文说的命名空间&#xff0c;单独创建一个域&#xff0c;自己有自己的生命空间&#xff0c;那么类怎么定义呢&#xff1f;C规定&#xff0c;假设 stack就是他的类名&#xff0c;那么前面要加个class&#xff0c;换行之后就是他…

汽车乘客热舒适度大挑战,如何利用仿真技术提高汽车环境舒适度

舒适性在人们选择汽车的决定性方面占比越来越重&#xff0c;而汽车乘员舱环境的舒适性是指为乘员提供舒适愉快便利的乘坐环境与条件&#xff0c;包括良好的平顺性、车内的低噪声、适宜的空气环境以及良好的驾驶操作性能。 舒适性 经济性 安全性、动力性 典型的乘员舱热舒适性模…

laravel的队列的使用

laravel队列 laravel的特性&#xff1a;laravel队列可以基于不同的后台存储服务提供统一的api&#xff0c;后台存储服务包括 Redis MySQL等。队列实现了业务解耦&#xff0c;异步处理&#xff0c;错误重试的功能。比如调用第三方api&#xff0c;无法保证api的可靠性&#xff0…

Transformer 与传统模型Informer

Transformer 与传统模型:Informer 如何改变时间序列预测的规则 Transformers 是那些聪明的注意力构建者,它们在机器学习的各个领域掀起了波澜。但在时间序列预测领域,它们才真正大显身手。你可能会问,为什么?想象一下,有一个水晶球,它不仅能看到未来,还能理解导致未来的…

TCP协议 配合 Wireshark 分析数据

在TCP连接中&#xff0c;无论是客户端还是服务端&#xff0c;都有可能成为发送端或接收端&#xff0c;这是因为TCP是一个全双工协议&#xff0c;允许数据在同一连接中双向流动 客户端&#xff08;Client&#xff09;&#xff1a;通常是指主动发起连接请求的一方。例如&#xf…

宠物空气净化器有用吗?为什么养宠家庭要买宠物空气净化器?

身为一个鼻炎患者&#xff0c;却喜欢猫咪&#xff0c;所以毅然决然的养了两只宠物&#xff0c;而且还是长毛猫&#xff0c;不要问为什么鼻炎还买两只猫咪&#xff0c;因为怕一只猫咪孤单&#xff0c;所以养了两只。对于很多人来说&#xff0c;猫咪就像焦虑不安时的精神搭子&…

如何让私域服务赢得用户的心?

私域流量的概念在当今的商业环境中已经变得极为重要&#xff0c;许多品牌和企业都投入大量资源尝试通过各种策略吸引并保留用户。然而&#xff0c;单纯的流量积累并不足以确保商业成功。当面对用户的沉默、缺乏活跃度以及无法变现的困境时&#xff0c;我们必须重新审视私域流量…

语音控制开关的语音识别ic芯片方案

语音控制开关是一种基于语音识别技术的设备&#xff0c;它通过内置的语音识别芯片&#xff0c;将用户的语音指令转化为电信号&#xff0c;从而实现对设备的控制。例如在智能家居设备上的应用&#xff0c;通常需要连接到家庭的Wi-Fi网络上&#xff0c;以便与智能手机或智能音箱等…

Java之初始泛型

1 包装类 在Java中&#xff0c;由于基本类型不是继承自Object&#xff0c;为了在泛型代码中可以支持基本类型&#xff0c;Java给每个基本类型都对应了一个包装类型。 1.1 基本数据类型和对应的包装类 基本数据类型包装类byteByteshortShortintIntegerlongLongfloatFloatdoub…

FaceFormer嘴形同步论文复现

一、项目地址 https://github.com/EvelynFan/FaceFormer 二、复现过程 1、项目环境 系统&#xff1a;Ubuntu 18.04.1 python版本&#xff1a;Python 3.7 使用conda创建一个虚拟环境&#xff0c;安装requirements.txt中所需要的库 2、安装ffmpeg 教程网址&#xff1a;http…

8个Python编程进阶常用技巧!

介绍 Python 炫酷功能&#xff08;例如&#xff0c;变量解包&#xff0c;偏函数&#xff0c;枚举可迭代对象等&#xff09;的文章层出不穷。但是还有很多 Python 的编程小技巧鲜被提及。因此&#xff0c;本文会试着介绍一些其它文章没有提到的小技巧&#xff0c;这些小技巧也是…

Selenium+Python自动化测试环境搭建

1. 什么是Selenium&#xff1f; Selenium主要用于web应用程序的自动化测试&#xff0c;但并不局限于此&#xff0c;它还支持所有基于web的管理任务自动化。 2、selenium 自动化流程如下&#xff1a; 自动化程序调用Selenium 客户端库函数&#xff08;比如点击按钮元素&#xff…

【计算机组成原理】六、总线:3.操作和定时

5.操作和定时 文章目录 5.操作和定时5.1总线传输的四个阶段5.2总线定时5.2.1同步通信5.2.2异步通信5.2.3半同步通信5.2.4分离式通信 2.3按时序控制方式 同步总线异步总线 5.1总线传输的四个阶段 总线周期&#xff1a; 申请分配阶段&#xff1a;由需要使用总线的主模块&#…

计算机组成原理:实验一运算器组成实验

一、实验目的 1.掌握算术逻辑运算加、减、乘、与的工作原理。 2.熟悉简单运算器的数据传送通路。 3.验证实验台运算器的8位加、减、与、直通功能。 4.验证实验台的4位乘4位功能。 5.按给定数据&#xff0c;完成几种指定的算术和逻辑运算。 二、实验电路 图1.1 运算器数据…

约瑟夫环和一元多项式

约瑟夫环 一、问题描述 假设有 n 个人围成一圈&#xff0c;从第一个人开始报数&#xff0c;报数到 m 的人将被淘汰出圈&#xff0c;然后从下一个人开始继续从 1 报数&#xff0c;如此重复&#xff0c;直到最后只剩下一个人。求最后剩下的这个人的编号。 二、问题分析 可…

DDR3详解

1.DDR3简介 DDR3 SDRAM&#xff0c;全称第三代双倍速率同步动态随机存取存储器&#xff0c;简称 DDR3&#xff0c;双倍速率&#xff08;double-data-rate&#xff09;&#xff0c;是指时钟的上升沿和下降沿都发生数据传输&#xff1b;同步&#xff0c;是指DDR3数据的读取写入是…

使用 nuxi build-module 命令构建 Nuxt 模块

title: 使用 nuxi build-module 命令构建 Nuxt 模块 date: 2024/8/31 updated: 2024/8/31 author: cmdragon excerpt: nuxi build-module 命令是构建 Nuxt 模块的核心工具,它将你的模块打包成适合生产环境的格式。通过使用 --stub 选项,你可以在开发过程中加快模块构建速度…

linux Vim的安装和基本使用

Vim 什么是 Vim Vim是一个高度可定制的文本编辑器&#xff0c;源自Unix系统的vi编辑器。它被广泛用于类Unix系统中&#xff0c;包括Linux、Mac OS和Windows平台。Vim特别受到程序员的青睐&#xff0c;因为它提供了丰富的编程功能&#xff0c;如代码补全、编译及错误跳转等。这…

Kubernetes 上安装 Jenkins

安装 Helm curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash添加 Jenkins Helm 仓库 首先添加 Jenkins Helm 仓库 helm repo add jenkins https://charts.jenkins.io helm repo update安装 Jenkins 使用 Helm 安装 Jenkins 的最新版本&…