在企业级项目中,目前较为流行的的认证和授权框架主要是 Spring Security 和 Shiro。Spring Security 相对于 Shiro 具有更加丰富的功能和社区资源,但相对而言 Spring Security 的上手难度也要大于 Shiro。因此,一般中大型项目会更倾向于使用 Spring Security 框架,而小型项目多采用 Shiro 框架。
1 Spring Security 是什么?官方是这样介绍的。
“Spring Security is a framework that focuses on providing both authentication and authorization to Java applications. Like all Spring projects, the real power of Spring Security is found in how easily it can be extended to meet custom requirements.”
Spring Security 是一个专注于为 Java 应用程序提供身份认证和授权的框架。这句话简明扼要地说出了 Spring Security 的两个核心功能:
身份认证(authentication),即验证用户身份的合法性,以判断用户能否登录。
授权(authorization),即验证用户是否有权限访问某些资源或者执行某些操作。
本文将结合源码对身份认证(authentication)的流程和核心组件进行详细讲解。
Spring Security 进行身份认证这一功能主要是通过一系列的过滤器链配合来实现的。在我们启动 Spring Security 项目时可以在控制台看到 DefaultSecurityFilterChain 打印出的默认过滤器链。
public DefaultSecurityFilterChain(RequestMatcher requestMatcher, List<Filter> filters) {
logger.info("Creating filter chain: " + requestMatcher + ", " + filters);
this.requestMatcher = requestMatcher; this.filters = new ArrayList(filters);
1.1 Spring Security 核心过滤器
先简要说一下过滤器(Filter),Filter 可以在服务器作出响应前拦截用户请求,并在拦截后修改 request 和 response,可实现一次编码、多处应用。Filter 主要有以下两点作用:
拦截请求:在 HttpServletRequest 到达 Servlet 之前进行拦截,查看和修改 HttpServletRequest 的 Header 和数据。
拦截响应:在 HttpServletResponse 到达客户端之前完成拦截,查看和修改 HttpServletResponse 的 Header 和数据。
过滤器链作为 Spring Security 的核心,下图呈现 Spring Security 过滤器链的一个简要执行流程。本文将以该流程为基础对认证流程进行剖析:
Spring Security 过滤器链执行流程图
SecurityContextPersistenceFilter:整个 Spring Security 过滤器链的开端。主要有两点作用:(1)当请求到来时,检查 Session中是否存在 SecurityContext,若不存在,则创建一个新的 SecurityContext;(2)在请求结束时,将 SecurityContext 放入 Session 中,并清空 SecurityContextHolder。
UsernamePasswordAuthenticationFilter:继承自抽象类 AbstractAuthenticationProcessingFilter。当进行表单登录时,该 Filter 将用户名和密码封装成 UsernamePasswordAuthenticationToken 进行验证。
AnonymousAuthenticationFilter:匿名身份过滤器,一般用于匿名登录。当前面的 Filter 认证后依然没有用户信息时,该Filter会生成一个匿名身份 AnonymousAuthenticationToken。
ExceptionTranslationFilter:异常转换过滤器,用于处理 FilterSecurityInterceptor 抛出的异常。但是只会处理两类异常:AuthenticationException 和 AccessDeniedException,其它的异常它会继续抛出。
1.2 Spring Security 核心组件
SecurityContextHolder:用于获取 SecurityContext 的静态工具类,是Spring Security 存储身份验证者详细信息的位置。
SecurityContext: 上下文对象,Authentication 对象会放在里面。
Authentication: 认证接口,定义了认证对象的数据形式。
AuthenticationManager: 用于校验 Authentication,返回一个认证完成后的 Authentication 对象。
1、SecurityContextHolder
public class SecurityContextHolder {
public static void clearContext() {
strategy.clearContext();
public static SecurityContext getContext() {
return strategy.getContext();
public static void setContext(SecurityContext context) {
strategy.setContext(context);
SecurityContextHolder 用于存储安全上下文(SecurityContext)的信息。而如何保证用户信息的安全,Spring Security 采用“用户信息和线程绑定”的策略,SecurityContextHolder 默认采用 ThreadLocal 机制保存用户的 SecurityContext,在使用中可以通过 SecurityContextHolder 工具轻松获取用户安全上下文。这意味着,只要是针对某个使用者的逻辑执行都是在同一个线程中进行,Spring Security 会在用户登录时自动绑定认证信息到当前线程,在用户退出时也会自动清除当前线程的认证信息。
SecurityContextHolder、SecurityContext 和 Authentication 的关系如图中所示。
Authentication authentication = SecurityContextHolder.getContext().getAuthentication()
SecurityUserDetails userDetails = (SecurityUserDetails) authentication.getPrincipal()
其中,getAuthentication() 返回认证信息,getPrincipal() 返回身份信息。 SecurityContext 是从 SecurityContextHolder 获得的。SecurityContext 包含一个 Authentication对象。
当然,有些应用程序并不完全适合使用 ThreadLocal 模式。例如,有些应用希望能在子线程中获取登录用户的信息,亦或者希望所有线程使用相同的安全上下文。 针对这些情况,SecurityContextHolder 可以在启动时配置策略以指定您希望如何存储上下文。
MODE_THREADLOCAL:SecurityContext 存储在线程中。
MODE_INHERITABLETHREADLOCAL:SecurityContext 存储在线程中,但子线程可以获取到父线程中的 SecurityContext。
MODE_GLOBAL:SecurityContext 在所有线程中都相同。
2、SecurityContext
public interface SecurityContext extends Serializable {
Authentication getAuthentication();
void setAuthentication(Authentication var1);
SecurityContext 即安全上下文,其中存储当前操作的用户是谁、该用户是否已经被认证、该用户拥有哪些角色权限等信息。
3、Authentication
public interface Authentication extends Principal, Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
Object getCredentials();
Object getDetails();
Object getPrincipal();
boolean isAuthenticated();
void setAuthenticated(boolean var1) throws IllegalArgumentException;
getAuthorities( ):获取权限信息列表。
getCredentials( ):获取密码信息,用户输入的密码字符串,在认证过后通常会被移除,保障用户信息安全。
getDetails( ):获取细节信息,web 应用中的实现接口通常为 WebAuthenticationDetails,它记录了访问者的 ip 地址和 sessionId 的值。
getPrincipal( ):获取身份信息,大部分情况下返回 UserDetails 接口的实现类,是框架中的常用接口之一。
isAuthenticated( ):判断认证状态,默认是 false,认证成功为 true。
setAuthenticated( ):设置是否进行认证。
4、AuthenticationManager
public interface AuthenticationManager {
Authentication authenticate(Authentication var1) throws AuthenticationException;
AuthenticationManager 接口其中只有一个方法 authenticate(),该方法用于进行身份验证,验证通过则返回带有完整认证身份信息的Authentication(包括 Authentication 中的所有属性)。
2 Spring Security认证流程
结合上面的时序图,让我们先熟悉下 Spring Security 的认证流程:
用户进行认证,用户名和密码被 SecurityFilterChain 中的 UsernamePasswordAuthenticationFilter 过滤器拦截,并将请求封装为 Authentication,其默认实现类是 UsernamePasswordAuthenticationToken。
将封装的 UsernamePasswordAuthenticationToken 提交至 AuthenticationManager(认证管理器)进行认证。
认证成功后, AuthenticationManager(身份管理器)会返回一个包含用户身份信息的 Authentication 实例(包括身份信息,细节信息,但密码通常会被移除)。
SecurityContextHolder (安全上下文容器)将认证成功的 Authentication 存储到 SecurityContext(安全上下文)中。
其中,AuthenticationManager 接口是认证相关的核心接口,ProviderManager 是它的实现类。因为 Spring Security 支持多种认证方式,所以 ProviderManager 维护着一个List<AuthenticationProvider> 列表,包含多种认证方式,最终实际的认证工作就是由列表中的 AuthenticationProvider 完成的。其中最常见的 web 表单认证的对应的 AuthenticationProvider 实现类为 DaoAuthenticationProvider,它的内部又维护着一个 UserDetailsService负责获取 UserDetails。最终 AuthenticationProvider 将 UserDetails 填充至 Authentication。
3 Spring Security认证的底层实现流程
根据 Spring Security 的认证流程图和源码,接下来一起看下 Spring Security 如何完成用户名密码认证。
步骤1:当用户发起认证请求,AbstractAuthenticationProcessingFilter 从 HttpServletRequest 创建 Authentication进行身份验证。创建的身份验证类型取决于 AbstractAuthenticationProcessingFilter 的子类。
3.1 UsernamePasswordAuthenticationFilter
以用户名密码认证为例 ,请求被 UsernamePasswordAuthenticationFilter 过滤器拦截,UsernamePasswordAuthenticationFilter 根据Request 中提交的用户名和密码创建一个 Token(UsernamePasswordAuthenticationToken)。
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
private String usernameParameter = "username";
private String passwordParameter = "password";
private boolean postOnly = true;
public UsernamePasswordAuthenticationFilter() {
super(new AntPathRequestMatcher("/login", "POST"));
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());
} else {
String username = this.obtainUsername(request);
String password = this.obtainPassword(request);
if (username == null) {
username = "";
if (password == null) {
password = "";
username = username.trim();
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
this.setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
protected void setDetails(HttpServletRequest request, UsernamePasswordAuthenticationToken authRequest) {
authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
3.2 UsernamePasswordAuthenticationToken
UsernamePasswordAuthenticationToken 又是什么呢?
public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = 500L;
private final Object principal;
private Object credentials;
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
super((Collection)null);
this.principal = principal;
this.credentials = credentials;
this.setAuthenticated(false);
public UsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(true);
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
if (isAuthenticated) {
throw new IllegalArgumentException("Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
} else {
super.setAuthenticated(false);
从上面的源码可以看出,其实 UsernamePasswordAuthenticationToken 的核心就是两个构造方法,分别用于初始化未认证和认证的 Token。
步骤2:将生成的未认证 Token 交给 AuthenticationManager 进行身份认证。
这一步是身份认证的核心,下面进行详细讲解:
未认证的 UsernamePasswordAuthenticationToken(携带用户名、密码信息)被提交给 AuthenticationManager。AuthenticationManager 的实现类 ProviderManager 负责对认证请求链 AuthenticationProviders 进行管理。
ProviderManager 通过循环的方式,发现 DaoAuthenticationProvider 的类型符合,使用 DaoAuthenticationProvider 进行认证。
DaoAuthenticationProvider 从 UserDetailsService 中查找 UserDetails。
DaoAuthenticationProvider 使用 PasswordEncoder 验证上一步返回的 UserDetails 中的用户密码。
当身份验证成功, Authentication 返回一个已认证的 UsernamePasswordAuthenticationToken ,其中包含 UserDetailsService 返回的 UserDetails 信息。最终,认证成功的 UsernamePasswordAuthenticationToken 添加到 SecurityContextHolder 完成账号密码的身份认证。
3.3 AuthenticationManager
AuthenticationManager 作为一个最上级的接口本身不包含验证的逻辑,只有一个 authenticate 方法用于处理身份验证请求,返回一个 Authentication 对象。它的作用只是用来管理 AuthenticationProvider。
public interface AuthenticationManager {
Authentication authenticate(Authentication var1) throws AuthenticationException;
AuthenticationManager 的实现方式有很多,通常是使用 ProviderManager 对认证请求链进行管理; 这里可以看一下 ProviderManager 源码中 authenticate 方法的注解。
* The list of {@link AuthenticationProvider}s will be successively tried until an <code>AuthenticationProvider</code> indicates it is capable of authenticating the type of <code>Authentication</code> object passed.
Authentication will then be attempted with that <code>AuthenticationProvider</code>.
* If more than one <code>AuthenticationProvider</code> supports the passed <code>Authentication</code> object, the first one able to successfully authenticate the <code>Authentication</code> object determines the <code>result</code>, overriding any possible <code>AuthenticationException</code> thrown by earlier supporting <code>AuthenticationProvider</code>s.
On successful authentication, no subsequent <code>AuthenticationProvider</code>s will be tried.
* If authentication was not successful by any supporting <code>AuthenticationProvider</code> the last thrown <code>AuthenticationException</code> will be rethrown.
翻译如下: AuthenticationProvider 列表将被链式连续尝试执行,直到一个 AuthenticationProvider 能够验证通过,即可结束。 如果任何一个能够成功验证 Authentication 对象确定结果,就会忽略之前 AuthenticationProvider 认证失败抛出的异常以及 null 响应。成功认证后,不会尝试后续的 AuthenticationProviders。 如果任何支持 AuthenticationProvider 的身份验证未成功,则最后抛出的 AuthenticationException 将被重新抛出。
简单点说,List 中只要有一个 AuthenticationProvider 能够验证成功,不管之前是否有失败的情况,结果都会返回这次成功的结果,之前抛出的异常也会被忽略。
3.4 ProviderManager
这里看一下ProviderManage r的源码,它是通过 for 循环的方式获取 AuthenticationProvider 对象。
public 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
for (AuthenticationProvider provider : getProviders()) {
// 判断是否支持处理
if (!provider.supports(toTest)) {
continue
if (logger.isTraceEnabled()) {
logger.trace(LogMessage.format("Authenticating request with %s (%d/%d)",
provider.getClass().getSimpleName(), ++currentPosition, size))
try {
//调用实现类的authenticate方法进行真实业务逻辑认证处理
result = provider.authenticate(authentication)
if (result != null) {
copyDetails(authentication, result)
break
catch (AccountStatusException | InternalAuthenticationServiceException ex) {
prepareException(ex, authentication)
throw ex
catch (AuthenticationException ex) {
lastException = ex
authenticate 这个方法是在 ProviderManager 类上的,这个类实现了 AuthenticationManager 接口,通过 for 循环的方式去获取所有的 AuthenticationProvider,而真正校验的逻辑是写在 AuthenticationProvider 中的。AuthenticationManager 其实只是将 AuthenticationProvider 收集起来,然后在登录时逐个进入 AuthenticationProvider 并判断此种验证逻辑是否支持本次登录方式,根据传进来的 Authentication 类型挑选出符合的 provider 进行校验处理。
3.5 AuthenticationProvider
public interface AuthenticationProvider{
Authentication authenticate(Authentication authentication) throws AuthenticationException;
boolean supports(Class<?> authentication);
Spring Security 进行身份认证的方式有许多种,除了最常见的账号密码认证、手机号认证,还有 ReremberMe 认证、三方认证等,它们均遵守着由 AuthenticationProvider 来处理认证请求的规则。
可以说到目前为止,还没有进行”真正的“账号密码校验。有些地方说是在默认的 DaoAuthenticationProvider 中判断Authentication
类型进行判断直接完成的。其实并不是很准确,应该说 AuthenticationProvider 的认证其实由 AbstractUserDetailsAuthenticationProvider
抽象类和 AbstractUserDetailsAuthenticationProvider 的子类 DaoAuthenticationProvider
共同协助完成的。
3.6 AbstractUserDetailsAuthenticationProvider
public abstract class AbstractUserDetailsAuthenticationProvider
implements AuthenticationProvider, InitializingBean, MessageSourceAware {
@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 {
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
catch (UsernameNotFoundException ex) {
this.logger.debug("Failed to find user '" + username + "'");
if (!this.hideUserNotFoundExceptions) {
throw ex;
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
try {
this.preAuthenticationChecks.check(user);
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);
this.postAuthenticationChecks.check(user);
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
Object principalToReturn = user;
if (this.forcePrincipalAsString) {
principalToReturn = user.getUsername();
return createSuccessAuthentication(principalToReturn, authentication, user);
通过上面的源码,和标注的注释。可以清晰的了解其验证流程:
首先获取用户名 username;然后尝试从缓存中获取 User;若缓存中没有,再调用 retrieveUser 方获取(子类DaoAuthenticationProvider 实现)。
然后,进行前置身份认证检查(校验账号是否锁定、是否可用、是否过期)和额外的检查(凭据/密码校验)。进行后置身份认证检查(校验凭据/密码是否过期)
若当前用户还没有被缓存的话,将用户存入缓存;最后,创建身份认证成功的 Authentication。
3.7 DaoAuthenticationProvider
DaoAuthenticationProvider 是 AuthenticationProvider 最常用的实现,其作用在注释中已经清楚的表明。
* An {@link AuthenticationProvider} implementation that retrieves user details from a
* {@link UserDetailsService}.
即通过 UsernamePasswordAuthenticationToken 获取到 username,然后通过 UserDetailsService 获取用户详细信息。
下面看一下 DaoAuthenticationProvider 是如何重写的 retrieveUser 和 additionalAuthenticationChecks 两个方法。(部分源码省略)
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
this.prepareTimingAttackProtection();
try {
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");
} else {
return loadedUser;
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
if (authentication.getCredentials() == null) {
this.logger.debug("Failed to authenticate since no credentials provided");
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
String presentedPassword = authentication.getCredentials().toString();
if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
this.logger.debug("Failed to authenticate since password does not match stored value");
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
可以看出,主要是利用 UserDetailsService 和 PasswordEncoder 来实现获取用户信息和密码认证。
至此身份认证的源码分析就结束了,在 spring security 的整个认证流程中其实最难理解的就是 AuthenticationManager 、ProviderManager、AuthenticationProvider 之间的关系,其关系如下图所示。
不同的认证方式对应不同的 AuthenticationProvider,一个完整的认证流程可由多个 AuthenticationProvider 来达成。在实际使用中,很多特定场景需要我们自定义 AuthenticationProvider 来进行认证。
步骤3:认证成功。
将认证信息存储到 SecurityContextHodler 并调用 RememberMeServices(如有开启)等,回调 AuthenticationSuccessHandler 进行处理。
步骤4:认证失败。
将认证信息存储到 SecurityContextHodler 并调用 RememberMeServices(如有开启)等,并回调 AuthenticationSuccessHandler 处理。
核心组件的调用流程:
下图即总结的核心组件的调用流程。在实际运用或问题排查过程中,我们均可以参照下面的流程进行学习和排查。
本文仅针对 Spring Security 的认证流程进行了详细讲解,但Spring Security 登录相关的内容远不止这些。如果大家有更好的想法,欢迎多多交流,一起学习,一起进步!
5 参考文档
Spring Security官方文档
MySQL - InnoDB 内存结构解析
浅谈AI目标检测技术发展史
数据仓库模型重构实践
从源码看Lucene的两阶段提交
Kubernetes弹性扩缩容之HPA和KEDA
政采云技术团队(Zero),一个富有激情、创造力和执行力的团队,Base 在风景如画的杭州。团队现有 500 多名研发小伙伴,既有来自阿里、华为、网易的“老”兵,也有来自浙大、中科大、杭电等校的新人。团队在日常业务开发之外,还分别在云原生、区块链、人工智能、低代码平台、中间件、大数据、物料体系、工程平台、性能体验、可视化等领域进行技术探索和实践,推动并落地了一系列的内部技术产品,持续探索技术的新边界。此外,团队还纷纷投身社区建设,目前已经是 google flutter、scikit-learn、Apache Dubbo、Apache Rocketmq、Apache Pulsar、CNCF Dapr、Apache DolphinScheduler、alibaba Seata 等众多优秀开源社区的贡献者。如果你想改变一直被事折腾,希望开始折腾事;如果你想改变一直被告诫需要多些想法,却无从破局;如果你想改变你有能力去做成那个结果,却不需要你;如果你想改变你想做成的事需要一个团队去支撑,但没你带人的位置;如果你想改变本来悟性不错,但总是有那一层窗户纸的模糊……如果你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的自己。如果你希望参与到随着业务腾飞的过程,亲手推动一个有着深入的业务理解、完善的技术体系、技术创造价值、影响力外溢的技术团队的成长过程,我觉得我们该聊聊。任何时间,等着你写点什么,发给 zcy-tc@cai-inc.com
微信公众号
文章同步发布,政采云技术团队公众号,欢迎关注
政采云技术团队已转移到新的政采云技术
政采云技术zero团队 @政采云有限公司
粉丝