一、是什么
- 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、判断是否有权限访问资源

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

- 当用户提交他们的用户名和密码时,UsernamePasswordAuthenticationFilter#attemptAuthentication 方法通过从 HttpServletRequest 实例中提取用户名和密码来创建 UsernamePasswordAuthenticationToken(这是一种 Authentication 类型。Authentication 接口表示认证信息。可以是用户名密码、匿名访问,也可以是短信验证、二维码、指纹。具体取决于你自己的认证方式,由自己扩展)。
- 接下来,UsernamePasswordAuthenticationToken 被传递到 AuthenticationManager 实例ProviderManager#authenticate 中进行身份验证。AuthenticationManager 的处理细节取决于用户信息的存储方式(即 Authentication 的类型。比如,UsernamePasswordAuthenticationToken 由 ProviderManager 委托给DaoAuthenticationProvider 处理,AnonymousAuthenticationToken 委托给 AnonymousAuthenticationProvider 处理,用户也可以自己定义一个 QRCodeAuthenticationToken 表示二维码认证信息,再自己定义一个 QRCodeAuthenticationProvider 来处理)。
- 如果身份验证失败,则失败。
- SecurityContextHolder 被清除。
- 调用 RememberMeServices.loginFail。如果未配置 Remember me,则没有任何操作。
- AuthenticationFailureHandler 被调用。
4.如果身份验证成功,则成功。
- SessionAuthenticationStrategy 收到新登录的通知。
- Authentication 在 SecurityContextHolder上设置。AbstractAuthenticationProcessingFilter#successfulAuthentication 中设置。
- 调用 RememberMeServices.loginSuccess。如果未配置 Remember me,则没有任何操作。
- ApplicationEventPublisher 发布 InteractiveAuthenticationSuccessEvent。
- 调用 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.loginSuccess
this.rememberMeServices.loginSuccess(request, response, authResult);
if (this.eventPublisher != null) {
// 6、ApplicationEventPublisher 发布 InteractiveAuthenticationSuccessEvent
this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
}
// 7、调用 AuthenticationSuccessHandler
this.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 默认登录页




最低0.47元/天 解锁文章
1万+

被折叠的 条评论
为什么被折叠?



