SpringSecurity(五):认证流程
Spring Security整个流程如上图所示,下面我们分别介绍一下各个组件
AbstracAuthenticationProcessingFilter
AbstracAuthenticationProcessingFilter是一个抽象类,如果使用账号密码的方式登录,AbstracAuthenticationProcessingFilter的实现类就是UsernamePasswordAuthenticationFilter,这个抽象类会构造出一个Authentication对象,UsernamePasswordAuthenticationFilter构造出的是UsernamePasswordAuthenticationToken
我们以UsernamePasswordAuthenticationFilter为例,看下登录的流程
(1)用户提交登陆请求时,UsernamePasswordAuthenticationFilter会从HttpServletRequest中取出登录名和密码,创建出一个UsernamePasswordAuthenticationToken对象
(2)UsernamePasswordAuthenticationToken对象被传入ProviderManager进行认证
(3)认证失败则会清除SecurityContextHolder相关信息,进行认证失败处理
(4)认证成功则进行登录信息的存储,认证成功跳转等。
AuthenticationManager
AuthenticationManager是一个认证管理器,它定义了Spring Security如何执行认证操作,如果认证成功后,AuthenticationManager会返回一个Authentication对象设置到SecurityContextHolder中。AuthenticationManager是一个接口,它的默认实现类也是我们使用最多的实现类是ProviderManager
ProviderManager
在Spring Security中由于系统可能同时支持多种认证方式,,不同的认证方式对应不同的AuthenticationProvider,,因此在ProviderManager中存在一个AuthenticationProvider列表,ProviderManager会对列表中每一个AuthenticationProvider执行认证,最终得到认证结果,ProviderManager也可以配置一个parent,可以使任意类型的AuthenticationProvider,当列表中所有的AuthenticationProvider均认证不通过是,由parent认证。但通常都是由ProviderManager自己扮演parent
我们重点看一下authenticate方法
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
//首先获取authentication的类型
Class<? extends Authentication> toTest = authentication.getClass();
//当前认证的异常
AuthenticationException lastException = null;
//parent认证的异常
AuthenticationException parentException = null;
//当前认证的结果
Authentication result = null;
//parent认证的结果
Authentication parentResult = null;
boolean debug = logger.isDebugEnabled();
//遍历所有provider
for (AuthenticationProvider provider : getProviders()) {
//如果该AuthenticationProvider不支持该authentication类型
if (!provider.supports(toTest)) {
continue;
}
if (debug) {
logger.debug("Authentication attempt using "
+ provider.getClass().getName());
}
try {
//获得认证结果
result = provider.authenticate(authentication);
if (result != null) {
//给authentication的details属性赋值
copyDetails(authentication, result);
break;
}
}
catch (AccountStatusException e) {
prepareException(e, authentication);
// SEC-546: Avoid polling additional providers if auth failure is due to
// invalid account status
throw e;
}
catch (InternalAuthenticationServiceException e) {
prepareException(e, authentication);
throw e;
}
catch (AuthenticationException e) {
lastException = e;
}
}
//没有认证结果 且parent认证器不为空
if (result == null && parent != null) {
// Allow the parent to try.
try {
result = parentResult = parent.authenticate(authentication);
}
catch (ProviderNotFoundException e) {
// ignore as we will throw below if no other exception occurred prior to
// calling parent and the parent
// may throw ProviderNotFound even though a provider in the child already
// handled the request
}
catch (AuthenticationException e) {
lastException = parentException = e;
}
}
//如果结果不为空,则把result中的凭证擦除,防止泄露
if (result != null) {
if (eraseCredentialsAfterAuthentication
&& (result instanceof CredentialsContainer)) {
// Authentication is complete. Remove credentials and other secret data
// from authentication
((CredentialsContainer) result).eraseCredentials();
}
// If the parent AuthenticationManager was attempted and successful than it will publish an
// AuthenticationSuccessEvent
// This check prevents a duplicate AuthenticationSuccessEvent if the parent AuthenticationManager
// already published it
if (parentResult == null) {
eventPublisher.publishAuthenticationSuccess(result);
}
return result;
}
// Parent was null, or didn't authenticate (or throw an exception).
if (lastException == null) {
lastException = new ProviderNotFoundException(messages.getMessage(
"ProviderManager.providerNotFound",
new Object[] { toTest.getName() },
"No AuthenticationProvider found for {0}"));
}
// If the parent AuthenticationManager was attempted and failed than it will publish
// an AbstractAuthenticationFailureEvent
// This check prevents a duplicate AbstractAuthenticationFailureEvent if the parent
// AuthenticationManager already published it
if (parentException == null) {
prepareException(lastException, authentication);
}
throw lastException;
}
接下来我们来理解下AuthenticationProvider
AuthenticationProvider就是对不同的身份类型执行不同的认证方法,源码如下:
public interface AuthenticationProvider {
// 具体认证流程
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
// supports函数用来指明该Provider是否适用于该类型的认证,如果不合适,则寻找另一个Provider进行验证处理。
boolean supports(Class<?> authentication);
}
当使用账号密码的方式登录时,实现类就是DaoAuthenticationProvider,DaoAuthenticationProvider继承自AbstractUserDetailsAuthenticationProvider,具体方法比较多,我们就不一一分析了,但有一个比较有意思的点
在获取用户对象时
protected final UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
try {
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
}
catch (UsernameNotFoundException ex) {
mitigateAgainstTimingAttack(authentication);
throw ex;
}
catch (InternalAuthenticationServiceException ex) {
throw ex;
}
catch (Exception ex) {
throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
}
}
会调用prepareTimingAttackProtection这个方法对常量USER_NOT_FOUND_PASSWPRD进行加密并保存,如果没有找到用户名则调用 mitigateAgainstTimingAttack方法将这个加密结果与用户传来的密码进行匹配。
private void mitigateAgainstTimingAttack(UsernamePasswordAuthenticationToken authentication) {
if (authentication.getCredentials() != null) {
String presentedPassword = authentication.getCredentials().toString();
this.passwordEncoder.matches(presentedPassword, this.userNotFoundEncodedPassword);
}
}
为什么都不存在这个用户名还需要比对密码?这不是注定失败吗?
主要是为了防止旁道攻击,在spring security中默认是隐藏了用户名不存在的异常(在AbstractUserDetailsAuthenticationProvider中有一个hideUserFoundException属性,默认为true)。当我们用户名不存在也进行密码检验时,用户就不能根据请求耗费的时长来判断该用户名是否存在了!
AbstractAuthenticationProcessingFilter
作为过滤器链的一环,AbstractAuthenticationProcessingFilter可以用来处理任何提交给它的身份认证。
AbstractAuthenticationProcessingFilter是一个抽象类,如果使用用户名/密码的方式登陆,那它对应的实现类就是UsernamePasswordAuthenticationFilter,构造出来的Authentication对象就是UsernamePasswordAuthenticationToken。至于AuthenticationManager,前面已经说过了一般情况下是ProviderManager
我们来梳理下大致流程:
(1)当用户提交登录请求时,UsernamePasswordAuthenticationFilter会从当前请求HttpServletRequest中提取出登录用户名/密码。然后创建出一个UsernamePasswordAuthenticationToken对象
(2)UsernamePasswordAuthenticationToken对象将会被传入ProviderManager中进行认证操作
(3)如果认证失败,则SecurityContextHolder中相关信息将被清除,登陆失败回调也会被调用
(4)如果认证成功,则会进行登录信息存储,Session并发处理,登陆成功事件发布以及登陆成功方法回调等
我们看下AbstractAuthenticationProcessingFilter的源码
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
// 判断该filter是否能处理该次请求,即请求的路径和该filter配置要处理的url是否match
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
Authentication authResult;
try {
authResult = attemptAuthentication(request, response);
if (authResult == null) {
//没有得到认证结果,表明子类实现中无法处理该类型的认证
return;
}
sessionStrategy.onAuthentication(authResult, request, response);
}
catch (InternalAuthenticationServiceException failed) {
unsuccessfulAuthentication(request, response, failed);
return;
}
catch (AuthenticationException failed) {
// Authentication failed
unsuccessfulAuthentication(request, response, failed);
return;
}
// 认证成功后,通过设置属性值 continueChainBeforeSuccessfulAuthentication
// 可以跳过认证成功后逻辑的处理
if (continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
//认证成功后处理
successfulAuthentication(request, response, chain, authResult);
}
protected void unsuccessfulAuthentication(HttpServletRequest request,
HttpServletResponse response, AuthenticationException failed)
throws IOException, ServletException {
SecurityContextHolder.clearContext();
// 子类可以配置 rememberMeServices
rememberMeServices.loginFail(request, response);
// 子类可以配置 failureHandler
failureHandler.onAuthenticationFailure(request, response, failed);
}
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, Authentication authResult)
throws IOException, ServletException {
SecurityContextHolder.getContext().setAuthentication(authResult);
// 子类可以配置 rememberMeServices
rememberMeServices.loginSuccess(request, response, authResult);
// Fire event 出发成功事件,如果有订阅者,则可以接受到该事件
if (this.eventPublisher != null) {
eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
authResult, this.getClass()));
}
// 子类可以配置 successHandler
successHandler.onAuthenticationSuccess(request, response, authResult);
}
我们可以看到关键的代码在调用attemptAuthentication方法来获取一个经过认证后的Authentication对象,该方法是一个抽象方法,具体实现在子类中。
我们接下来看下UsernamePasswordAuthenticationFilter源码
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}
String username = obtainUsername(request);
String password = obtainPassword(request);
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
(1)通过obtainUsername和obtainPassword获取请求中的用户名和密码字段,这两个方法实际就是request.getParameter方法,注意默认用户名字段为username,密码字段为password
(2)拿到用户名传来的账号密码后,构造出一个UsernamePasswordAuthenticationToken,然后调用getAuthenticationManager().authenticate方法进行认证,进入到我们前面说的ProviderManager的流程中了
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!