活动地址: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

LazyCsrfTokenRepositoryHttpSessionCsrfTokenRepository 外包了一层,作用是:延迟保存新的 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 防御的 校验流程 分析

  • 如果是 POSTPUT 之类的由 DefaultRequiresCsrfMatcher 定义的需要 CSRF 拦截的请求,就必须在 请求头 或者 请求参数 中携带对应 session 中存的值

SpringSecurity - 启动流程分析(八)- CsrfFilter 过滤器相关推荐

  1. Alian解读SpringBoot 2.6.0 源码(八):启动流程分析之刷新应用上下文(下)

    目录 一.背景 1.1.刷新的整体调用流程 1.2.本文解读范围 二.初始化特定上下文子类中的其他特殊bean 2.1.初始化主体资源 2.2.创建web服务 三.检查监听器bean并注册它们 四.实 ...

  2. Alian解读SpringBoot 2.6.0 源码(八):启动流程分析之刷新应用上下文(中)

    目录 一.背景 1.1.刷新的整体调用流程 1.2.本文解读范围 二.调用后处理器 2.1.调用在上下文中注册为beanFactory的后置处理器 2.2.invokeBeanFactoryPostP ...

  3. Alian解读SpringBoot 2.6.0 源码(八):启动流程分析之刷新应用上下文(上)

    目录 一.背景 1.1.run方法整体流程 1.2.刷新的整体调用流程 1.3.本文解读范围 二.准备刷新 2.1.准备刷新的流程 2.2.初始化上下文环境中servlet相关属性源 2.3.校验re ...

  4. SpringBoot(十二)启动流程分析之创建应用上下文AnnotationConfigServletWebServerApplicationContext

    SpringBoot版本:2.1.1      ==>启动流程分析汇总 接上篇博客Spring Boot 2.1.1(十一)启动流程分析之设置系统属性spring.beaninfo.ignore ...

  5. 基于 LPC1114 的 M0 启动流程分析

    基于LPC1114的M0启动流程分析 作者:解琛 时间:2020 年 8 月 1 日 基于LPC1114的M0启动流程分析 一..s 启动进程 1.1 堆栈配置 1.2 中断向量表 1.3 定义中断服 ...

  6. Cocos2d-x3.3RC0的Android编译Activity启动流程分析

    http://www.itnose.net/detail/6142692.html 本文将从引擎源码Jni分析Cocos2d-x3.3RC0的Android Activity的启动流程,下面是详细分析 ...

  7. Alian解读SpringBoot 2.6.0 源码(六):启动流程分析之创建应用上下文

    目录 一.背景 1.1.run方法整体流程 1.2.本文解读范围 二.创建应用上下文 2.1.初始化入口 2.2.初始化AbstractApplicationContext 2.3.初始化Generi ...

  8. 解析并符号 读取dll_Spring IOC容器之XmlBeanFactory启动流程分析和源码解析

    一. 前言 Spring容器主要分为两类BeanFactory和ApplicationContext,后者是基于前者的功能扩展,也就是一个基础容器和一个高级容器的区别.本篇就以BeanFactory基 ...

  9. Zygote进程启动流程分析

    文中的源代码版本为api23 Zygote进程启动流程分析 先说结论,zygote进程启动过程中主要做了下面这些事情: 启动DVM虚拟机 预加载部分资源,如一些通用类.通用资源.共享库等 启动syst ...

最新文章

  1. java类的基本组成和使用,Java架构师成长路线
  2. php 循环链表,PHP实现循环链表功能
  3. 数据科学中的Docker
  4. mariadb 配置mysql_mysql-mariadb实践中用到的配置(不断完善中)
  5. Postman批量接口测试
  6. Tiny之Web工程构建
  7. oracle数据库pfile文件,Oracle pfile/spfile参数文件详解
  8. 面向程序员编程——精研排序算法
  9. 读大道至简第二章感悟
  10. Java中一个线程只有六个状态。至于阻塞、可运行、挂起状态都是人们为了便于理解,自己加上去的。...
  11. 解决VMWare Workstation 响应慢
  12. 轻量级MVC框架(自行开发)
  13. usionCharts 技术文档-Jsp画图
  14. 百度地图开发android开发,android的百度地图开发(一)
  15. JSLIU 的 wxWindows 入门
  16. Windows的Git Bash使用tree命令
  17. boost入门(三):Asio简单示例
  18. Zabbix5.0如何发送短信
  19. 呸!都TM开始打广告了,垃圾!
  20. 全球及中国农业微量营养素行业商业模式分析及投资风险预测报告2022-2028年版

热门文章

  1. 2016华为开发者决赛专访:移动CRM助力中小企业沟通更有效率
  2. 一个绘制虚线的非常规函数(常规方法,打印机上绘制不出虚线)
  3. 1225:金银岛——贪心
  4. CentOS7.7系统安装-自制镜像
  5. Oracle:使用Impdp导入dmp文件的详细过程
  6. FlinkCDC系列01: SQL-DDL方言翻译器:SQL-Translator测试手记
  7. 数据同步神器Canel-day01
  8. EPON OLT网管系统的实现
  9. 有种技艺:有道云笔记不开会员 去除广告大法(屡试不爽)
  10. C#——文件分割与合并