当前位置: 首页 > news >正文

Spring Security6 从源码慢速开始

一、是什么

  • Spring Security 能解决什么问题?

用户身份认证(Authentication)和用户授权 (Authorization 也叫访问控制)。

认证:解决我是谁?

授权:解决我能在系统中干什么?

  • 基本原理

Spring Security 本质是一个过滤器链。但是 Spring Security 采用了有别于传统 Servlet 过滤器链的另一套实现。

左边是传统的 Servlet 过滤器链 FilterChain,右边是 Spring Security 实现的过滤器链 SecurityFilterChain。

  • FilterChain

客户端向应用程序发送请求,Servlet 容器创建一个 FilterChain,其中包含 Filters and Servlet,Filters and Servlet 根据请求 URI 的路径处理 HttpServletRequest,在 Spring MVC 应用程序中,Servlet 是 DispatcherServlet 的实例。最多一个 Servlet 可以处理单个 HttpServletRequest 和 HttpServletResponse。

  • FilterChainProxy

在 Servlet 容器中,仅根据 URL 调用 Filter。但是,FilterChainProxy 可以通过利用RequestMatcher 接口,根据 HttpServletRequest 中的任何内容(请求头、请求路径、请求参数)来确定调用。FilterChainProxy 可用于确定应该使用哪个 SecurityFilterChain。

二、快速开始

2.1、hello world

从 官网 下载一个 hello-security demo, 或者 新建一个 Spring Boot 项目,引入依赖也可以。

2.1.1、引入依赖

<parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>3.4.3</version><relativePath/>
</parent>
<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency>
</dependencies>

依赖的版本由 Spring Boot 去指定。

我当前用到的版本如下:

JDK 17

<spring-security.version>6.4.3</spring-security.version>

<spring-boot-starter-web.version>3.4.3</spring-boot-starter-web.version>

2.1.2、新建 Controller(表示我们要访问的资源)

@RestController
@RequestMapping("/res")
public class ResourceController {@RequestMapping("/echo")public String echo() {return "Hello Security!";    }
}

2.1.3、日志配置

新版 Spring Security 默认是不打印  Security Filters 过滤器,可以通过如下方式打印:

修改项目日志配置文件 logback.xml 或 logback-spring.xml

<configuration><appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"><encoder><pattern>%d{yyyy-MM-dd HH:mm:ss} - %msg%n</pattern></encoder></appender><logger name="org.springframework.security" level="DEBUG"/><root level="info"><appender-ref ref="STDOUT"/></root>
</configuration>

 将 org.springframework.security 包的日志级别设置为 DEBUG 或 TRACE

就可以在控制台看到

2025-04-09 17:15:57 - Will secure any request with filters: 
DisableEncodeUrlFilter, 
WebAsyncManagerIntegrationFilter, 
SecurityContextHolderFilter, 
HeaderWriterFilter, 
CsrfFilter, 
LogoutFilter, 
UsernamePasswordAuthenticationFilter, 
DefaultResourcesFilter, 
DefaultLoginPageGeneratingFilter, 
DefaultLogoutPageGeneratingFilter, 
BasicAuthenticationFilter, 
RequestCacheAwareFilter, 
SecurityContextHolderAwareRequestFilter, 
AnonymousAuthenticationFilter, 
ExceptionTranslationFilter, 
AuthorizationFilter

 这是 Spring Security 默认的过滤器链。

 2.1.4、访问测试

启动服务,浏览器访问测试 http://localhost:8080/res/echo

 引入 Spring Security 以后,所有对于 Spring Web 接口的访问,都需要用户名密码。

用户名 user,密码在控制台,是一个 UUID。

 输入用户名、密码登录之后,跳转回请求接口。

三、流程讲解

3.1、流程讲解

表单提交登录的认证过程如下图:

3.1.1、判断是否有权限访问资源

  1. 首先,用户向未获得授权的资源 /private(在我们例子中是 /res/echo)发出未经身份验证的请求。
  2. ​Spring Security 的 AuthorizationFilter(旧版本是 FilterSecurityInterceptor,Spring 官网图没有更新)通过抛出 AccessDeniedException 来指示未经身份验证的请求被拒绝。
  3. 由于用户未经过身份验证,因此 ExceptionTranslationFilter 启动 Start Authentication,并使用配置的 AuthenticationEntryPoint 将重定向发送到登录页面。在大多数情况下,AuthenticationEntryPoint 是 LoginUrlAuthenticationEntryPoint 的实例。
  4. 然后,浏览器将请求它重定向到的登录页面。
  5. DefaultLoginPageGeneratingFilter#generateLoginPageHtml 生成登录页内容返回给浏览器。

3.1.2、用户提交用户名密码后认证

  1. 当用户提交他们的用户名和密码时,UsernamePasswordAuthenticationFilter#attemptAuthentication 方法通过从 HttpServletRequest 实例中提取用户名和密码来创建 UsernamePasswordAuthenticationToken(这是一种 Authentication 类型。Authentication 接口表示认证信息。可以是用户名密码、匿名访问,也可以是短信验证、二维码、指纹。具体取决于你自己的认证方式,由自己扩展)。
  2. 接下来,UsernamePasswordAuthenticationToken 被传递到 AuthenticationManager 实例ProviderManager#authenticate 中进行身份验证。AuthenticationManager 的处理细节取决于用户信息的存储方式(即 Authentication 的类型。比如,UsernamePasswordAuthenticationToken 由 ProviderManager 委托给DaoAuthenticationProvider 处理,AnonymousAuthenticationToken 委托给 AnonymousAuthenticationProvider 处理,用户也可以自己定义一个 QRCodeAuthenticationToken 表示二维码认证信息,再自己定义一个 QRCodeAuthenticationProvider 来处理)。
  3. 如果身份验证失败,则失败。
  1.  SecurityContextHolder 被清除。
  2. 调用 RememberMeServices.loginFail。如果未配置 Remember me,则没有任何操作。
  3. AuthenticationFailureHandler 被调用。

     4.如果身份验证成功,则成功。

  1. SessionAuthenticationStrategy 收到新登录的通知。
  2. Authentication 在 SecurityContextHolder上设置。AbstractAuthenticationProcessingFilter#successfulAuthentication 中设置。
  3. 调用 RememberMeServices.loginSuccess。如果未配置 Remember me,则没有任何操作。
  4. ApplicationEventPublisher 发布 InteractiveAuthenticationSuccessEvent。
  5. 调用 AuthenticationSuccessHandler。通常,这是一个 SimpleUrlAuthenticationSuccessHandler,当我们重定向到登录页面时,它会重定向到 ExceptionTranslationFilter 保存的请求。

3.1.3、身份验证成功后

身份验证成功后,Spring Security 帮我做了一些重要的事情。

UsernamePasswordAuthenticationFilter 继承自 AbstractAuthenticationProcessingFilter。

AbstractAuthenticationProcessingFilter 在 doFilter 方法中调用 UsernamePasswordAuthenticationFilter#attemptAuthentication 方法进行身份认证,认证成功后 AbstractAuthenticationProcessingFilter 会调用自身的 successfulAutentication 方法对返回的 Authentication 进行处理。此时的 Authentication 已包含经过认证的用户信息且 isAuthenticated=true。

我们来重点看一下 AbstractAuthenticationProcessingFilter#successfulAutentication 会做哪些事情。

protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,Authentication authResult) throws IOException, ServletException {// 1、创建一个空的 SecurityContext 上下文SecurityContext context = this.securityContextHolderStrategy.createEmptyContext();// 2、将当前认证过的用户信息保存到 SecurityContext 上下文context.setAuthentication(authResult);// 3、将 SecurityContext 上下文存放到 ThreadLocal 中this.securityContextHolderStrategy.setContext(context);// 4、将 SecurityContext 上下文也存放在 HttpRequest、HttpSession 中。this.securityContextRepository.saveContext(context, request, response);if (this.logger.isDebugEnabled()) {this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", authResult));}// 5、调用 RememberMeServices.loginSuccessthis.rememberMeServices.loginSuccess(request, response, authResult);if (this.eventPublisher != null) {// 6、ApplicationEventPublisher 发布 InteractiveAuthenticationSuccessEventthis.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));}// 7、调用 AuthenticationSuccessHandlerthis.successHandler.onAuthenticationSuccess(request, response, authResult);
}

 1、创建一个空的 SecurityContext 上下文。

 2、将当前认证过的用户信息保存到 SecurityContext 上下文。

请求经过 UsernamePasswordAuthenticationFilter 认证、并将认证结果保存到 SecurityContext 上下文之后。

请求会继续经过 ExceptionTranslationFilter 到达 AuthorizationFilter。AuthorizationFilter 会对 SecurityContext 上下文中的 Authentication 进行判断,如果 isAuthenticated=true。过滤器继续往下;否则抛出 AccessDeniedException("Access Denied")。抛出的 AccessDeniedException 异常会被 ExceptionTranslationFilter 捕获到,进行重定向。

//-------------- AuthorizationFilter 过滤器
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws ServletException, IOException {HttpServletRequest request = (HttpServletRequest)servletRequest;HttpServletResponse response = (HttpServletResponse)servletResponse;if (this.observeOncePerRequest && this.isApplied(request)) {chain.doFilter(request, response);} else if (this.skipDispatch(request)) {chain.doFilter(request, response);} else {String alreadyFilteredAttributeName = this.getAlreadyFilteredAttributeName();request.setAttribute(alreadyFilteredAttributeName, Boolean.TRUE);try {AuthorizationDecision decision = this.authorizationManager.check(this::getAuthentication, request);this.eventPublisher.publishAuthorizationEvent(this::getAuthentication, request, decision);// 判断是否通过认证if (decision != null && !decision.isGranted()) {// 没有抛出异常throw new AccessDeniedException("Access Denied");}chain.doFilter(request, response);} finally {request.removeAttribute(alreadyFilteredAttributeName);}}}private Authentication getAuthentication() {// 从上下文中拿到认证信息Authentication authentication = this.securityContextHolderStrategy.getContext().getAuthentication();if (authentication == null) {throw new AuthenticationCredentialsNotFoundException("An Authentication object was not found in the SecurityContext");} else {return authentication;}}
//--------------- ExceptionTranslationFilter 过滤器
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {try {chain.doFilter(request, response);} catch (IOException var7) {// 捕获 AuthorizationFilter 抛出的异常IOException ex = var7;throw ex;} catch (Exception var8) {Exception ex = var8;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);}if (securityException == null) {this.rethrow(ex);}if (response.isCommitted()) {throw new ServletException("Unable to handle the Spring Security Exception because the response is already committed.", ex);}this.handleSpringSecurityException(request, response, chain, (RuntimeException)securityException);}}

 3、将 SecurityContext 上下文存放到 ThreadLocal 中。

SecurityContextHolder 在初始化的时候,如果我们没有配置 spring.security.strategy 属性,Spring Security 默认会帮我们初始化一个 ThreadLocalSecurityContextHolderStrategy。

ThreadLocalSecurityContextHolderStrategy 持有一个 ThreadLocal,用来存放当前请求线程的 SecurityContext 上下文。

final class ThreadLocalSecurityContextHolderStrategy implements SecurityContextHolderStrategy {private static final ThreadLocal<Supplier<SecurityContext>> contextHolder = new ThreadLocal<>();
}

4、将 SecurityContext 上下文也存放在 HttpRequest、HttpSession 中。

存放到 HttpSession attribute key 为 "SPRING_SECURITY_CONTEXT"。

存放到 HttpRequest attribute key 为 "org.springframework.security.web.context.RequestAttributeSecurityContextRepository.SPRING_SECURITY_CONTEXT"。

  • 基于 Session 的有状态应用

Spring Security 是可以实现有状态的应用的。这个是基于 Session 来实现的。

当用户提交用户名密码认证成功后,Spring Security 会将用户认证信息 Authentication 保存到 SecurityContext 中,并将 SecurityContext 设置到 Session 的 Attribute 中,下一次请求进来,从 Session 中拿到 Authentication 进行校验,因为 isAuthenticated=true,所以免认证就可以访问。

1、当我们请求 Security 默认登录页

http://localhost:8080/login

当用户首次访问启用了会话管理功能的 Web 页面时,Web 服务器会为该用户创建一个新的会话 Session,并生成一个唯一的 JSESSIONID。这个 JSESSIONID 会通过 HTTP 响应头中的 Set-Cookie 字段发送到客户端(浏览器),浏览器会将其存储为一个名为 JSESSIONID 的 cookie。后续,当用户在该网站的不同页面之间进行浏览或发送请求时,浏览器会自动在 HTTP 请求头中包含这个 JSESSIONID,Web 服务器接收到请求后,通过解析这个 JSESSIONID 来找到与之对应的会话数据 Session,从而识别用户身份并维护会话状态,比如保存用户的登录信息。

2、当我们输入用户名/密码 登录后,在浏览器输入一个资源地址

http://localhost:8080/res/echo

Spring Security 从 HttpSession 获取 SecurityContext 上下文,该上下文包含了用户的已认证信息 Authentication。

HttpSessionSecurityContextRepository#readSecurityContextFromSession

5、调用 RememberMeServices.loginSuccess。如果未配置 Remember me,则没有任何操作。

6、ApplicationEventPublisher 发布 InteractiveAuthenticationSuccessEvent。

7、调用 AuthenticationSuccessHandler(Spring Security 扩展点)

3.2、组件介绍

到此,我们先小结一下,介绍一下几个组件。

3.2.1、Authentication 认证信息接口

Authentication 对象在 Spring Security 中有两个主要目的:

1)封装用户身份验证的凭证
作为 AuthenticationManager 的输入,用于封装(用户提供的)用于身份验证的凭证。在这种情况下使用时,isAuthenticated()返回 false。可以是用户名密码,也可以是短信验证、二维码、指纹。具体取决于你自己的认证方式,由自己扩展。

2)表示当前经过身份验证的用户。

当前用户的 Authentication 可以从 SecurityContext 获得。

SecurityContextHolder.getContext().getAuthentication()

Authentication 对象包含:

  • principal - 主体,用来标识用户。使用用户名/密码进行身份验证时,这通常是 UserDetails 的实例。
  • credentials - 通常是密码。在许多情况下,这将在用户进行身份验证后被清除,以确保它不会被泄露。
  • authorities - GrantedAuthorities 是授予用户的高级权限。
  • isAuthenticated - 表示用户信息经过 AuthenticationManager#authenticate 认证后,是否通过认证。true 通过;false 不通过。

3.2.2、AuthenticationManager 认证管理器接口(Spring Security 扩展点)

@FunctionalInterface
public interface AuthenticationManager {// 定义如何执行身份验证Authentication authenticate(Authentication authentication) throws AuthenticationException;
}

AuthenticationManager 是定义如何执行身份验证的 API。由调用 AuthenticationManager 的控制器(即 Spring Security Filters 实例,比如 UsernamePasswordAuthenticationFilter)在SecurityContextHolder 上设置返回的 Authentication。
虽然 AuthenticationManager 的实现可以是任何内容,但最常见的实现是 ProviderManager。

  • 怎么知道 AuthenticationManager 的默认实现是 ProviderManager?

3.2.3、ProviderManager

ProviderManager 是 AuthenticationManager 最常用的实现。ProviderManager 会将任务委托给一个 AuthenticationProvider 实例列表。每个 AuthenticationProvider 都有机会表明认证应该成功、失败,或者表明它无法做出决定,并允许下游的 AuthenticationProvider 来做决定。如果配置的 AuthenticationProvider 实例都无法进行认证,认证将以 ProviderNotFoundException 失败,这是一个特殊的 AuthenticationException,表明 ProviderManager 未配置支持传入的 Authentication 类型的 AuthenticationProvider。

这句话的意思是,ProviderManager 中存在一个 AuthenticationProvider 数组。请求被 ProviderManager 委托给 AuthenticationProvider 列表进行真正的认证处理。

public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {private List<AuthenticationProvider> providers = Collections.emptyList();
}

 

ProviderManager 中调用 AuthenticationProvider 伪代码如下:

public Authentication authenticate(Authentication authentication) {Class<? extends Authentication> toTest = authentication.getClass();Authentication result = null;Authentication parentResult = null;for (AuthenticationProvider provider : this.getProviders()) {// 遍历 provider 列表// 判断当前 provider 是否有能力处理给定 token 类型的认证// 不行就交给下游 provider 处理 if (!provider.supports(toTest)) {continue;}// 调用 provider#authenticate 接口进行身份认证result = provider.authenticate(authentication);}if (result == null && this.parent != null) {// 如果当前 AuthenticationManager 都无法处理此种类型的 token// 则委托给它的 parent 处理// parent 其实也是委托给它持有的 providers 列表处理try {parentResult = this.parent.authenticate(authentication);result = parentResult;}// 如果最后连 parent 都处理不了// 抛出 AuthenticationException 异常}
}

3.2.4、AuthenticationProvider 认证服务提供者接口(Spring Security 扩展点)

AuthenticationProvider 是真正执行认证逻辑的接口。

你可以将多个 AuthenticationProvider 实例注入到 ProviderManager 中。每个 AuthenticationProvider 执行一种特定类型的认证。例如,DaoAuthenticationProvider 支持基于用户名/密码的认证,处理 UsernamePasswordAuthenticationToken。AnonymousAuthenticationProvider 支持不需要认证的匿名访问,处理 AnonymousAuthenticationToken。

每一个 AuthenticationProvider 都定义了两个方法,一个是认证处理逻辑;一个是声明它支持的 token 类型。用户也可以自己定义一个 QRCodeAuthenticationToken 表示二维码认证信息,再自己定义一个 QRCodeAuthenticationProvider supports 这个 Token 来处理。

public interface AuthenticationProvider {// 定义认证逻辑Authentication authenticate(Authentication authentication) throws AuthenticationException;// 声明支持的 token 类型boolean supports(Class<?> authentication);
}
-----------------------------------------
public abstract class AbstractUserDetailsAuthenticationProviderimplements AuthenticationProvider, InitializingBean, MessageSourceAware {@Overridepublic boolean supports(Class<?> authentication) {return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication));}
}
-----------------------------------------
public class AnonymousAuthenticationProvider implements AuthenticationProvider, MessageSourceAware {@Overridepublic boolean supports(Class<?> authentication) {return (AnonymousAuthenticationToken.class.isAssignableFrom(authentication));}
}

3.2.5、双亲委派

如果你将断点打在 AuthenticationManagerBuilder#performBuild 方法中,会发现它跑两次。

AuthenticationManagerBuilder 是用来构造 AuthenticationManager 的构造器,它跑了两次,是因为它构造了两个 ProviderManager,形成父子关系。

ProviderManager 还允许配置一个可选的父级 AuthenticationManager,如果没有任何 AuthenticationProvider 能够执行认证,就会委派给该父级管理器(正如前面伪代码分析的那样)。父级可以是任何类型的 AuthenticationManager,但通常是一个 ProviderManager 实例 。

如果不自己配置,Spring Security 默认会给我们构造出如下图的一个 ProviderManager 父子关系:

之所以用双亲委派,其实就是定义一种兜底的方案。用户名/密码的认证是一种最常见的认证方式,把它放在父 ProviderManager 中,可以作为共享的认证方式。

实际上,多个 ProviderManager 实例可能会共享同一个父级 AuthenticationManager。在存在多个 SecurityFilterChain 实例且这些实例具有一些共同的认证(共享的父级 AuthenticationManager),但也有不同的认证机制(不同的 ProviderManager 实例)的情况下,这种情况有点常见 。

 3.2.6、DaoAuthenticationProvider 用户名/密码认证服务提供者实现

DaoAuthenticationProvider 是一个 AuthenticationProvider 实现,它使用 UserDetailsService 和 PasswordEncoder 对用户名和密码进行身份验证。

  1. 身份验证过滤器 UsernamePasswordAuthenticationFilter 将 UsernamePasswordAuthenticationToken 传递给 AuthenticationManager,AuthenticationManager 是由 ProviderManager 实现的。
  2. ProviderManager 配置为使用 DaoAuthenticationProvider 类型的 AuthenticationProvider。
  3. DaoAuthenticationProvider 从 UserDetailsService 中查找 UserDetails。
  4. DaoAuthenticationProvider 使用 PasswordEncoder 对上一 步返回的 UserDetails 中的密码进行验证。
  5. 身份验证成功后,返回的 Authentication 为 UsernamePasswordAuthenticationToken 类型,并且具有一个主体 principal,该 principal 是 UserDetailsService 返回的 UserDetails。最终,返回的 UsernamePasswordAuthenticationToken 由身份验证过滤器 UsernamePasswordAuthenticationFilter 在 SecurityContextHolder 上设置。
  •  DaoAuthenticationProvider 怎么加入到 ProviderManager 的 providers 数组的?

  • DaoAuthenticationProvider 持有 UserDetailsService 和 PasswordEncoder
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {private PasswordEncoder passwordEncoder;private UserDetailsService userDetailsService;
}
  • InitializeUserDetailsBeanManagerConfigurer$InitializeUserDetailsManagerConfigurer
class InitializeUserDetailsBeanManagerConfigurer extends GlobalAuthenticationConfigurerAdapter {// 容器对象private final ApplicationContext context;class InitializeUserDetailsManagerConfigurer extends GlobalAuthenticationConfigurerAdapter {public void configure(AuthenticationManagerBuilder auth) throws Exception {// 从容器中获取 UserDetailsService 和 PasswordEncoderUserDetailsService userDetailsService = (UserDetailsService)InitializeUserDetailsBeanManagerConfigurer.this.context.getBean(beanNames[0],UserDetailsService.class);PasswordEncoder passwordEncoder = (PasswordEncoder)this.getBeanOrNull(PasswordEncoder.class);DaoAuthenticationProvider provider;if (passwordEncoder != null) {// 如果你有提供了,走这里provider = new DaoAuthenticationProvider(passwordEncoder);} else {// 如果没有提供,走这里provider = new DaoAuthenticationProvider();}provider.setUserDetailsService(userDetailsService);}}private <T> T getBeanOrNull(Class<T> type) {return InitializeUserDetailsBeanManagerConfigurer.this.context.getBeanProvider(type).getIfUnique();}
}

PasswordEncoder 和 UserDetailsService 都是从 IOC 容器中获取的,这为后续扩展提供了一种可能。Spring Security 它可以配置一个默认的实现,但是这个默认的实现以条件注解的方式注入 ioc 容器,如果我们提供了自定义实现,那么默认的实现就不注入。

3.2.7、UserDetailsService 用户详情加载服务接口(Spring Security 扩展点)

public interface UserDetailsService {// 它里面只有一个 根据用户名加载 UserDetails 的接口// UserDetails 可以把它理解为 表示用户信息的 接口UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

UserDetailsService 由 DaoAuthenticationProvider 用于检索用户名、密码和其他属性,以便使用用户名和密码进行身份验证。Spring Security 提供了 UserDetailsService 的内存(InMemoryUserDetailsManager)、JDBC(JdbcUserDetailsManager)和缓存(CachingUserDetailsService)实现。JDBC 的实现,用户的所有表结构,都要按照它的来,实际很少使用。

  • 怎么确定 UserDetailsService 默认实现是 InMemoryUserDetailsManager?

在 Spring Boot 自动装配导入的类中,有一个 UserDetailsServiceAutoConfiguration

它通过条件注解的方式注入了 InMemoryUserDetailsManager

@ConditionalOnMissingBean(// 这四个都是 Spring Security 的扩展点// 如果你自己定义了,那就用你的value = {AuthenticationManager.class, AuthenticationProvider.class, UserDetailsService.class, AuthenticationManagerResolver.class}
)

只有当 AuthenticationManager.class, AuthenticationProvider.class, UserDetailsService.class, AuthenticationManagerResolver.class 类型的 bean 都不在 Spring应用上下文中存在时,UserDetailsServiceAutoConfiguration 才会被创建。如果其中任意一个类型已经有一个 bean 存在于上下文中,则 UserDetailsServiceAutoConfiguration 将不会被创建。

  • InMemoryUserDetailsManager 初始化用的是配置是 SecurityProperties

// 从配置文件获取
@ConfigurationProperties(prefix = "spring.security")
public class SecurityProperties {public static class User {private String name = "user";private String password = UUID.randomUUID().toString();private List<String> roles = new ArrayList();private boolean passwordGenerated = true;}
}

这也解释了,为什么 Spring Security 默认用户名是 user,而它的密码是 UUID.

3.2.8、PasswordEncoder 密码编码接口(Spring Security 扩展点)

public interface PasswordEncoder {// 密码加密String encode(CharSequence rawPassword);// 密码匹配boolean matches(CharSequence rawPassword, String encodedPassword);default boolean upgradeEncoding(String encodedPassword) {return false;}
}

Spring Security 的 PasswordEncoder 接口用于执行密码的单向转换,以使密码被安全地存储。通常,PasswordEncoder 用于存储需要在身份验证时与用户提供的密码进行比较的密码。

  •  怎么确定 PasswordEncoder 默认实例是 NoOpPasswordEncoder?

还记得 DaoAuthenticationProvider 么?在InitializeUserDetailsBeanManagerConfigurer$InitializeUserDetailsManagerConfigurer 初始化 DaoAuthenticationProvider 时

class InitializeUserDetailsBeanManagerConfigurer extends GlobalAuthenticationConfigurerAdapter {// 容器对象private final ApplicationContext context;class InitializeUserDetailsManagerConfigurer extends GlobalAuthenticationConfigurerAdapter {public void configure(AuthenticationManagerBuilder auth) throws Exception {UserDetailsService userDetailsService = (UserDetailsService)InitializeUserDetailsBeanManagerConfigurer.this.context.getBean(beanNames[0],UserDetailsService.class);// 从 IOC 容器中查找PasswordEncoder passwordEncoder = (PasswordEncoder)this.getBeanOrNull(PasswordEncoder.class);DaoAuthenticationProvider provider;if (passwordEncoder != null) {// 如果你有提供了,走这里provider = new DaoAuthenticationProvider(passwordEncoder);} else {// 如果没有提供,走这里provider = new DaoAuthenticationProvider();}provider.setUserDetailsService(userDetailsService);}}private <T> T getBeanOrNull(Class<T> type) {return InitializeUserDetailsBeanManagerConfigurer.this.context.getBeanProvider(type).getIfUnique();}
}

如果你在配置中提供了 PasswordEncoder,Spring Security 会用你提供的 PasswordEncoder,如果没有,它调用无参的构造函数来构造 DaoAuthenticationProvider。

public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {public DaoAuthenticationProvider() {this(PasswordEncoderFactories.createDelegatingPasswordEncoder());}
}

无参构造函数调用密码编码工厂 PasswordEncoderFactories,生成一个 DelegatingPasswordEncoder 它类似一个 Map 结构:

public final class PasswordEncoderFactories {public static PasswordEncoder createDelegatingPasswordEncoder() {String encodingId = "bcrypt";Map<String, PasswordEncoder> encoders = new HashMap();encoders.put(encodingId, new BCryptPasswordEncoder());encoders.put("ldap", new LdapShaPasswordEncoder());encoders.put("MD4", new Md4PasswordEncoder());encoders.put("MD5", new MessageDigestPasswordEncoder("MD5"));encoders.put("noop", NoOpPasswordEncoder.getInstance());encoders.put("pbkdf2", Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_5());encoders.put("pbkdf2@SpringSecurity_v5_8", Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8());encoders.put("scrypt", SCryptPasswordEncoder.defaultsForSpringSecurity_v4_1());encoders.put("scrypt@SpringSecurity_v5_8", SCryptPasswordEncoder.defaultsForSpringSecurity_v5_8());encoders.put("SHA-1", new MessageDigestPasswordEncoder("SHA-1"));encoders.put("SHA-256", new MessageDigestPasswordEncoder("SHA-256"));encoders.put("sha256", new StandardPasswordEncoder());encoders.put("argon2", Argon2PasswordEncoder.defaultsForSpringSecurity_v5_2());encoders.put("argon2@SpringSecurity_v5_8", Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8());return new DelegatingPasswordEncoder(encodingId, encoders);}
}

然后,还记得 InMemoryUserDetailsManager 吗?

 在 Spring Boot 自动装配导入的类中,有一个 UserDetailsServiceAutoConfiguration

它通过条件注解的方式注入了 InMemoryUserDetailsManager

这里在生成 InMemoryUserDetailsManager 实例的时候,给默认用户 user 密码做了一次加工,当没有 PasswordEncoder 的时候,在密码前面加上 {noop},最后生成的密码类似 

{noop}c0d16efe-9468-487a-8132-5bd3988ff030

最后,在 DaoAuthenticationProvider 进行身份认证的时候

调用 DelegatingPasswordEncoder matches

从 prefixEncodedPassword  {noop}c0d16efe-9468-487a-8132-5bd3988ff030 解析出 noop,

然后用 noop 从 DelegatingPasswordEncoder 的 map 中拿到 NoOpPasswordEncoder,

去掉 prefixEncodedPassword 的前缀 {noop},与用户传进来的密码 rawPassword 进行匹配。

NoOpPasswordEncoder 的匹配逻辑是,不做加工,原样匹配。它还是一个单例类。

public final class NoOpPasswordEncoder implements PasswordEncoder {private static final PasswordEncoder INSTANCE = new NoOpPasswordEncoder();private NoOpPasswordEncoder() {}public String encode(CharSequence rawPassword) {return rawPassword.toString();}public boolean matches(CharSequence rawPassword, String encodedPassword) {// 不做加工,原样匹配return rawPassword.toString().equals(encodedPassword);}public static PasswordEncoder getInstance() {return INSTANCE;}
}

    四、自定义用户名密码

    4.1、使用 application.yml

    spring:application:name: hello-securitysecurity:user:name: adminpassword: 123456roles:- admin

     重启服务,使用 admin/123456 也能登录成功。

    4.2、使用 Java Bean 配置方式

    • 方式一:User.UserBuilder
    @Configuration
    @EnableWebSecurity // 开启 spring security
    public class SecurityConfig {@Beanpublic UserDetailsService users() {// 使用默认加密方式 bcrypt 对密码进行加密,添加用户信息User.UserBuilder users = User.withDefaultPasswordEncoder();UserDetails user = users.username("user").password("123456").roles("USER").build();UserDetails admin = users.username("admin").password("123456").roles("USER", "ADMIN").build();return new InMemoryUserDetailsManager(user, admin);}
    }
    • 方式二:{id}encodedPassword

    Spring Security 密码加密格式为:{id}encodedPassword

    @Configuration
    @EnableWebSecurity // 开启 spring security
    public class SecurityConfig {@Beanpublic UserDetailsService users() {UserDetails user = User.builder().username("user").password("{bcrypt}$2a$10$9ItJHL.xW70xCcX79dj4lObvRXK9dOyRe1xJPFhanapE5hbwqeYl2").roles("USER").build();UserDetails admin = User.builder().username("admin").password("{bcrypt}$2a$10$Q.28EEhF2WEvc4d4311Xj.WyeGlm9y3AMz8Fh60rjNMZOAFZ0br9y").roles("USER", "ADMIN").build();return new InMemoryUserDetailsManager(user, admin);}
    }

     方式三:自定义 PasswordEncoder

    @Configuration
    @EnableWebSecurity // 开启 spring security
    public class SecurityConfig {@Beanpublic UserDetailsService users() {UserDetails user = User.builder().username("user").password(passwordEncoder().encode("123456")).roles("USER").build();UserDetails admin = User.builder().username("admin").password(passwordEncoder().encode("123456")).roles("USER", "ADMIN").build();return new InMemoryUserDetailsManager(user, admin);}@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}
    }

     重启服务,这三种方式用 user/123456、admin/123456 都可以登录。

    • 为什么 Spring Security 使用 BCryptPasswordEncoder 做为默认加密方式?

    1、单向加密性

    public class Test {public static void main(String[] args) {BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();String aa = encoder.encode("123456");String bb = encoder.encode("123456");System.out.println(aa);System.out.println(bb);}
    }
    // 对于同一个密码,每次加密输出的都不相同
    $2a$10$9ItJHL.xW70xCcX79dj4lObvRXK9dOyRe1xJPFhanapE5hbwqeYl2
    $2a$10$Q.28EEhF2WEvc4d4311Xj.WyeGlm9y3AMz8Fh60rjNMZOAFZ0br9y

    五、自定义用户信息加载方式

    实现 UserDetailsService 接口,自定义从数据库获取用户信息。

    • SecurityConfig
    @Configuration
    @EnableWebSecurity // 开启 spring security
    public class SecurityConfig {@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}
    }
    • DbUserDetailsService
    @Service
    public class DbUserDetailsService implements UserDetailsService {@Autowiredprivate PasswordEncoder passwordEncoder;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {// 用 map 模拟从数据库获取用户信息,并封装成 UserDetails 对象Map<String, User> map = new HashMap<>();User user1 = new User();user1.setUsername("user");user1.setPassword(passwordEncoder.encode("123456")); // 数据库里面要存密文user1.setRoles("USER");map.put("user", user1);User user2 = new User();user2.setUsername("admin");user2.setPassword(passwordEncoder.encode("123456"));user2.setRoles("USER", "ADMIN");map.put("admin", user2);// 查询User user = map.get(username);// 封装成 UserDetails 对象返回return org.springframework.security.core.userdetails.User.builder().username(user.getUsername()).password(user.getPassword()).roles(user.getRoles()).build();}public static class User {private String username;private String password;private final List<String> roles = new ArrayList<>();public String getUsername() {return username;}public void setUsername(String username) {this.username = username;}public String getPassword() {return password;}public void setPassword(String password) {this.password = password;}public String[] getRoles() {return roles.toArray(String[]::new);}public void setRoles(String... roles) {this.roles.addAll(Arrays.asList(roles));}}
    }

    六、自定义登录页面

    在讲解自定义登录页面之前,有必要讲解两个比较重要的组件:SecurityFilterChain 和 HttpSecurity。

    6.1、组件介绍

    6.1.1、SecurityFilterChain Security 过滤器链接口(Spring Security 扩展点)

    我们开头讲过 Spring Security 本质是一个过滤器链。这个过滤器链就是 SecurityFilterChain。

    SecurityFilterChain 有一个 Filter 列表:

    public interface SecurityFilterChain {// 定义请求是否需要交由该过滤器链处理boolean matches(HttpServletRequest request);// 获取过滤器列表List<Filter> getFilters();
    }
    ------------------------------------------------
    // DefaultSecurityFilterChain 是接口 SecurityFilterChain 唯一默认实现
    public final class DefaultSecurityFilterChain implements SecurityFilterChain, BeanNameAware, BeanFactoryAware {// 持有过滤器列表private final List<Filter> filters;// ...... 其他属性
    }

     它里面的过滤器,就是类似于我们之前打印出来的:

    2025-04-09 17:15:57 - Will secure any request with filters: 
    DisableEncodeUrlFilter, 
    WebAsyncManagerIntegrationFilter, 
    SecurityContextHolderFilter, 
    HeaderWriterFilter, 
    CsrfFilter, 
    LogoutFilter, 
    UsernamePasswordAuthenticationFilter, 
    DefaultResourcesFilter, 
    DefaultLoginPageGeneratingFilter, 
    DefaultLogoutPageGeneratingFilter, 
    BasicAuthenticationFilter, 
    RequestCacheAwareFilter, 
    SecurityContextHolderAwareRequestFilter, 
    AnonymousAuthenticationFilter, 
    ExceptionTranslationFilter, 
    AuthorizationFilter

    可是我们之前并没有自己定义 SecurityFilterChain 或者这些过滤器,那它们是怎么来的呢?

    •  默认 SecurityFilterChain 初始化

     在 Spring Boot 自动装配导入的类中,有一个 SecurityAutoConfiguration

    SecurityAutoConfiguration 导入了 SpringBootWebSecurityConfiguration

    在 SpringBootWebSecurityConfiguration 初始化的时候,会往容器中生成一个 SecurityFilterChain 的实例 DefaultSecurityFilterChain。

    @Configuration(proxyBeanMethods = false)
    @ConditionalOnWebApplication(type = Type.SERVLET)
    class SpringBootWebSecurityConfiguration {@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());// 生成 DefaultSecurityFilterChainreturn http.build();}}
    }

    请注意 @ConditionalOnDefaultWebSecurity 这个条件注解:

    @ConditionalOnDefaultWebSecurity 这个条件注解依赖于 DefaultWebSecurityCondition 这个条件类。

    DefaultWebSecurityCondition 条件类包含两个静态内部类:

    • Classes: 这个内部类使用了 @ConditionalOnClass 注解,表示只有当 SecurityFilterChain.class 和 HttpSecurity.class 类存在于类路径上时,才会满足这个条件。
    • Beans: 这个内部类使用了 @ConditionalOnMissingBean 注解,表示只有当 Spring 容器中不存在 SecurityFilterChain.class 类型的 Bean 时,才会满足这个条件。

    这两个条件是通过 AllNestedConditions 组合在一起的,默认情况下需要同时满足所有嵌套的条件才能使整个 DefaultWebSecurityCondition 满足。这意味着,只有当 SecurityFilterChain 和 HttpSecurity 类存在,并且 Spring 容器中没有 SecurityFilterChain 类型的 Bean 对象时, @ConditionalOnDefaultWebSecurity 被这个注解的配置才会被启用。SecurityFilterChain defaultSecurityFilterChain 才会被注入到 IOC 容器中。这是 Spring Security 的一个扩展点


    如果我们项目中配置了 SecurityFilterChain,则 Spring Security 默认的 SecurityFilterChain 配置就不会生效。


     
    @Target({ ElementType.TYPE, ElementType.METHOD })
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Conditional(DefaultWebSecurityCondition.class)
    public @interface ConditionalOnDefaultWebSecurity {}
    ----------------------------------------------------------------
    // DefaultWebSecurityCondition
    class DefaultWebSecurityCondition extends AllNestedConditions {DefaultWebSecurityCondition() {super(ConfigurationPhase.REGISTER_BEAN);}@ConditionalOnClass({ SecurityFilterChain.class, HttpSecurity.class })static class Classes {}@ConditionalOnMissingBean({ SecurityFilterChain.class })static class Beans {}
    }
      •  默认的 Security Filters 是怎么加入到 SecurityFilterChain 的列表 filters 中的?

      首先,HttpSecurityConfiguration 生成 HttpSecurity 实例的时候,往 HttpSecurity 的 LinkedHashMap configurers(该属性继承自 AbstractConfiguredSecurityBuilder) 加入13个 Configurer。这些 Configurer 是对应 Filter 的装配器。

      @Configuration(proxyBeanMethods = false)
      class HttpSecurityConfiguration {@Bean(HTTPSECURITY_BEAN_NAME)@Scope("prototype")HttpSecurity httpSecurity() throws Exception {LazyPasswordEncoder passwordEncoder = new LazyPasswordEncoder(this.context);AuthenticationManagerBuilder authenticationBuilder = new DefaultPasswordEncoderAuthenticationManagerBuilder(this.objectPostProcessor, passwordEncoder);authenticationBuilder.parentAuthenticationManager(authenticationManager());authenticationBuilder.authenticationEventPublisher(getAuthenticationEventPublisher());HttpSecurity http = new HttpSecurity(this.objectPostProcessor, authenticationBuilder, createSharedObjects());WebAsyncManagerIntegrationFilter webAsyncManagerIntegrationFilter = new WebAsyncManagerIntegrationFilter();webAsyncManagerIntegrationFilter.setSecurityContextHolderStrategy(this.securityContextHolderStrategy);http// 加入 CsrfConfigurer.csrf(withDefaults()).addFilter(webAsyncManagerIntegrationFilter)// 加入 ExceptionHandlingConfigurer.exceptionHandling(withDefaults())// 加入 HeadersConfigurer.headers(withDefaults())// 加入 SessionManagementConfigurer.sessionManagement(withDefaults())// 加入 SecurityContextConfigurer.securityContext(withDefaults())// 加入 RequestCacheConfigurer.requestCache(withDefaults())// 加入 AnonymousConfigurer.anonymous(withDefaults())// 加入 ServletApiConfigurer.servletApi(withDefaults())// 加入 DefaultLoginPageConfigurer.apply(new DefaultLoginPageConfigurer<>());// 加入 LogoutConfigurerhttp.logout(withDefaults());// 不加入 CorsConfigurerapplyCorsIfAvailable(http);// 不加入 AbstractHttpConfigurerapplyDefaultConfigurers(http);return http;}
      }

      然后在 Spring Boot 自动装配导入的类中,有一个 SecurityAutoConfiguration

      SecurityAutoConfiguration 导入了 SpringBootWebSecurityConfiguration

      在 SpringBootWebSecurityConfiguration 生成 SecurityFilterChain 实例的时候,会调用 HttpSecurity 的 build 方法,挨个调用 Configurer 生成 13 个 Filter,加入到 HttpSecurity 的 List<OrderedFilter> filters 里面。

      @Configuration(proxyBeanMethods = false)
      @ConditionalOnWebApplication(type = Type.SERVLET)
      class SpringBootWebSecurityConfiguration {@Configuration(proxyBeanMethods = false)@ConditionalOnDefaultWebSecuritystatic class SecurityFilterChainConfiguration {@Bean@Order(SecurityProperties.BASIC_AUTH_ORDER)SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {// 加入 AuthorizeHttpRequestsConfigurerhttp.authorizeHttpRequests((requests) -> requests.anyRequest().authenticated());// 加入 FormLoginConfigurerhttp.formLogin(withDefaults());// 加入 HttpBasicConfigurerhttp.httpBasic(withDefaults());// 到此,13个 Configurer 加入结束return http.build();}}
      }

      这里单看一个 CsrfConfigurer,其他类似

      最后,在生成 SecurityFilterChain 的实现 DefaultSecurityFilterChain 的时候,把 HttpSecurity 持有的 filters 包装一下,传递给 DefaultSecurityFilterChain。

      HttpSecurity.filters --> DefaultSecurityFilterChain.filters


        从这可以看出,HttpSecurity 是 SecurityFilterChain 的装配流水线。我们可以给 HttpSecurity 配置各种过滤器 Configurer,来定制我们过滤器链。HttpSecurity 只有在 http.build(); 的时候,才真正生成并配置 SecurityFilterChain。

        6.1.2、Configurer 与 Filter 对应关系

        ConfigurerFilter
        FormLoginConfigurerUsernamePasswordAuthenticationFilter
        CsrfConfigurerCsrfFilter
        LogoutConfigurerLogoutFilter
        HttpBasicConfigurerBasicAuthenticationFilter
        RequestCacheConfigurerRequestCacheAwareFilter
        AnonymousConfigurerAnonymousAuthenticationFilter
        ExceptionHandlingConfigurerExceptionTranslationFilter
        HeadersConfigurerHeaderWriterFilter
        没有 ConfigurerWebAsyncManagerIntegrationFilter
        SessionManagementConfigurerDisableEncodeUrlFilter
        SecurityContextConfigurerSecurityContextHolderFilter
        DefaultLoginPageConfigurerDefaultLoginPageGeneratingFilter
        DefaultLogoutPageGeneratingFilter
        DefaultResourcesFilter
        ServletApiConfigurerSecurityContextHolderAwareRequestFilter
        AuthorizeHttpRequestsConfigurerAuthorizationFilter

        6.1.3、自定义登录页面

        • 新建登录页

        在项目 /resources/static 下,新建 login.html

        <!DOCTYPE html>
        <html xmlns="http://www.w3.org/1999/xhtml">
        <head><title>Please Log In</title>
        </head>
        <body>
        <h1>Please Log In</h1>
        <form action="/login" method="post"><div><label><input type="text" name="username" placeholder="Username"/></label></div><div><label><input type="password" name="password" placeholder="Password"/></label></div><input type="submit" value="Log in" />
        </form>
        </body>
        </html>

        FormLoginConfigurer#loginPage 注释上有说明自定义表单的要求:

        1. 必须是 POST 提交。
        2. 表单提交给 FormLoginConfigurer#loginProcessingUrl 配置的 URL。
        3. 用户名必须叫 username,可以通过 FormLoginConfigurer#usernameParameter 修改。
        4. 密码必须叫 password,可以通过 FormLoginConfigurer#passwordParameter 修改。
        •  SecurityConfig
        @Configuration
        @EnableWebSecurity // 开启 spring security
        public class SecurityConfig {@Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {// 用户名/密码是表单提交的方式http.formLogin((formLogin) -> formLogin// 指定自定义登录页.loginPage("/login.html")// 定义 UsernamePasswordAuthenticationFilter 要拦截的路径// 默认 UsernamePasswordAuthenticationFilter.DEFAULT_ANT_PATH_REQUEST_MATCHER=/login// 通过这个配置覆盖。需要与表单提交 action 一致.loginProcessingUrl("/login"));// 对请求进行访问控制设置http.authorizeHttpRequests((authorizeHttpRequests) -> authorizeHttpRequests// 设置哪些路径可以直接访问,不需要认证.requestMatchers("/login.html","/login").permitAll()// 其他路径的请求都需要认证.anyRequest().authenticated());//关闭跨站点请求伪造csrf防护http.csrf(AbstractHttpConfigurer::disable);return http.build();}
        }
        •  重启服务,访问登录页

        登录成功后:

        之所以会出现这个页面,是因为我们没有定义登录成功后的处理。

        七、自定义登录成功/失败处理逻辑

        @Configuration
        @EnableWebSecurity // 开启 spring security
        public class SecurityConfig {@Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {// 用户名/密码是表单提交的方式http.formLogin((formLogin) -> formLogin// 指定自定义登录页.loginPage("/login.html")// 定义 UsernamePasswordAuthenticationFilter 要拦截的路径// 默认 UsernamePasswordAuthenticationFilter.DEFAULT_ANT_PATH_REQUEST_MATCHER=/login// 通过这个配置覆盖。需要与表单提交 action 一致.loginProcessingUrl("/login")//登录成功处理逻辑.successHandler(new LoginSuccessHandler())// 登录失败处理逻辑.failureHandler(new LoginFailureHandler()));// 对请求进行访问控制设置http.authorizeHttpRequests((authorizeHttpRequests) -> authorizeHttpRequests// 设置哪些路径可以直接访问,不需要认证.requestMatchers("/login.html","/login").permitAll()// 其他路径的请求都需要认证.anyRequest().authenticated());//关闭跨站点请求伪造csrf防护http.csrf(AbstractHttpConfigurer::disable);return http.build();}public static class LoginSuccessHandler implements AuthenticationSuccessHandler {@Overridepublic void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {response.setContentType("text/html;charset=utf-8");response.getWriter().write("登录成功");}}public static class LoginFailureHandler implements AuthenticationFailureHandler {@Overridepublic void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException {response.setContentType("text/html;charset=utf-8");response.getWriter().write("登录失败"+exception.getMessage());}}}

         登录成功:

        登录失败:

        八、基于 JWT 无状态认证

        十、扩展阅读

        FilterChainProxy 初始化(即 请求是怎么从 Servlet 过滤器链 转移到 SecurityFilterChain)

        •  FilterChainProxy 初始化

        WebSecurityConfiguration 初始化的时候,会往容器注册一个 Filter,这个 Filter 的 Bean Name 是 "springSecurityFilterChain", Filter 的实例是 FilterChainProxy。

        // 记住这个名字,等会儿会用到
        public static final String DEFAULT_FILTER_NAME = "springSecurityFilterChain";@Configuration(proxyBeanMethods = false)
        public class WebSecurityConfiguration implements ImportAware, BeanClassLoaderAware {private WebSecurity webSecurity;private List<SecurityFilterChain> securityFilterChains = Collections.emptyList();/*** 注意这个注入* Spring 会自动将所有匹配的 SecurityFilterChain beans 装配到这个集合中* 如果你没有自定义 SecurityFilterChain,那么这边只会有 defaultSecurityFilterChain 一个元素* 如果你定义了,那么多条 SecurityFilterChain 都会被注入进来,并赋值给 securityFilterChains */@Autowired(required = false)void setFilterChains(List<SecurityFilterChain> securityFilterChains) {this.securityFilterChains = securityFilterChains;}@Bean(name = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME)public Filter springSecurityFilterChain() throws Exception {boolean hasFilterChain = !this.securityFilterChains.isEmpty();if (!hasFilterChain) {this.webSecurity.addSecurityFilterChainBuilder(() -> {this.httpSecurity.authorizeHttpRequests((authorize) -> authorize.anyRequest().authenticated());this.httpSecurity.formLogin(Customizer.withDefaults());this.httpSecurity.httpBasic(Customizer.withDefaults());return this.httpSecurity.build();});}for (SecurityFilterChain securityFilterChain : this.securityFilterChains) {// 加入到 webSecurity 的属性 securityFilterChainBuilders = new ArrayList<>();this.webSecurity.addSecurityFilterChainBuilder(() -> securityFilterChain);}for (WebSecurityCustomizer customizer : this.webSecurityCustomizers) {customizer.customize(this.webSecurity);}// 返回的是 FilterChainProxyreturn this.webSecurity.build();}}

        默认的 SecurityFilterChain(DefaultSecurityFilterChain)也是在这个时候加入到 FilterChainProxy 的数组 filterChains 中的。

        public final class WebSecurity extends AbstractConfiguredSecurityBuilder<Filter, WebSecurity>implements SecurityBuilder<Filter>, ApplicationContextAware, ServletContextAware {private final List<SecurityBuilder<? extends SecurityFilterChain>> securityFilterChainBuilders = new ArrayList<>();@Overrideprotected Filter performBuild() throws Exception {int chainSize = this.ignoredRequests.size() + this.securityFilterChainBuilders.size();List<SecurityFilterChain> securityFilterChains = new ArrayList<>(chainSize);// 省略不重要的代码......// 系统中如果有定义超过两条 SecurityFilterChain,而且其中一条还拦截所有请求,会造成其他的 SecurityFilterChain 不会被执行// 所以这里做一个报错提醒DefaultSecurityFilterChain anyRequestFilterChain = null;for (SecurityBuilder<? extends SecurityFilterChain> securityFilterChainBuilder : this.securityFilterChainBuilders) {// 取出之前加入到 WebSecurity securityFilterChainBuilders 的 SecurityFilterChainSecurityFilterChain securityFilterChain = securityFilterChainBuilder.build();if (anyRequestFilterChain != null) {String message = "A filter chain that matches any request [" + anyRequestFilterChain+ "] has already been configured, which means that this filter chain [" + securityFilterChain+ "] will never get invoked. Please use `HttpSecurity#securityMatcher` to ensure that there is only one filter chain configured for 'any request' and that the 'any request' filter chain is published last.";throw new IllegalArgumentException(message);}if (securityFilterChain instanceof DefaultSecurityFilterChain defaultSecurityFilterChain) {// Java 16 开始引入的预览特性。它允许你在一个 if 条件语句中同时进行类型检查和类型的转换if (defaultSecurityFilterChain.getRequestMatcher() instanceof AnyRequestMatcher) {anyRequestFilterChain = defaultSecurityFilterChain;}}securityFilterChains.add(securityFilterChain);requestMatcherPrivilegeEvaluatorsEntries.add(getRequestMatcherPrivilegeEvaluatorsEntry(securityFilterChain));}if (this.privilegeEvaluator == null) {this.privilegeEvaluator = new RequestMatcherDelegatingWebInvocationPrivilegeEvaluator(requestMatcherPrivilegeEvaluatorsEntries);}// 生成 FilterChainProxy 实例,并关联 securityFilterChainsFilterChainProxy filterChainProxy = new FilterChainProxy(securityFilterChains);if (this.httpFirewall != null) {filterChainProxy.setFirewall(this.httpFirewall);}if (this.requestRejectedHandler != null) {filterChainProxy.setRequestRejectedHandler(this.requestRejectedHandler);}else if (!this.observationRegistry.isNoop()) {CompositeRequestRejectedHandler requestRejectedHandler = new CompositeRequestRejectedHandler(new ObservationMarkingRequestRejectedHandler(this.observationRegistry),new HttpStatusRequestRejectedHandler());filterChainProxy.setRequestRejectedHandler(requestRejectedHandler);}filterChainProxy.setFilterChainDecorator(getFilterChainDecorator());filterChainProxy.afterPropertiesSet();Filter result = filterChainProxy;if (this.debugEnabled) {this.logger.warn("\n\n" + "********************************************************************\n"+ "**********        Security debugging is enabled.       *************\n"+ "**********    This may include sensitive information.  *************\n"+ "**********      Do not use in a production system!     *************\n"+ "********************************************************************\n\n");result = new DebugFilter(filterChainProxy);}this.postBuildAction.run();return result;}
        }

        紧接着,在 Spring Boot 自动装配导入的类中,有一个 SecurityAutoConfiguration

        SecurityAutoConfiguration 在初始化的时候,会实例化一个 DelegatingFilterProxyRegistrationBean,它是一个 ServletContextInitializer 的实例,所以当 Sevlet 容器启动的时候,会调用 ServletContextInitializer#onStartup 方法,实例化一个 DelegatingFilterProxy,DelegatingFilterProxy 代理的 Bean 是 "springSecurityFilterChain" 也就是 FilterChainProxy。FilterChainProxy 是以一种懒加载的方式,跟 DelegatingFilterProxy 建立关联关系。

        @AutoConfiguration(after = SecurityAutoConfiguration.class)
        @ConditionalOnWebApplication(type = Type.SERVLET)
        @EnableConfigurationProperties(SecurityProperties.class)
        @ConditionalOnClass({ AbstractSecurityWebApplicationInitializer.class, SessionCreationPolicy.class })
        public class SecurityFilterAutoConfiguration {// 值是 "springSecurityFilterChain"private static final String DEFAULT_FILTER_NAME = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME;@Bean@ConditionalOnBean(name = DEFAULT_FILTER_NAME)public DelegatingFilterProxyRegistrationBean securityFilterChainRegistration(SecurityProperties securityProperties) {DelegatingFilterProxyRegistrationBean registration = new DelegatingFilterProxyRegistrationBean(DEFAULT_FILTER_NAME);registration.setOrder(securityProperties.getFilter().getOrder());registration.setDispatcherTypes(getDispatcherTypes(securityProperties));return registration;}}

        DelegatingFilterProxyRegistrationBean 的继承关系如下:

        而 ServletContextInitializer 是 Spring 框架提供的一个接口,主要用于在 Servlet 容器启动时对 ServletContext 进行编程式配置。在 Spring Boot 项目里,Spring Boot 会自动扫描所有实现了 ServletContextInitializer 接口的 Bean 并在 Servlet 容器启动时自动调用其 onStartup方法。

        DelegatingFilterProxyRegistrationBean 会实例化一个 DelegatingFilterProxy,

        // DelegatingFilterProxy 代理的 BeanName 是 "springSecurityFilterChain"
        DelegatingFilterProxy.targetBeanName = DelegatingFilterProxyRegistrationBean.targetBeanName = "springSecurityFilterChain"

        同时还为 DelegatingFilterProxy 传入一个 WebApplicationContext,就是 Spring Web 容器。

        DelegatingFilterProxy 在 AbstractFilterRegistrationBean#addRegistration 被加入到 ServletContext 上下文中,它的 Filter 名称也是 "springSecurityFilterChain" 拦截的是 DEFAULT_URL_MAPPINGS = { "/*" }; 也就是所有请求。

        最后,当请求进来的时候,会被 DelegatingFilterProxy 拦截,并调用它的 doFilter 方法(DelegatingFilterProxy 本质上是一个 Filter),从 WebApplicationContext 容器中以 "springSecurityFilterChain" 查找到 FilterChainProxy,将 DelegatingFilterProxy 跟 FilterChainProxy 建立关联关系,然后将请求委托给 FilterChainProxy 处理。

        • DelegatingFilterProxy、FilterChainProxy、SecurityFilterChain、Security Filters 它们之间的关系?

        DelegatingFilterProxy 有一个属性,Filter delegate 持有的就是它所代理的 FilterChainProxy:

        public class DelegatingFilterProxy extends GenericFilterBean {// 持有的是 FilterChainProxy@Nullableprivate volatile Filter delegate;// ...... 其他属性
        }

        FilterChainProxy 有一个 SecurityFilterChain 数组:

        public class FilterChainProxy extends GenericFilterBean {private List<SecurityFilterChain> filterChains;// ...... 其他属性
        }

        SecurityFilterChain 有一个 Filter 数组:

        // DefaultSecurityFilterChain 是接口 SecurityFilterChain 唯一默认实现
        public final class DefaultSecurityFilterChain implements SecurityFilterChain, BeanNameAware, BeanFactoryAware {private final List<Filter> filters;// ...... 其他属性
        }

        DelegatingFilterProxy、FilterChainProxy、SecurityFilterChain、Security Filters 之间的关系如图:


        http://www.mrgr.cn/news/98375.html

        相关文章:

      • HarmonyOS:使用Refresh组件实现页面下拉刷新上拉加载更多
      • PVE 8.4.1 安装 KDE Plasma 桌面环境 和 PVE换源
      • linux中查看.ypc二进制文件
      • Linux服务之网络共享
      • Melos 发布pub.dev
      • 30学Java第十天——类加载的过程
      • 【动手学强化学习】番外7-MAPPO应用框架2学习与复现
      • AWS Redshift的使用场景及一些常见问题
      • 绿算轻舟系列FPGA加速卡:驱动数字化转型的核心动力
      • electron-builder参数详解
      • ukui-greeter编译与安装
      • C/C++的数据类型
      • 数据库原理及应用mysql版陈业斌实验三
      • mongodb 安装配置
      • AI 项目详细开发步骤指南
      • antv x6使用(支持节点排序、新增节点、编辑节点、删除节点、选中节点)
      • 【Java集合】HashMap源码深度分析
      • 大数据面试问答-批处理性能优化
      • poi-tl
      • Spark-SQL核心编程(一)