〇、写在前面
本篇是基于前两篇Demo进行的登录认证源码流程的讲解,可以先大致看看前两篇,能主动来了解源码,我想对使用Spring Security也有一定经验了,肯定能很快熟悉这个Demo,这里附上项目地址:spring-security-demo,以及前两篇博文地址: 《手把手教你如何使用Spring Security(上):登录授权》 《手把手教你如何使用Spring Security(中):接口认证》
一、登录授权流程
前面整体框架设计的源码文章中提到,其实请求进来就是走一条我们配置好的过滤器链,而登录授权这个过滤器(
UserAuthenticationFilter
)也是过滤器链中的一环,现在我们只关注这个过滤器是怎么走的,关于过滤器链上的其他过滤器我们后面文章慢慢分析。Demo中配置的过滤器链:
流程概述
-
先看看项目大致的类
-
对于登录授权,请求会先进入过滤器,再使用Provider认证处理,最后进行成功/失败处理器处理,所以大致流程是这样的:
➜ filter :
UserAuthenticationFilter
➜ provider :UserAuthenticationProvider
➜ handler :UserLoginSuccessHandler/HttpStatusLoginFailureHandler
- 虽然顺序是这样,但是并不是filter中调用provider,provider中调用handler,控制这一流程的是其实是UserAuthenticationFilter的父类
AbstractAuthenticationProcessingFilter
AbstractAuthenticationProcessingFilter
- 这个类里面主要关注
doFilter()
方法,这里就是控制着整个登录认证的流程,源码:
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
// 判断请求是否能被这个过滤器处理,不能的话直接进入下一个过滤器
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
if (logger.isDebugEnabled()) {
logger.debug("Request is to process authentication");
}
Authentication authResult;
try {
// 这个方法就是由子类实现,是在我们 UserAuthenticationFilter 中实现的
authResult = attemptAuthentication(request, response);
if (authResult == null) {
return;
}
sessionStrategy.onAuthentication(authResult, request, response);
}
catch (InternalAuthenticationServiceException failed) {
logger.error(
"An internal error occurred while trying to authenticate the user.",
failed);
// 失败处理器
unsuccessfulAuthentication(request, response, failed);
return;
}
catch (AuthenticationException failed) {
// 失败处理器
unsuccessfulAuthentication(request, response, failed);
return;
}
if (continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
// 成功处理器
successfulAuthentication(request, response, chain, authResult);
}
- 这个方法也简单,主要就是调用了一个认证方法
attemptAuthentication()
去认证,如果认证过程中有异常,就会被catch
然后被失败处理器处理
,认证成功就走主流程最后被成功处理器处理
,处理完之后不会再走下面的过滤器。 - 熟悉设计模式的会发现这里是个典型的
模板方法模式
,这里定义了一个算法骨架,而具体的实现都由子类去完成,包括认证逻辑(attemptAuthentication()
)、失败处理(unsuccessfulAuthentication()
)、成功处理(successfulAuthentication()
)实际上都分别被我们定义的UserAuthenticationFilter
、UserLoginSuccessHandler
、HttpStatusLoginFailureHandler
进行实现处理。
好像忽略了一个类,流程中其他类都有被调用,那
UserAuthenticationProvider
呢?我们看看UserAuthenticationFilter
类中的attemptAuthentication()
方法:
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException, IOException, ServletException {
// TODO 这里的逻辑主要有两个作用,一个是进行初步的校验,一个是组装待认证的Token,举几个例子:
// 1.微信授权登录:客户端会传过来一些加密串,这里逻辑主要解密这些加密串的数据获取unionId、openId、手机号以及用户昵称头像等基本信息,
// 然后组装Token传给Provider进行下一步认证,如果这里报错直接就返回异常,不会进行下一步认证。
// 2.手机短信验证码登录:这里主要验证短信验证码的正确性,然后组装Token传给Provider进行下一步认证,如果短信验证码错误直接抛异常
// 3.账号密码图形验证码登录:这里主要验证图形验证码的正确性,然后组装Token传给Provider进行下一步认证,如果图形验证码错误直接抛异常
// ...
// =================================================== 示例 ===============================================
String body = StreamUtils.copyToString(request.getInputStream(), StandardCharsets.UTF_8);
String mobile = null, password = null, verifyCode = null;
if(StringUtils.hasText(body)) {
UserLoginRequest loginRequest = JSON.parseObject(body, UserLoginRequest.class);
mobile = loginRequest.getMobile();
password = loginRequest.getPassword();
verifyCode = loginRequest.getVerifyCode();
}
// TODO 这里验证图形验证码 verifyCode 是否正确
UserAuthenticationToken token = new UserAuthenticationToken(
null, mobile, password);
// 这里进行下一步认证,会走到我们定义的 UserAuthenticationProvider 中
return this.getAuthenticationManager().authenticate(token);
}
- 主要看最后一句,它实际调用的是
ProviderManager
的authenticate()
方法,来了解下这个类。
ProviderManager
authenticate()
方法
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
Authentication result = null;
boolean debug = logger.isDebugEnabled();
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}
if (debug) {
logger.debug("Authentication attempt using "
+ provider.getClass().getName());
}
try {
result = provider.authenticate(authentication);
if (result != null) {
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;
}
}
}
-
这里循环所有的
provider
,每个provider都调用自己的supports()
方法判断是否支持认证,能支持认证的话就执行自己的authenticate()
方法进行认证 -
这里是典型的
策略模式+委托模式
的组合使用案例,如下UML类图,AuthenticationProvider
为策略接口,右边三个就是实际的策略类,实现了策略接口以及实现了supports()
和authenticate()
,中间两个就是我们自己定义的Provider,这就是为啥我们要实现AuthenticationProvider
接口。 -
左边的
ProviderManager
就是这些策略的委托类,所有的provider都会被收集到该类的providers
属性中,然后认证的时候由委托类循环所有策略,支持认证的话再调用对应的策略去认证。 -
所有的策略是什么时候被加入到委托类的
providers
中的?
在配置主类
WebSecurityConfig
中我们配了一段这个代码,就是在这里配置的。
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(userAuthenticationProvider())
.authenticationProvider(jwtAuthenticationProvider());
}
@Bean
protected AuthenticationProvider userAuthenticationProvider() throws Exception {
return new UserAuthenticationProvider();
}
@Bean
protected AuthenticationProvider jwtAuthenticationProvider() throws Exception {
return new JwtAuthenticationProvider(securityConfig);
}
ProviderManager
是什么时候被谁创建的?
在 Spring Security源码(二):建造者详解 中我们留下个小悬念,主要建造者就这三个,
WebSecurity
建造的是核心过滤器FilterChainProxy,HttpSecurity
建造的是核心过滤器里的其中一条过滤器链,而AuthenticationManagerBuilder
就是本次的主角了,它就是建造ProviderManager
用的。(图片内容为之前文章截图)
ProviderManager
它是由AuthenticationManagerBuilder
在启动的时候被创建的,它实现了performBuild()
方法,关于启动流程请看之前的文章,文末会放出地址。
@Override
protected ProviderManager performBuild() throws Exception {
if (!isConfigured()) {
logger.debug("No authenticationProviders and no parentAuthenticationManager defined. Returning null.");
return null;
}
ProviderManager providerManager = new ProviderManager(authenticationProviders,
parentAuthenticationManager);
if (eraseCredentials != null) {
providerManager.setEraseCredentialsAfterAuthentication(eraseCredentials);
}
if (eventPublisher != null) {
providerManager.setAuthenticationEventPublisher(eventPublisher);
}
providerManager = postProcess(providerManager);
return providerManager;
}
二、接口认证流程
流程概述
- 认证流程其实和登录授权的流程几乎一致,大致也是类似以下,请参考具体登录流程
➜ filter :
JwtAuthenticationFilter
➜ provider :JwtAuthenticationProvider
➜ handler :JwtRefreshSuccessHandler/HttpStatusLoginFailureHandler
但是,但是,还是有区别的,会发现登录授权和接口认证的两个filter继承了不同的类,来看看为什么这么处理。
为什么继承不同的类?
先思考个问题 :
登录授权的目的和接口认证的最终目的是啥?
-
登录授权的目的是
获取JWT令牌
,接口认证的目的是访问后台资源
,只不过访问后台资源之前需要认证请求的合法性,即校验JWT成功之后才能访问后台资源。 -
再来回顾请求会走过的过滤器链:
-
对于 登录授权 ,
请求不会走完所有的过滤器
,它按顺序走前面的过滤器一直走完UserAuthenticationFilter
过滤器就直接返回了,不再继续往下走。 -
对于 接口认证 ,
请求会走完所有的过滤器
(前提不会在认证的时候报异常,比如报JWT不存在的话也会直接返回报错信息),并且是由最后一个FilterSecurityInterceptor
过滤器来判断最终是否能否访问后台接口资源的,也就是说走完了JwtAuthenticationFilter
过滤器还会继续往下走,直到最后一个过滤器判断请求不合法,或者请求认证成功返回接口资源。 -
前面提到
AbstractAuthenticationProcessingFilter
的控制流程(doFilter()
)不会继续往下走过滤器,所以这就是为什么UserAuthenticationFilter
能继承AbstractAuthenticationProcessingFilter
而JwtAuthenticationFilter
并不能继承这个,不会往下走过滤器也就是不会调用:
chain.doFilter(request, response);
- 所以,接口认证的
JwtAuthenticationFilter
需要自己去实现控制认证流程,来看一下他的doFilter()
方法:
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// 是否是白名单URL
if (permissiveRequest(request)) {
filterChain.doFilter(request, response);
return;
}
Authentication authResult = null;
AuthenticationException failed = null;
try {
String token = getJwtToken(request);
if (StringUtils.isNotBlank(token)) {
JwtAuthenticationToken authToken = new JwtAuthenticationToken(JWT.decode(token));
authResult = this.getAuthenticationManager().authenticate(authToken);
} else {
failed = LoginAuthenticationException.JWT_IS_EMPTY;
}
} catch (JWTDecodeException e) {
logger.error("JWT format error", e);
failed = LoginAuthenticationException.JWT_FORMAT_ERROR;
} catch (InternalAuthenticationServiceException e) {
logger.error("An internal error occurred while trying to authenticate the user.");
failed = LoginAuthenticationException.AUTH_ERROR;
} catch (AuthenticationException e) {
failed = e;
}
if (authResult != null) {
successfulAuthentication(request, response, filterChain, authResult);
} else {
// 是否是匿名用户访问的URL
if (!anonymityRequest(request)) {
unsuccessfulAuthentication(request, response, failed);
return;
}
}
filterChain.doFilter(request, response);
}
- 具体实现在前两章已说明,主要看最后一句,认证完之后还会往下走剩下的过滤器。当然如果认证的时候就已经报错了,也是直接return,不会继续走剩下的过滤器。
三、总结
- 登录授权和接口认证其实主要就是调用两个Filter的 dofilter() 方法,里面都包含了流程上的控制。
- 关注
ProvierManager
类,主要作用是Provider的委托类。 - 注意登录授权和接口认证Filter的实现,注意整体上的流程,登录授权的目的是获取JWT令牌,不会再走剩下的过滤器,接口认证成功的话会走完所有的的过滤器,再返回后台资源。
四、系列文章
Spring Security 系列
- 《手把手教你如何使用Spring Security(上):登录授权》
- 《手把手教你如何使用Spring Security(中):接口认证》
- 《手把手教你如何使用Spring Security(下):访问控制》
- 《Spring Security源码(一):整体框架设计》
- 《Spring Security源码(二):建造者详解》
- 《Spring Security源码(三):HttpSecurity详解》
- 《Spring Security源码(四):配置器详解》
- 《Spring Security源码(五):FilterChainProxy是如何创建的?》
- 《Spring Security源码(六):FilterChainProxy是如何运行的?》
- 《Spring Security源码(七):设计模式在框架中的应用》
- 《Spring Security源码(八):登录认证源码流程》
- 《Spring Security源码(九):过滤器链上的过滤器是如何排序的?》
- 《Spring Security源码(十):权限访问控制是如何做到的?》
Spring Security OAuth 系列
Java 面试宝典是大明哥全力打造的 Java 精品面试题,它是一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅只是一道道面试题,而是一套完整的 Java 知识体系,一套你 Java 知识点的扫盲贴。
它的内容包括:
- 大厂真题:Java 面试宝典里面的题目都是最近几年的高频的大厂面试真题。
- 原创内容:Java 面试宝典内容全部都是大明哥原创,内容全面且通俗易懂,回答部分可以直接作为面试回答内容。
- 持续更新:一次购买,永久有效。大明哥会持续更新 3+ 年,累计更新 1000+,宝典会不断迭代更新,保证最新、最全面。
- 覆盖全面:本宝典累计更新 1000+,从 Java 入门到 Java 架构的高频面试题,实现 360° 全覆盖。
- 不止面试:内容包含面试题解析、内容详解、知识扩展,它不仅仅只是一份面试题,更是一套完整的 Java 知识体系。
- 宝典详情:https://www.yuque.com/chenssy/sike-java/xvlo920axlp7sf4k
- 宝典总览:https://www.yuque.com/chenssy/sike-java/yogsehzntzgp4ly1
- 宝典进展:https://www.yuque.com/chenssy/sike-java/en9ned7loo47z5aw
目前 Java 面试宝典累计更新 400+ 道,总字数 42w+。大明哥还在持续更新中,下图是大明哥在 2024-12 月份的更新情况:
想了解详情的小伙伴,扫描下面二维码加大明哥微信【daming091】咨询
同时,大明哥也整理一套目前市面最常见的热点面试题。微信搜[大明哥聊 Java]或扫描下方二维码关注大明哥的原创公众号[大明哥聊 Java] ,回复【面试题】 即可免费领取。