SpringSecurity - 启动流程分析(八)- CsrfFilter 过滤器
活动地址:CSDN21天学习挑战赛
前言
在 SpringSecurity - 启动流程分析(五)- (七) 这几篇文章中,我们主要是对 UsernamePasswordAuthenticationFilter
这一个 Filter
做了源码分析,接下来我们来看一下 CsrfFilter
分析
HttpSecurity
在 HttpSecurity
中加载默认配置:
public CsrfConfigurer<HttpSecurity> csrf() throws Exception {ApplicationContext context = getContext();return getOrApply(new CsrfConfigurer<>(context));
}
CsrfConfigurer
public final class CsrfConfigurer<H extends HttpSecurityBuilder<H>>extends AbstractHttpConfigurer<CsrfConfigurer<H>, H> {// 1、初始化 CsrfTokenRepositoryprivate CsrfTokenRepository csrfTokenRepository = new LazyCsrfTokenRepository(new HttpSessionCsrfTokenRepository());// 2、初始化 RequestMatcherprivate RequestMatcher requireCsrfProtectionMatcher = CsrfFilter.DEFAULT_CSRF_MATCHER;// 可以自定义忽略 csrf 验证的 RequestMatcherprivate List<RequestMatcher> ignoredCsrfProtectionMatchers = new ArrayList<>();...@SuppressWarnings("unchecked")@Override// 核心方法public void configure(H http) {// 这里初始化 CsrfFilter,并把 csrfTokenRepository 作为参数传递CsrfFilter filter = new CsrfFilter(this.csrfTokenRepository);// 设置 filter 的一些属性...http.addFilter(filter);}
}
1、CsrfTokenRepository
以 LazyCsrfTokenRepository
在 HttpSessionCsrfTokenRepository
外包了一层,作用是:延迟保存新的 CsrfToken
直到上一次生成的 CsrfToken
被访问。
这里可能还不理解什么是
CsrfToken
,先知道有这个概念就行,继续往下看
private CsrfTokenRepository csrfTokenRepository = new LazyCsrfTokenRepository(new HttpSessionCsrfTokenRepository());
2、requireCsrfProtectionMatcher
这个属性里面是需要 CSRF
保护的请求匹配器 CsrfFilter.DEFAULT_CSRF_MATCHER
:
public static final RequestMatcher DEFAULT_CSRF_MATCHER = new DefaultRequiresCsrfMatcher();private static final class DefaultRequiresCsrfMatcher implements RequestMatcher {private final HashSet<String> allowedMethods = new HashSet<>(Arrays.asList("GET", "HEAD", "TRACE", "OPTIONS"));@Overridepublic boolean matches(HttpServletRequest request) {// 注意这里是取反,就是匹配除了上面几种请求方式之外的其他请求方式,比如 POST、PUT 等return !this.allowedMethods.contains(request.getMethod());}@Overridepublic String toString() {return "CsrfNotRequired " + this.allowedMethods;}}
CsrfFilter
/*** <p>* Applies* <a href="https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)" >CSRF</a>* protection using a synchronizer token pattern. Developers are required to ensure that* {@link CsrfFilter} is invoked for any request that allows state to change. Typically* this just means that they should ensure their web application follows proper REST* semantics (i.e. do not change state with the HTTP methods GET, HEAD, TRACE, OPTIONS).* </p>*/
public final class CsrfFilter extends OncePerRequestFilter {}
从源码注释中可以了解到关于 CSRF
相关的介绍,而且网上也有很多关于 CSRF
的介绍,这里就不赘述了,直接来分析一下其中的核心内容:
1、doFilterInternal()
首先看一下 doFilterInternal()
方法,这个是过滤器的入口方法:
@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)throws ServletException, IOException {request.setAttribute(HttpServletResponse.class.getName(), response);// 1、首先会加载一个 CsrfTokenCsrfToken csrfToken = this.tokenRepository.loadToken(request);boolean missingToken = (csrfToken == null);if (missingToken) {// 2、如果 session 中没有的话,会生成一个。通过 1.2.2 的分析,知道这里是内部类 SaveOnAccessCsrfTokencsrfToken = this.tokenRepository.generateToken(request);this.tokenRepository.saveToken(csrfToken, request, response);}request.setAttribute(CsrfToken.class.getName(), csrfToken);request.setAttribute(csrfToken.getParameterName(), csrfToken);// 如果符合 requireCsrfProtectionMatcher 里面定义的那些 GET、OPTION 等请求,直接放行if (!this.requireCsrfProtectionMatcher.matches(request)) {if (this.logger.isTraceEnabled()) {this.logger.trace("Did not protect against CSRF since request did not match "+ this.requireCsrfProtectionMatcher);}filterChain.doFilter(request, response);return;}// 获取请求头中的 _csrf 属性String actualToken = request.getHeader(csrfToken.getHeaderName());if (actualToken == null) {// 如果请求头中没有值,获取请求参数中的 X-CSRF-TOKEN 属性值actualToken = request.getParameter(csrfToken.getParameterName());}// 如果请求的信息中没有携带这些东西,就会抛出异常// 3、这里调用内部类 SaveOnAccessCsrfToken 的 getToken() 方法if (!equalsConstantTime(csrfToken.getToken(), actualToken)) {this.logger.debug(LogMessage.of(() -> "Invalid CSRF token found for " + UrlUtils.buildFullRequestUrl(request)));AccessDeniedException exception = (!missingToken) ? // session 中有 *.CSRF_TOKEN 这个属性的值,但是请求没有携带 csrf 相关的值,会抛出 InvalidCsrfTokenExceptionnew InvalidCsrfTokenException(csrfToken, actualToken)// session 中缺少 *.CSRF_TOKEN 这个属性的值会抛出 MissingCsrfTokenException: new MissingCsrfTokenException(actualToken);// 处理异常this.accessDeniedHandler.handle(request, response, exception);return;}filterChain.doFilter(request, response);
}
1.1、CsrfToken
CsrfToken csrfToken = this.tokenRepository.loadToken(request);
从上面对 CsrfConfigurer
的分析中,我们知道这个 tokenRepository
就是 new LazyCsrfTokenRepository(new HttpSessionCsrfTokenRepository())
,所以这里的 loadToken()
方法,是调用的 HttpSessionCsrfTokenRepository
中的重写方法:
private String sessionAttributeName = DEFAULT_CSRF_TOKEN_ATTR_NAME;private static final String DEFAULT_CSRF_TOKEN_ATTR_NAME = HttpSessionCsrfTokenRepository.class.getName().concat(".CSRF_TOKEN");@Override
public CsrfToken loadToken(HttpServletRequest request) {HttpSession session = request.getSession(false);if (session == null) {return null;}// 从 session 中获取指定属性的值return (CsrfToken) session.getAttribute(this.sessionAttributeName);
}
1.2、LazyCsrfTokenRepository
@Override
public CsrfToken generateToken(HttpServletRequest request) {// 1、这里的 delegate 是 HttpSessionCsrfTokenRepositoryreturn wrap(request, this.delegate.generateToken(request));// 通过 1.2.2 的分析,也就是说这个方法返回的是包了一层 DefaultCsrfToken 的 SaveOnAccessCsrfToken
}private CsrfToken wrap(HttpServletRequest request, CsrfToken token) {HttpServletResponse response = getResponse(request);// 2、这个 token 是生成的 DefaultCsrfTokenreturn new SaveOnAccessCsrfToken(this.delegate, request, response, token);
}
1.2.1、查看 HttpSessionCsrfTokenRepository
中的 generateToken()
方法:
private String parameterName = DEFAULT_CSRF_PARAMETER_NAME;private String headerName = DEFAULT_CSRF_HEADER_NAME;private static final String DEFAULT_CSRF_PARAMETER_NAME = "_csrf";private static final String DEFAULT_CSRF_HEADER_NAME = "X-CSRF-TOKEN";@Override
public CsrfToken generateToken(HttpServletRequest request) {// 创建了一个 CsrfTokenreturn new DefaultCsrfToken(this.headerName, this.parameterName, createNewToken());
}private String createNewToken() {return UUID.randomUUID().toString();
}
1.2.2、查看 LazyCsrfTokenRepository
的内部类 SaveOnAccessCsrfToken
中的构造方法:
private static final class SaveOnAccessCsrfToken implements CsrfToken {private transient CsrfTokenRepository tokenRepository;private transient HttpServletRequest request;private transient HttpServletResponse response;private final CsrfToken delegate;// 入参 CsrfTokenRepository 是 HttpSessionCsrfTokenRepository// 入参 CsrfToken 是 DefaultCsrfTokenSaveOnAccessCsrfToken(CsrfTokenRepository tokenRepository, HttpServletRequest request,HttpServletResponse response, CsrfToken delegate) {this.tokenRepository = tokenRepository;this.request = request;this.response = response;this.delegate = delegate;}@Overridepublic String getHeaderName() {return this.delegate.getHeaderName();}@Overridepublic String getParameterName() {return this.delegate.getParameterName();}@Overridepublic String getToken() {saveTokenIfNecessary();return this.delegate.getToken();}private void saveTokenIfNecessary() {if (this.tokenRepository == null) {return;}synchronized (this) {if (this.tokenRepository != null) {this.tokenRepository.saveToken(this.delegate, this.request, this.response);this.tokenRepository = null;this.request = null;this.response = null;}}}...
}
1.3、调用 SaveOnAccessCsrfToken
中的 getToken()
方法:
@Override
public String getToken() {saveTokenIfNecessary();// 2、返回 DefaultCsrfToken 中的 getToken() 方法的返回值return this.delegate.getToken();
}private void saveTokenIfNecessary() {if (this.tokenRepository == null) {return;}synchronized (this) {if (this.tokenRepository != null) {// tokenRepository 是 HttpSessionCsrfTokenRepository// delagate 是 DefaultCsrfToken// 1、核心流程,调用的是 HttpSessionCsrfTokenRepository 中的 saveToken 方法this.tokenRepository.saveToken(this.delegate, this.request, this.response);this.tokenRepository = null;this.request = null;this.response = null;}}
}
1.3.1、调用 HttpSessionCsrfTokenRepository
中的 saveToken()
方法:
private String sessionAttributeName = DEFAULT_CSRF_TOKEN_ATTR_NAME;private static final String DEFAULT_CSRF_TOKEN_ATTR_NAME = HttpSessionCsrfTokenRepository.class.getName().concat(".CSRF_TOKEN");@Override
public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response) {if (token == null) {HttpSession session = request.getSession(false);if (session != null) {session.removeAttribute(this.sessionAttributeName);}}else {HttpSession session = request.getSession();// 会保存到 session 中,用于之后请求对比 CsrfToken 中的值session.setAttribute(this.sessionAttributeName, token);}
}
总结
以上就是
SpringSecurity
中关于CSRF
防御的校验流程
分析
- 如果是
POST
、PUT
之类的由DefaultRequiresCsrfMatcher
定义的需要CSRF
拦截的请求,就必须在请求头
或者请求参数
中携带对应session
中存的值
SpringSecurity - 启动流程分析(八)- CsrfFilter 过滤器相关推荐
- Alian解读SpringBoot 2.6.0 源码(八):启动流程分析之刷新应用上下文(下)
目录 一.背景 1.1.刷新的整体调用流程 1.2.本文解读范围 二.初始化特定上下文子类中的其他特殊bean 2.1.初始化主体资源 2.2.创建web服务 三.检查监听器bean并注册它们 四.实 ...
- Alian解读SpringBoot 2.6.0 源码(八):启动流程分析之刷新应用上下文(中)
目录 一.背景 1.1.刷新的整体调用流程 1.2.本文解读范围 二.调用后处理器 2.1.调用在上下文中注册为beanFactory的后置处理器 2.2.invokeBeanFactoryPostP ...
- Alian解读SpringBoot 2.6.0 源码(八):启动流程分析之刷新应用上下文(上)
目录 一.背景 1.1.run方法整体流程 1.2.刷新的整体调用流程 1.3.本文解读范围 二.准备刷新 2.1.准备刷新的流程 2.2.初始化上下文环境中servlet相关属性源 2.3.校验re ...
- SpringBoot(十二)启动流程分析之创建应用上下文AnnotationConfigServletWebServerApplicationContext
SpringBoot版本:2.1.1 ==>启动流程分析汇总 接上篇博客Spring Boot 2.1.1(十一)启动流程分析之设置系统属性spring.beaninfo.ignore ...
- 基于 LPC1114 的 M0 启动流程分析
基于LPC1114的M0启动流程分析 作者:解琛 时间:2020 年 8 月 1 日 基于LPC1114的M0启动流程分析 一..s 启动进程 1.1 堆栈配置 1.2 中断向量表 1.3 定义中断服 ...
- Cocos2d-x3.3RC0的Android编译Activity启动流程分析
http://www.itnose.net/detail/6142692.html 本文将从引擎源码Jni分析Cocos2d-x3.3RC0的Android Activity的启动流程,下面是详细分析 ...
- Alian解读SpringBoot 2.6.0 源码(六):启动流程分析之创建应用上下文
目录 一.背景 1.1.run方法整体流程 1.2.本文解读范围 二.创建应用上下文 2.1.初始化入口 2.2.初始化AbstractApplicationContext 2.3.初始化Generi ...
- 解析并符号 读取dll_Spring IOC容器之XmlBeanFactory启动流程分析和源码解析
一. 前言 Spring容器主要分为两类BeanFactory和ApplicationContext,后者是基于前者的功能扩展,也就是一个基础容器和一个高级容器的区别.本篇就以BeanFactory基 ...
- Zygote进程启动流程分析
文中的源代码版本为api23 Zygote进程启动流程分析 先说结论,zygote进程启动过程中主要做了下面这些事情: 启动DVM虚拟机 预加载部分资源,如一些通用类.通用资源.共享库等 启动syst ...
最新文章
- java类的基本组成和使用,Java架构师成长路线
- php 循环链表,PHP实现循环链表功能
- 数据科学中的Docker
- mariadb 配置mysql_mysql-mariadb实践中用到的配置(不断完善中)
- Postman批量接口测试
- Tiny之Web工程构建
- oracle数据库pfile文件,Oracle pfile/spfile参数文件详解
- 面向程序员编程——精研排序算法
- 读大道至简第二章感悟
- Java中一个线程只有六个状态。至于阻塞、可运行、挂起状态都是人们为了便于理解,自己加上去的。...
- 解决VMWare Workstation 响应慢
- 轻量级MVC框架(自行开发)
- usionCharts 技术文档-Jsp画图
- 百度地图开发android开发,android的百度地图开发(一)
- JSLIU 的 wxWindows 入门
- Windows的Git Bash使用tree命令
- boost入门(三):Asio简单示例
- Zabbix5.0如何发送短信
- 呸!都TM开始打广告了,垃圾!
- 全球及中国农业微量营养素行业商业模式分析及投资风险预测报告2022-2028年版