概要

前面一节,通过简单配置即可实现SpringSecurity表单认证功能,而今天这一节将通过阅读源码的形式来学习SpringSecurity是如何实现这些功能, 前方高能预警,本篇分析源码篇幅较长

过滤器链

前面我说过SpringSecurity是基于过滤器链的形式,那么我解析将会介绍一下具体有哪些过滤器。

Filter Class 介绍
SecurityContextPersistenceFilter 判断当前用户是否登录
CrsfFilter 用于防止csrf攻击
LogoutFilter 处理注销请求
UsernamePasswordAuthenticationFilter 处理表单登录的请求(也是我们今天的主角)
BasicAuthenticationFilter 处理http basic认证的请求

由于过滤器链中的过滤器实在太多,我没有一一列举,调了几个比较重要的介绍一下。

通过上面我们知道SpringSecurity对于表单登录的认证请求是交给了UsernamePasswordAuthenticationFilter处理的,那么具体的认证流程如下:

从上图可知,UsernamePasswordAuthenticationFilter继承于抽象类AbstractAuthenticationProcessingFilter

具体认证是:

  1. 进入doFilter方法,判断是否要认证,如果需要认证则进入attemptAuthentication方法,如果不需要直接结束
  2. attemptAuthentication方法中根据username跟password构造一个UsernamePasswordAuthenticationToken对象(此时的token是未认证的),并且将它交给ProviderManger来完成认证。
  3. ProviderManger中维护这一个AuthenticationProvider对象列表,通过遍历判断并且最后选择DaoAuthenticationProvider对象来完成最后的认证。
  4. DaoAuthenticationProvider根据ProviderManger传来的token取出username,并且调用我们写的UserDetailsService的loadUserByUsername方法从数据库中读取用户信息,然后对比用户密码,如果认证通过,则返回用户信息也是就是UserDetails对象,在重新构造UsernamePasswordAuthenticationToken(此时的token是 已经认证通过了的)。

接下来我们将通过源码来分析具体的整个认证流程。

AbstractAuthenticationProcessingFilter

AbstractAuthenticationProcessingFilter 是一个抽象类。所有的认证认证请求的过滤器都会继承于它,它主要将一些公共的功能实现,而具体的验证逻辑交给子类实现,有点类似于父类设置好认证流程,子类负责具体的认证逻辑,这样跟设计模式的模板方法模式有点相似。

现在我们分析一下 它里面比较重要的方法

1、doFilter

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)throws IOException, ServletException {// 省略不相干代码。。。// 1、判断当前请求是否要认证if (!requiresAuthentication(request, response)) {// 不需要直接走下一个过滤器chain.doFilter(request, response);return;}try {// 2、开始请求认证,attemptAuthentication具体实现给子类,如果认证成功返回一个认证通过的Authenticaion对象authResult = attemptAuthentication(request, response);if (authResult == null) {return;}// 3、登录成功 将认证成功的用户信息放入session SessionAuthenticationStrategy接口,用于扩展sessionStrategy.onAuthentication(authResult, request, response);}catch (InternalAuthenticationServiceException failed) {//2.1、发生异常,登录失败,进入登录失败handler回调unsuccessfulAuthentication(request, response, failed);return;}catch (AuthenticationException failed) {//2.1、发生异常,登录失败,进入登录失败处理器unsuccessfulAuthentication(request, response, failed);return;}// 3.1、登录成功,进入登录成功处理器。successfulAuthentication(request, response, chain, authResult);}
复制代码

2、successfulAuthentication

登录成功处理器

protected void successfulAuthentication(HttpServletRequest request,HttpServletResponse response, FilterChain chain, Authentication authResult)throws IOException, ServletException {//1、登录成功 将认证成功的Authentication对象存入SecurityContextHolder中//     SecurityContextHolder本质是一个ThreadLocalSecurityContextHolder.getContext().setAuthentication(authResult);//2、如果开启了记住我功能,将调用rememberMeServices的loginSuccess 将生成一个token//   将token放入cookie中这样 下次就不用登录就可以认证。具体关于记住我rememberMeServices的相关分析我                   们下面几篇文章会深入分析的。rememberMeServices.loginSuccess(request, response, authResult);// Fire event//3、发布一个登录事件。if (this.eventPublisher != null) {eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));}//4、调用我们自己定义的登录成功处理器,这样也是我们扩展得知登录成功的一个扩展点。successHandler.onAuthenticationSuccess(request, response, authResult);}
复制代码

3、unsuccessfulAuthentication

登录失败处理器

protected void unsuccessfulAuthentication(HttpServletRequest request,HttpServletResponse response, AuthenticationException failed)throws IOException, ServletException {//1、登录失败,将SecurityContextHolder中的信息清空SecurityContextHolder.clearContext();//2、关于记住我功能的登录失败处理rememberMeServices.loginFail(request, response);//3、调用我们自己定义的登录失败处理器,这里可以扩展记录登录失败的日志。failureHandler.onAuthenticationFailure(request, response, failed);}
复制代码

关于AbstractAuthenticationProcessingFilter主要分析就到这。我们可以从源码中知道,当请求进入该过滤器中具体的流程是

  1. 判断该请求是否要被认证
  2. 调用attemptAuthentication方法开始认证,由于是抽象方法具体认证逻辑给子类
  3. 如果登录成功,则将认证结果Authentication对象根据session策略写入session中,将认证结果写入到SecurityContextHolder,如果开启了记住我功能,则根据记住我功能,生成token并且写入cookie中,最后调用一个successHandler对象的方法,这个对象可以是我们配置注入的,用于处理我们的自定义登录成功的一些逻辑(比如记录登录成功日志等等)。
  4. 如果登录失败,则清空SecurityContextHolder中的信息,并且调用我们自己注入的failureHandler对象,处理我们自己的登录失败逻辑。

UsernamePasswordAuthenticationFilter

从上面分析我们可以知道,UsernamePasswordAuthenticationFilter是继承于AbstractAuthenticationProcessingFilter,并且实现它的attemptAuthentication方法,来实现认证具体的逻辑实现。接下来,我们通过阅读UsernamePasswordAuthenticationFilter的源码来解读,它是如何完成认证的。 由于这里会涉及UsernamePasswordAuthenticationToken对象构造,所以我们先看看UsernamePasswordAuthenticationToken的源码

1、UsernamePasswordAuthenticationToken

// 继承至AbstractAuthenticationToken
// AbstractAuthenticationToken主要定义一下在SpringSecurity中toke需要存在一些必须信息
// 例如权限集合  Collection<GrantedAuthority> authorities; 是否认证通过boolean authenticated = false;认证通过的用户信息Object details;
public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {// 未登录情况下 存的是用户名 登录成功情况下存的是UserDetails对象private final Object principal;// 密码private Object credentials;/*** 构造函数,用户没有登录的情况下,此时的authenticated是false,代表尚未认证*/public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {super(null);this.principal = principal;this.credentials = credentials;setAuthenticated(false);}/*** 构造函数,用户登录成功的情况下,多了一个参数 是用户的权限集合,此时的authenticated是true,代表认证成功*/public UsernamePasswordAuthenticationToken(Object principal, Object credentials,Collection<? extends GrantedAuthority> authorities) {super(authorities);this.principal = principal;this.credentials = credentials;super.setAuthenticated(true); // must use super, as we override}
}
复制代码

接下来我们就可以分析attemptAuthentication方法了。

2、attemptAuthentication

public Authentication attemptAuthentication(HttpServletRequest request,HttpServletResponse response) throws AuthenticationException {// 1、判断是不是post请求,如果不是则抛出AuthenticationServiceException异常,注意这里抛出的异常都在AbstractAuthenticationProcessingFilter#doFilter方法中捕获,捕获之后会进入登录失败的逻辑。if (postOnly && !request.getMethod().equals("POST")) {throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());}// 2、从request中拿用户名跟密码String username = obtainUsername(request);String password = obtainPassword(request);// 3、非空处理,防止NPE异常if (username == null) {username = "";}if (password == null) {password = "";}// 4、除去空格username = username.trim();// 5、根据username跟password构造出一个UsernamePasswordAuthenticationToken对象 从上文分析可知道,此时的token是未认证的。UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);// 6、配置一下其他信息 ip 等等setDetails(request, authRequest);//  7、调用ProviderManger的authenticate的方法进行具体认证逻辑return this.getAuthenticationManager().authenticate(authRequest);}
复制代码

ProviderManager

维护一个AuthenticationProvider列表,进行认证逻辑验证

1、authenticate

public Authentication authenticate(Authentication authentication)throws AuthenticationException {// 1、拿到token的类型。Class<? extends Authentication> toTest = authentication.getClass();AuthenticationException lastException = null;Authentication result = null;// 2、遍历AuthenticationProvider列表for (AuthenticationProvider provider : getProviders()) {// 3、AuthenticationProvider不支持当前token类型,则直接跳过if (!provider.supports(toTest)) {continue;}try {// 4、如果Provider支持当前token,则交给Provider完成认证。result = provider.authenticate(authentication);}catch (AccountStatusException e) {throw e;}catch (InternalAuthenticationServiceException e) {throw e;}catch (AuthenticationException e) {lastException = e;}}// 5、登录成功 返回登录成功的tokenif (result != null) {eventPublisher.publishAuthenticationSuccess(result);return result;}}
复制代码

AbstractUserDetailsAuthenticationProvider

1、authenticate

AbstractUserDetailsAuthenticationProvider实现了AuthenticationProvider接口,并且实现了部分方法,DaoAuthenticationProvider继承于AbstractUserDetailsAuthenticationProvider类,所以我们先来看看AbstractUserDetailsAuthenticationProvider的实现。

public abstract class AbstractUserDetailsAuthenticationProvider implementsAuthenticationProvider, InitializingBean, MessageSourceAware {// 国际化处理protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();/*** 对token一些检查,具体检查逻辑交给子类实现,抽象方法*/protected abstract void additionalAuthenticationChecks(UserDetails userDetails,UsernamePasswordAuthenticationToken authentication)throws AuthenticationException;/*** 认证逻辑的实现,调用抽象方法retrieveUser根据username获取UserDetails对象*/public Authentication authenticate(Authentication authentication)throws AuthenticationException {// 1、获取usernmaeString username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED": authentication.getName();// 2、尝试去缓存中获取UserDetails对象UserDetails user = this.userCache.getUserFromCache(username);// 3、如果为空,则代表当前对象没有缓存。if (user == null) {cacheWasUsed = false;try {//4、调用retrieveUser去获取UserDetail对象,为什么这个方法是抽象方法大家很容易知道,如果UserDetail信息存在关系数据库 则可以重写该方法并且去关系数据库获取用户信息,如果UserDetail信息存在其他地方,可以重写该方法用其他的方法去获取用户信息,这样丝毫不影响整个认证流程,方便扩展。user = retrieveUser(username,(UsernamePasswordAuthenticationToken) authentication);}catch (UsernameNotFoundException notFound) {// 捕获异常 日志处理 并且往上抛出,登录失败。if (hideUserNotFoundExceptions) {throw new BadCredentialsException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials","Bad credentials"));}else {throw notFound;}}}try {// 5、前置检查  判断当前用户是否锁定,禁用等等preAuthenticationChecks.check(user);// 6、其他的检查,在DaoAuthenticationProvider是检查密码是否一致additionalAuthenticationChecks(user,(UsernamePasswordAuthenticationToken) authentication);}catch (AuthenticationException exception) {}// 7、后置检查,判断密码是否过期postAuthenticationChecks.check(user);// 8、登录成功通过UserDetail对象重新构造一个认证通过的Token对象return createSuccessAuthentication(principalToReturn, authentication, user);}protected Authentication createSuccessAuthentication(Object principal,Authentication authentication, UserDetails user) {// 调用第二个构造方法,构造一个认证通过的Token对象UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(principal, authentication.getCredentials(),authoritiesMapper.mapAuthorities(user.getAuthorities()));result.setDetails(authentication.getDetails());return result;}}
复制代码

接下来我们具体看看retrieveUser的实现,没看源码大家应该也可以知道,retrieveUser方法应该是调用UserDetailsService去数据库查询是否有该用户,以及用户的密码是否一致。

DaoAuthenticationProvider

DaoAuthenticationProvider 主要是通过UserDetailService来获取UserDetail对象。

1、retrieveUser

protected final UserDetails retrieveUser(String username,UsernamePasswordAuthenticationToken authentication)throws AuthenticationException {try {// 1、调用UserDetailsService接口的loadUserByUsername方法获取UserDeail对象UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);// 2、如果loadedUser为null 代表当前用户不存在,抛出异常 登录失败。if (loadedUser == null) {throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");}// 3、返回查询的结果return loadedUser;}}
复制代码

2、additionalAuthenticationChecks

protected void additionalAuthenticationChecks(UserDetails userDetails,UsernamePasswordAuthenticationToken authentication)throws AuthenticationException {// 1、如果密码为空,则抛出异常、if (authentication.getCredentials() == null) {throw new BadCredentialsException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials","Bad credentials"));}// 2、获取用户输入的密码String presentedPassword = authentication.getCredentials().toString();// 3、调用passwordEncoder的matche方法 判断密码是否一致if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {logger.debug("Authentication failed: password does not match stored value");// 4、如果不一致 则抛出异常。throw new BadCredentialsException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials","Bad credentials"));}}
复制代码

总结

至此,整认证流程已经分析完毕,大家如果有什么不懂可以关注我的公众号一起讨论。

学习是一个漫长的过程,学习源码可能会很困难但是只要努力一定就会有获取,大家一致共勉。

【SpringSecurity系列02】SpringSecurity 表单认证逻辑源码解读相关推荐

  1. 开源自定义表单提交系统源码 支持自定义字段功能强大

    分享一个强大的可以自定义字段的表单提交系统源码,完全开源可以二次开发,支持新建N个表单主题,不限制数量,解决所有表单的应用场景.自定义表单模型,任何类型都支持,功能十分强大. 此套万能表单系统厉害了, ...

  2. 开源表单网站系统源码支持自定义字段提交

    分享一个完全开源的自定义表单提交系统源码,功能强大,支持三级联动,支持在线付费报名,带完整搭建教程. 春哥万能自定义表单系统是支持自定义的万能表单系统,支持普通表单.付费报名.预约服务等三合一功能,支 ...

  3. 【.net+jquery】绘制自定义表单(含源码)

    前言 两年前的时候就想做一个类似的功能,当时思路大家都讨论好了,诸多原因最终还是夭折了.没想到两年多后再这有重新提出要写一个绘制表单的功能.对此也是有点小激动呢?总共用时8.5天的时间基本功能也就实现 ...

  4. HTML5 CSS登录注册表单界面示例源码

    先上图 高质量前端技术交流,有问必答q群:579720104 html <!DOCTYPE html> <html lang="zh"> <head& ...

  5. 非常实用表单设计器及智能表单引擎(项目源码)

    非常实用表单设计器及智能表单引擎(项目源码) 项目集成了智能表单设计.工作流可视化设计(js源码可直接引用)以及统计的定制设计 介绍一个非常实用(已经应用许多项目)表单设计器及智能表单引擎的开发平台, ...

  6. 非常不错的表单设计器(源码)

    该设计器是开源的,用户可以根据自己的需要进行控件的二次开发.官网可以下载demo以及开发说明.官网地址:http://formdesign.leipi.org/

  7. SharePoint 2013 表单认证使用ASP.Net配置工具添加用户

    前 言 上面一篇博客,我们了解到如何为SharePoint 2013配置表单身份认证,但是添加用户是一个麻烦事儿:其实,我们还可以用Asp.Net的配置工具,为SharePoint 2013添加表单用 ...

  8. 为ASP.NET MVC配置基于Active Directory的表单认证方式

    为ASP.NET MVC配置基于Active Directory的表单认证方式 最近一直在研究基于Active Directory的表单认证方式,同时也在关注ASP.NET MVC的情况,同时也在应用 ...

  9. ASP.NET 安全认证(二)——灵活运用 Form 表单认证中的 deny 与 allow 及保护 .htm 等文件 ....

    话说上回,简单地说了一下 Form 表单认证的用法.或许大家觉得太简单,对那些大内高手来说应该是"洒洒水啦""小 Kiss 啦(小意思)".今天咱们来点的花样吧 ...

最新文章

  1. 图灵5月书讯:阅读是对自己沉默的爱
  2. Android之如何设置TextView中不同字段的字体颜色
  3. java定义一个指针类型变量赋值吗,C++中指针的数据类型和运算相关知识小结
  4. AOE网(求关键路径)(c/c++)
  5. poj 2892---Tunnel Warfare(线段树单点更新、区间合并)
  6. 技巧:教你一招优化 Go GC
  7. linux 下安装fbprophet
  8. qthread run结束了算销毁吗_Java线程的run()方法和start()方法有什么区别?
  9. bzip2recover cat chattr chgrp chmod
  10. python数据格式化后导入数据库_MySQL导入格式化数据
  11. Windows蓝屏代码及解决方案最全合集
  12. zip压缩文件加密码以及Office文件打开需要密码
  13. Opencv裁剪图片大小
  14. BEEF的简介与使用
  15. Unity pc端内嵌网页插件Embedded Browser基本使用流程
  16. 【CAN】PCAN连线
  17. 华为杭州研究所面试记
  18. 全面掌握前端框架Vue.js
  19. Python复习的知识点
  20. 【开源STM32自平衡小车】 教你如何自己DIY一辆双轮自平衡小车

热门文章

  1. C++ 引用和指针有什么区别?
  2. Python 列表 reverse( )方法
  3. 7-8 连续因子 (20 分)
  4. 7-172 一元多项式求导 (20 分)
  5. 上海三校生应用计算机考试试卷,三校生计算机考试试题.doc
  6. php mysql子查询,mysql子查询命令
  7. 六元均匀直线阵的各元间距为_小间距led显示屏的封装方式有哪些?本文带你了解!...
  8. 部署Linux单机kafka踩坑
  9. C++学习 ---- 系列文章
  10. Python2.7编程基础(博主推荐)