Spring Security 源码解析(一)AbstractAuthenticationProcessingFilter
# 前言
最近在做 Spring OAuth2 登录,并在登录之后保存 Cookies。具体而言就是 Spring OAuth2 和 Spring Security 集成。Google一下竟然没有发现一种能满足我的要求。最终只有研究源码了。
有时间会画个 UML 图。
# 一些基础知识
- Spring Security 验证身份的方式是利用 Filter,再加上 HttpServletRequest 的一些信息进行过滤。
- 类 Authentication 保存的是身份认证信息。
- 类 AuthenticationProvider 提供身份认证途径。
- 类 AuthenticationManager 保存的 AuthenticationProvider 集合,并调用 AuthenticationProvider 进行身份认证。
# AbstractAuthenticationProcessingFilter
## 设计模式
### 抽象工厂模式
AbstractAuthenticationProcessingFilter 是一个抽象类,主要的功能是身份认证。OAuth2ClientAuthenticationProcessingFilter(Spriing OAuth2)、RememberMeAuthenticationFilter(RememberMe)都继承了 AbstractAuthenticationProcessingFilter ,并重写了方法 attemptAuthentication 进行身份认证。
/**
* Performs actual authentication. 进行真正的认证。
* <p>
* The implementation should do one of the following: 具体实现需要做如下事情:
* <ol>
* <li>Return a populated authentication token for the authenticated user, indicating
* successful authentication</li> 返回一个具体的 Authentication认证对象。
* <li>Return null, indicating that the authentication process is still in progress.
* Before returning, the implementation should perform any additional work required to
* complete the process.</li> 返回 null,表示实现的子类不能处理该身份认证,还需要别的类进行身份认证(往 FilterChain 传递)。
* <li>Throw an <tt>AuthenticationException</tt> if the authentication process fails</li> 抛出异常 AuthenticationException 表示认证失败。
* </ol>
*
* @param request from which to extract parameters and perform the authentication
* @param response the response, which may be needed if the implementation has to do a
* redirect as part of a multi-stage authentication process (such as OpenID).
*
* @return the authenticated user token, or null if authentication is incomplete.
*
* @throws AuthenticationException if authentication fails.
*/
public abstract Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException, IOException,
ServletException;
这个方法的目的很明确,就是需要子类提供身份认证的具体实现。子类根据 HttpServletRequest 等信息进行身份认证,并返回 Authentication 对象、 null、异常,分别表示认证成功返回的身份认证信息、需要其他 Filter 继续进行身份认证、认证失败。下面是一个 OAuth2ClientAuthenticationProcessingFilter 对于方法 attemptAuthentication 的实现,具体代码的行为就不解释了。
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException, IOException, ServletException { OAuth2AccessToken accessToken;
try {
accessToken = restTemplate.getAccessToken();
} catch (OAuth2Exception e) {
BadCredentialsException bad = new BadCredentialsException("Could not obtain access token", e);
publish(new OAuth2AuthenticationFailureEvent(bad));
throw bad;
}
try {
OAuth2Authentication result = tokenServices.loadAuthentication(accessToken.getValue());
if (authenticationDetailsSource!=null) {
request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE, accessToken.getValue());
request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_TYPE, accessToken.getTokenType());
result.setDetails(authenticationDetailsSource.buildDetails(request));
}
publish(new AuthenticationSuccessEvent(result));
return result;
}
catch (InvalidTokenException e) {
BadCredentialsException bad = new BadCredentialsException("Could not obtain user details from token", e);
publish(new OAuth2AuthenticationFailureEvent(bad));
throw bad;
} }
至于方法 attemptAuthentication 是怎么被调用的?身份认证流程很简单,但是身份认证完成之前、完成之后,也需要做很多的操作。大部分操作都是一尘不变的,身份认证之前确认是否要进行身份验证、保存身份认证信息、成功处理、失败处理等。具体流程,在下面的方法中体现。可以看出这就是个工厂,已经确定好身份认证的流程,所以我们需要做的事情就是重写身份认证机制(方法 attemptAuthentication)就可以了。
/**
* Invokes the
* {@link #requiresAuthentication(HttpServletRequest, HttpServletResponse)
* requiresAuthentication} method to determine whether the request is for
* authentication and should be handled by this filter. If it is an authentication
* request, the
* {@link #attemptAuthentication(HttpServletRequest, HttpServletResponse)
* attemptAuthentication} will be invoked to perform the authentication. There are
* then three possible outcomes:
* <ol>
* <li>An <tt>Authentication</tt> object is returned. The configured
* {@link SessionAuthenticationStrategy} will be invoked (to handle any
* session-related behaviour such as creating a new session to protect against
* session-fixation attacks) followed by the invocation of
* {@link #successfulAuthentication(HttpServletRequest, HttpServletResponse, FilterChain, Authentication)}
* method</li>
* <li>An <tt>AuthenticationException</tt> occurs during authentication. The
* {@link #unsuccessfulAuthentication(HttpServletRequest, HttpServletResponse, AuthenticationException)
* unsuccessfulAuthentication} method will be invoked</li>
* <li>Null is returned, indicating that the authentication process is incomplete. The
* method will then return immediately, assuming that the subclass has done any
* necessary work (such as redirects) to continue the authentication process. The
* assumption is that a later request will be received by this method where the
* returned <tt>Authentication</tt> object is not null.
* </ol>
*/
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res; // 是否需要身份认证
if (!requiresAuthentication(request, response)) {
// 不需要身份认证,传递到 FilterChain 继续过滤
chain.doFilter(request, response); return;
} if (logger.isDebugEnabled()) {
logger.debug("Request is to process authentication");
} Authentication authResult; try {
// 进行身份认证,该方法需要子类重写
authResult = attemptAuthentication(request, response);
if (authResult == null) {
// return immediately as subclass has indicated that it hasn't completed
// authentication
return;
}
// 身份认证成功,保存 session
sessionStrategy.onAuthentication(authResult, request, response);
}
// 身份认证代码出错
catch (InternalAuthenticationServiceException failed) {
logger.error(
"An internal error occurred while trying to authenticate the user.",
failed);
// 身份认证失败一系列事物处理,包括调用 RememberMeServices 等
unsuccessfulAuthentication(request, response, failed); return;
}
// 身份认证失败异常
catch (AuthenticationException failed) {
// Authentication failed
// 身份认证失败一系列事物处理,包括调用 RememberMeServices 等
unsuccessfulAuthentication(request, response, failed); return;
} // Authentication success
// 身份认证成功之后是否需要传递到 FilterChain
if (continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
} // 身份认证成功一系列事物处理,包括调用 RememberMeServices 等
successfulAuthentication(request, response, chain, authResult);
}
}
### 策略模式
这里还可以看一下方法 doFilter 的内部调用,比如下面这个方法。
/**
* Default behaviour for successful authentication.
* <ol>
* <li>Sets the successful <tt>Authentication</tt> object on the
* {@link SecurityContextHolder}</li>
* <li>Informs the configured <tt>RememberMeServices</tt> of the successful login</li>
* <li>Fires an {@link InteractiveAuthenticationSuccessEvent} via the configured
* <tt>ApplicationEventPublisher</tt></li>
* <li>Delegates additional behaviour to the {@link AuthenticationSuccessHandler}.</li>
* </ol>
*
* Subclasses can override this method to continue the {@link FilterChain} after
* successful authentication.
* @param request
* @param response
* @param chain
* @param authResult the object returned from the <tt>attemptAuthentication</tt>
* method.
* @throws IOException
* @throws ServletException
*/
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, Authentication authResult)
throws IOException, ServletException { if (logger.isDebugEnabled()) {
logger.debug("Authentication success. Updating SecurityContextHolder to contain: "
+ authResult);
} // 认证成功设置身份认证信息
SecurityContextHolder.getContext().setAuthentication(authResult); // RememberMeServices 设置成功登录信息,如 Cookie 等
rememberMeServices.loginSuccess(request, response, authResult); // 认证成功发送事件
// Fire event
if (this.eventPublisher != null) {
eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
authResult, this.getClass()));
} // 认证成功处理器
successHandler.onAuthenticationSuccess(request, response, authResult);
}
Spring Security 还是很贴心的把这个方法的修饰符设定成了 protected,以满足我们重写身份认证成功之后的机制,虽然大多数情况下并不需要。不需要的原因是认证成功之后的流程基本最多也就是这样,如果想改变一些行为,可以直接传递给 AbstractAuthenticationProcessingFilter 一些具体实现即可,如 AuthenticationSuccessHandler(认证成功处理器)。根据在这个处理器内可以进行身份修改、返回结果修改等行为。下面是该对象的定义。
private AuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();
各种各样的 AuthenticationSuccessHandler 可以提供多种多样的认证成功行为,这是一种策略模式。
# 后记
Spring Security 采取了多种设计模式,这是 Spring 家族代码的一贯特性。让人比较着急的是,Spring Security 虽然可以做到开箱即用,但是想要自定义代码的话,必须要熟悉 Spring Security 代码。比如如何使用 RememberMeServices。RememberMeService 有三个方法,登录成功操作、登录失败操作、自动登录操作。你可以重写这些方法,但你如果不看源码,你无法得知这些方法会在什么时候调用、在哪个 Filter 中调用、需要做什么配置。
Spring Security 源码解析(一)AbstractAuthenticationProcessingFilter的更多相关文章
- Spring Security源码解析一:UsernamePasswordAuthenticationFilter之登录流程
一.前言 spring security安全框架作为spring系列组件中的一个,被广泛的运用在各项目中,那么spring security在程序中的工作流程是个什么样的呢,它是如何进行一系列的鉴权和 ...
- Spring Security 源码解析(一)
上篇 Spring Security基本配置已讲述了Spring Security最简单的配置,本篇将开始分析其基本原理 在上篇中可以看到,在访问 http://localhost:18081/use ...
- spring事务源码解析
前言 在spring jdbcTemplate 事务,各种诡异,包你醍醐灌顶!最后遗留了一个问题:spring是怎么样保证事务一致性的? 当然,spring事务内容挺多的,如果都要讲的话要花很长时间, ...
- Spring Security 源码分析(四):Spring Social实现微信社交登录
社交登录又称作社会化登录(Social Login),是指网站的用户可以使用腾讯QQ.人人网.开心网.新浪微博.搜狐微博.腾讯微博.淘宝.豆瓣.MSN.Google等社会化媒体账号登录该网站. 前言 ...
- Spring IoC源码解析之invokeBeanFactoryPostProcessors
一.Bean工厂的后置处理器 Bean工厂的后置处理器:BeanFactoryPostProcessor(触发时机:bean定义注册之后bean实例化之前)和BeanDefinitionRegistr ...
- Spring IoC源码解析之getBean
一.实例化所有的非懒加载的单实例Bean 从org.springframework.context.support.AbstractApplicationContext#refresh方法开发,进入到 ...
- Spring系列(五):Spring AOP源码解析
一.@EnableAspectJAutoProxy注解 在主配置类中添加@EnableAspectJAutoProxy注解,开启aop支持,那么@EnableAspectJAutoProxy到底做了什 ...
- Spring系列(六):Spring事务源码解析
一.事务概述 1.1 什么是事务 事务是一组原子性的SQL查询,或者说是一个独立的工作单元.要么全部执行,要么全部不执行. 1.2 事务的特性(ACID) ①原子性(atomicity) 一个事务必须 ...
- Spring系列(三):Spring IoC源码解析
一.Spring容器类继承图 二.容器前期准备 IoC源码解析入口: /** * @desc: ioc原理解析 启动 * @author: toby * @date: 2019/7/22 22:20 ...
随机推荐
- Linux下用户和组管理
用户与组之间的关系是,组下面有若干个用户,每个用户必须从属于唯一一个组.组可以理解为权限的集合.用户管理的命令有:useradd, userdel, usermod, passwd, chsh, ch ...
- python小练习之读取文件写入excel
文件是个json文件 内容为: 导入excel后的格式为 屡一下思路 一步步怎么实现: 1 首先需要读取json文件 然后将读取的内容转为字典 2 将excel的列名写入一个list中 然后遍历执行写 ...
- Luogu P1747 好奇怪的游戏
题目背景 <爱与愁的故事第三弹·shopping>娱乐章. 调调口味来道水题. 题目描述 爱与愁大神坐在公交车上无聊,于是玩起了手机.一款奇怪的游戏进入了爱与愁大神的眼帘:***(游戏名被 ...
- qwe 简易深度框架
qwe github地址 简介 简单的深度框架,参考Ng的深度学习课程作业,使用了keras的API设计. 方便了解网络具体实现,避免深陷于成熟框架的细节和一些晦涩的优化代码. 网络层实现了Dense ...
- R语言︱SNA-社会关系网络—igraph包(中心度、中心势)(二)
每每以为攀得众山小,可.每每又切实来到起点,大牛们,缓缓脚步来俺笔记葩分享一下吧,please~ --------------------------- SNA社会关系网络分析中,关键的就是通过一些指 ...
- HighCharts之2D饼图
HighCharts之2D饼图 1. HighCharts之2D饼图源码 <!DOCTYPE html> <html> <head> <meta charse ...
- CF#418 Div2 D. An overnight dance in discotheque
一道树形dp裸体,自惭形秽没有想到 首先由于两两圆不能相交(可以相切)就决定了一个圆和外面一个圆的包含关系 又可以发现这样的树中,奇数深度的圆+S,偶数深度的圆-S 就可以用树形dp 我又写挫了= = ...
- Linux压缩、解压文件
对于.tar格式的文件压缩和解压比较常用,今天对于.zip格式的文件用同样的命令无效.真是被自己蠢到了,忽略了后缀格式... 1.对于tar格式文件 压缩: tar –zcvf 压缩完后的名称 被压 ...
- GridView中使用 jQuery DatePicker (UpdatePanel)
1.无UpdatePanel 1.代码 <script> $(function () { $('.myDatePickerClass').datepicker({ dateFormat ...
- C#中(int)、Conver.Toint32()、int.Parse()三种类型转换方式的区别与联系--C#基础知识
自己刚学习C#,总结了一些知识,想分享给大家.毕竟刚学习这门语言,学得不深,如果哪里有错误,请帮忙指出一下哈,谢谢! 1.(int)可用于单精度.双精度等其他数值类型的转换(到整型int),不能用于转 ...