要学就学透彻!Spring Security 中 CSRF 防御源码解析
上篇文章松哥和大家聊了什么是 CSRF 攻击,以及 CSRF 攻击要如何防御。主要和大家聊了 Spring Security 中处理该问题的几种办法。
今天松哥来和大家简单的看一下 Spring Security 中,CSRF 防御源码。
本文是本系列第 19 篇,阅读本系列前面文章有助于更好的理解本文:
- 挖一个大坑,Spring Security 开搞!
- 松哥手把手带你入门 Spring Security,别再问密码怎么解密了
- 手把手教你定制 Spring Security 中的表单登录
- Spring Security 做前后端分离,咱就别做页面跳转了!统统 JSON 交互
- Spring Security 中的授权操作原来这么简单
- Spring Security 如何将用户数据存入数据库?
- Spring Security+Spring Data Jpa 强强联手,安全管理只有更简单!
- Spring Boot + Spring Security 实现自动登录功能
- Spring Boot 自动登录,安全风险要怎么控制?
- 在微服务项目中,Spring Security 比 Shiro 强在哪?
- SpringSecurity 自定义认证逻辑的两种方式(高级玩法)
- Spring Security 中如何快速查看登录用户 IP 地址等信息?
- Spring Security 自动踢掉前一个登录用户,一个配置搞定!
- Spring Boot + Vue 前后端分离项目,如何踢掉已登录用户?
- Spring Security 自带防火墙!你都不知道自己的系统有多安全!
- 什么是会话固定攻击?Spring Boot 中要如何防御会话固定攻击?
- 集群化部署,Spring Security 要如何处理 session 共享?
- 松哥手把手教你在 SpringBoot 中防御 CSRF 攻击!so easy!
本文主要从两个方面来和大家讲解:
- 返回给前端的
_csrf
参数是如何生成的。 - 前端传来的
_csrf
参数是如何校验的。
1.随机字符串生成
我们先来看一下 Spring Security 中的 csrf 参数是如何生成的。
首先,Spring Security 中提供了一个保存 csrf 参数的规范,就是 CsrfToken:
public interface CsrfToken extends Serializable {String getHeaderName();String getParameterName();String getToken();}
这里三个方法都好理解,前两个是获取 _csrf
参数的 key,第三个是获取 _csrf
参数的 value。
CsrfToken 有两个实现类,如下:
默认情况下使用的是 DefaultCsrfToken,我们来稍微看下 DefaultCsrfToken:
public final class DefaultCsrfToken implements CsrfToken {private final String token;private final String parameterName;private final String headerName;public DefaultCsrfToken(String headerName, String parameterName, String token) {this.headerName = headerName;this.parameterName = parameterName;this.token = token;}public String getHeaderName() {return this.headerName;}public String getParameterName() {return this.parameterName;}public String getToken() {return this.token;}
}
这段实现很简单,几乎没有添加额外的方法,就是接口方法的实现。
CsrfToken 相当于就是 _csrf
参数的载体。那么参数是如何生成和保存的呢?这涉及到另外一个类:
public interface CsrfTokenRepository {CsrfToken generateToken(HttpServletRequest request);void saveToken(CsrfToken token, HttpServletRequest request,HttpServletResponse response);CsrfToken loadToken(HttpServletRequest request);
}
这里三个方法:
- generateToken 方法就是 CsrfToken 的生成过程。
- saveToken 方法就是保存 CsrfToken。
- loadToken 则是如何加载 CsrfToken。
CsrfTokenRepository 有四个实现类,在上篇文章中,我们用到了其中两个:HttpSessionCsrfTokenRepository 和 CookieCsrfTokenRepository,其中 HttpSessionCsrfTokenRepository 是默认的方案。
我们先来看下 HttpSessionCsrfTokenRepository 的实现:
public final class HttpSessionCsrfTokenRepository implements CsrfTokenRepository {private static final String DEFAULT_CSRF_PARAMETER_NAME = "_csrf";private static final String DEFAULT_CSRF_HEADER_NAME = "X-CSRF-TOKEN";private static final String DEFAULT_CSRF_TOKEN_ATTR_NAME = HttpSessionCsrfTokenRepository.class.getName().concat(".CSRF_TOKEN");private String parameterName = DEFAULT_CSRF_PARAMETER_NAME;private String headerName = DEFAULT_CSRF_HEADER_NAME;private String sessionAttributeName = DEFAULT_CSRF_TOKEN_ATTR_NAME;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.setAttribute(this.sessionAttributeName, token);}}public CsrfToken loadToken(HttpServletRequest request) {HttpSession session = request.getSession(false);if (session == null) {return null;}return (CsrfToken) session.getAttribute(this.sessionAttributeName);}public CsrfToken generateToken(HttpServletRequest request) {return new DefaultCsrfToken(this.headerName, this.parameterName,createNewToken());}private String createNewToken() {return UUID.randomUUID().toString();}
}
这段源码其实也很好理解:
- saveToken 方法将 CsrfToken 保存在 HttpSession 中,将来再从 HttpSession 中取出和前端传来的参数做笔记。
- loadToken 方法当然就是从 HttpSession 中读取 CsrfToken 出来。
- generateToken 是生成 CsrfToken 的过程,可以看到,生成的默认载体就是 DefaultCsrfToken,而 CsrfToken 的值则通过 createNewToken 方法生成,是一个 UUID 字符串。
- 在构造 DefaultCsrfToken 是还有两个参数 headerName 和 parameterName,这两个参数是前端保存参数的 key。
这是默认的方案,适用于前后端不分的开发,具体用法可以参考上篇文章。
如果想在前后端分离开发中使用,那就需要 CsrfTokenRepository 的另一个实现类 CookieCsrfTokenRepository ,代码如下:
public final class CookieCsrfTokenRepository implements CsrfTokenRepository {static final String DEFAULT_CSRF_COOKIE_NAME = "XSRF-TOKEN";static final String DEFAULT_CSRF_PARAMETER_NAME = "_csrf";static final String DEFAULT_CSRF_HEADER_NAME = "X-XSRF-TOKEN";private String parameterName = DEFAULT_CSRF_PARAMETER_NAME;private String headerName = DEFAULT_CSRF_HEADER_NAME;private String cookieName = DEFAULT_CSRF_COOKIE_NAME;private boolean cookieHttpOnly = true;private String cookiePath;private String cookieDomain;public CookieCsrfTokenRepository() {}@Overridepublic CsrfToken generateToken(HttpServletRequest request) {return new DefaultCsrfToken(this.headerName, this.parameterName,createNewToken());}@Overridepublic void saveToken(CsrfToken token, HttpServletRequest request,HttpServletResponse response) {String tokenValue = token == null ? "" : token.getToken();Cookie cookie = new Cookie(this.cookieName, tokenValue);cookie.setSecure(request.isSecure());if (this.cookiePath != null && !this.cookiePath.isEmpty()) {cookie.setPath(this.cookiePath);} else {cookie.setPath(this.getRequestContext(request));}if (token == null) {cookie.setMaxAge(0);}else {cookie.setMaxAge(-1);}cookie.setHttpOnly(cookieHttpOnly);if (this.cookieDomain != null && !this.cookieDomain.isEmpty()) {cookie.setDomain(this.cookieDomain);}response.addCookie(cookie);}@Overridepublic CsrfToken loadToken(HttpServletRequest request) {Cookie cookie = WebUtils.getCookie(request, this.cookieName);if (cookie == null) {return null;}String token = cookie.getValue();if (!StringUtils.hasLength(token)) {return null;}return new DefaultCsrfToken(this.headerName, this.parameterName, token);}public static CookieCsrfTokenRepository withHttpOnlyFalse() {CookieCsrfTokenRepository result = new CookieCsrfTokenRepository();result.setCookieHttpOnly(false);return result;}private String createNewToken() {return UUID.randomUUID().toString();}
}
和 HttpSessionCsrfTokenRepository 相比,这里 _csrf
数据保存的时候,都保存到 cookie 中去了,当然读取的时候,也是从 cookie 中读取,其他地方则和 HttpSessionCsrfTokenRepository 是一样的。
OK,这就是我们整个 _csrf
参数生成的过程。
总结一下,就是生成一个 CsrfToken,这个 Token,本质上就是一个 UUID 字符串,然后将这个 Token 保存到 HttpSession 中,或者保存到 Cookie 中,待请求到来时,从 HttpSession 或者 Cookie 中取出来做校验。
2.参数校验
那接下来就是校验了。
校验主要是通过 CsrfFilter 过滤器来进行,我们来看下核心的 doFilterInternal 方法:
protected void doFilterInternal(HttpServletRequest request,HttpServletResponse response, FilterChain filterChain)throws ServletException, IOException {request.setAttribute(HttpServletResponse.class.getName(), response);CsrfToken csrfToken = this.tokenRepository.loadToken(request);final boolean missingToken = csrfToken == null;if (missingToken) {csrfToken = this.tokenRepository.generateToken(request);this.tokenRepository.saveToken(csrfToken, request, response);}request.setAttribute(CsrfToken.class.getName(), csrfToken);request.setAttribute(csrfToken.getParameterName(), csrfToken);if (!this.requireCsrfProtectionMatcher.matches(request)) {filterChain.doFilter(request, response);return;}String actualToken = request.getHeader(csrfToken.getHeaderName());if (actualToken == null) {actualToken = request.getParameter(csrfToken.getParameterName());}if (!csrfToken.getToken().equals(actualToken)) {if (this.logger.isDebugEnabled()) {this.logger.debug("Invalid CSRF token found for "+ UrlUtils.buildFullRequestUrl(request));}if (missingToken) {this.accessDeniedHandler.handle(request, response,new MissingCsrfTokenException(actualToken));}else {this.accessDeniedHandler.handle(request, response,new InvalidCsrfTokenException(csrfToken, actualToken));}return;}filterChain.doFilter(request, response);
}
这个方法我来稍微解释下:
- 首先调用 tokenRepository.loadToken 方法读取 CsrfToken 出来,这个 tokenRepository 就是你配置的 CsrfTokenRepository 实例,CsrfToken 存在 HttpSession 中,这里就从 HttpSession 中读取,CsrfToken 存在 Cookie 中,这里就从 Cookie 中读取。
- 如果调用 tokenRepository.loadToken 方法没有加载到 CsrfToken,那说明这个请求可能是第一次发起,则调用 tokenRepository.generateToken 方法生成 CsrfToken ,并调用 tokenRepository.saveToken 方法保存 CsrfToken。
- 大家注意,这里还调用 request.setAttribute 方法存了一些值进去,这就是默认情况下,我们通过 jsp 或者 thymeleaf 标签渲染
_csrf
的数据来源。 - requireCsrfProtectionMatcher.matches 方法则使用用来判断哪些请求方法需要做校验,默认情况下,“GET”, “HEAD”, “TRACE”, “OPTIONS” 方法是不需要校验的。
- 接下来获取请求中传递来的 CSRF 参数,先从请求头中获取,获取不到再从请求参数中获取。
- 获取到请求传来的 csrf 参数之后,再和一开始加载到的 csrfToken 做比较,如果不同的话,就抛出异常。
如此之后,就完成了整个校验工作了。
3.LazyCsrfTokenRepository
前面我们说了 CsrfTokenRepository 有四个实现类,除了我们介绍的两个之外,还有一个 LazyCsrfTokenRepository,这里松哥也和大家做一个简单介绍。
在前面的 CsrfFilter 中大家发现,对于常见的 GET 请求实际上是不需要 CSRF 攻击校验的,但是,每当 GET 请求到来时,下面这段代码都会执行:
if (missingToken) {csrfToken = this.tokenRepository.generateToken(request);this.tokenRepository.saveToken(csrfToken, request, response);
}
生成 CsrfToken 并保存,但实际上却没什么用,因为 GET 请求不需要 CSRF 攻击校验。
所以,Spring Security 官方又推出了 LazyCsrfTokenRepository。
LazyCsrfTokenRepository 实际上不能算是一个真正的 CsrfTokenRepository,它是一个代理,可以用来增强 HttpSessionCsrfTokenRepository 或者 CookieCsrfTokenRepository 的功能:
public final class LazyCsrfTokenRepository implements CsrfTokenRepository {@Overridepublic CsrfToken generateToken(HttpServletRequest request) {return wrap(request, this.delegate.generateToken(request));}@Overridepublic void saveToken(CsrfToken token, HttpServletRequest request,HttpServletResponse response) {if (token == null) {this.delegate.saveToken(token, request, response);}}@Overridepublic CsrfToken loadToken(HttpServletRequest request) {return this.delegate.loadToken(request);}private CsrfToken wrap(HttpServletRequest request, CsrfToken token) {HttpServletResponse response = getResponse(request);return new SaveOnAccessCsrfToken(this.delegate, request, response, token);}private static final class SaveOnAccessCsrfToken implements CsrfToken {private transient CsrfTokenRepository tokenRepository;private transient HttpServletRequest request;private transient HttpServletResponse response;private final CsrfToken delegate;SaveOnAccessCsrfToken(CsrfTokenRepository tokenRepository,HttpServletRequest request, HttpServletResponse response,CsrfToken delegate) {this.tokenRepository = tokenRepository;this.request = request;this.response = response;this.delegate = delegate;}@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;}}}}
}
这里,我说三点:
- generateToken 方法,该方法用来生成 CsrfToken,默认 CsrfToken 的载体是 DefaultCsrfToken,现在换成了 SaveOnAccessCsrfToken。
- SaveOnAccessCsrfToken 和 DefaultCsrfToken 并没有太大区别,主要是 getToken 方法有区别,在 SaveOnAccessCsrfToken 中,当开发者调用 getToken 想要去获取 csrfToken 时,才会去对 csrfToken 做保存操作(调用 HttpSessionCsrfTokenRepository 或者 CookieCsrfTokenRepository 的 saveToken 方法)。
- LazyCsrfTokenRepository 自己的 saveToken 则做了修改,相当于放弃了 saveToken 的功能,调用该方法并不会做保存操作。
使用了 LazyCsrfTokenRepository 之后,只有在使用 csrfToken 时才会去存储它,这样就可以节省存储空间了。
LazyCsrfTokenRepository 的配置方式也很简单,在我们使用 Spring Security 时,如果对 csrf 不做任何配置,默认其实就是 LazyCsrfTokenRepository+HttpSessionCsrfTokenRepository 组合。
当然我们也可以自己配置,如下:
@Override
protected void configure(HttpSecurity http) throws Exception {http.authorizeRequests().anyRequest().authenticated().and().formLogin().loginPage("/login.html").successHandler((req,resp,authentication)->{resp.getWriter().write("success");}).permitAll().and().csrf().csrfTokenRepository(new LazyCsrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()));
}
4.小结
今天主要和小伙伴聊了一下 Spring Security 中 csrf 防御的原理。
整体来说,就是两个思路:
- 生成 csrfToken 保存在 HttpSession 或者 Cookie 中。
- 请求到来时,从请求中提取出来 csrfToken,和保存的 csrfToken 做比较,进而判断出当前请求是否合法。
好啦,不知道小伙伴们有没有 GET 到呢?如果觉得有收获,记得点个在看鼓励下松哥哦~
要学就学透彻!Spring Security 中 CSRF 防御源码解析相关推荐
- springsecurity sessionregistry session共享_要学就学透彻!Spring Security 中 CSRF 防御源码解析...
今日干货 刚刚发表查看:66666回复:666 公众号后台回复 ssm,免费获取松哥纯手敲的 SSM 框架学习干货. 上篇文章松哥和大家聊了什么是 CSRF 攻击,以及 CSRF 攻击要如何防御.主要 ...
- Spring Security CSRF防御源码分析
一.CSRF简介 1.CSRF是什么? CSRF(Cross-site request forgery),也被称为:one click attack/session riding,中文名称:跨站请求伪 ...
- Spring AOP 增强器获取的源码解析
Spring AOP 增强器获取的源码解析 转载于:https://juejin.im/post/5c2b230ae51d45778a5cad3e
- elementui组件_elementui 中 loading 组件源码解析(续)
上一篇我们说了elementui如何将loading组件添加到 Vue 实例上,具体内容见上期 elementui 中 loading 组件源码解析. 这一篇我们开始讲讲自定义指令 自定义指令 关于自 ...
- Spring Boot 核心原理与源码解析 - 目录
准备重新写 SpringBoot 配置文件解析原理 , 先在这里把要写的内容记下来 Spring Boot 核心原理与源码解析 - 目录 1\何时解析\如何解析 application.propert ...
- elementui table某一列是否显示_elementui 中 loading 组件源码解析(续)
上一篇我们说了elementui如何将loading组件添加到 Vue 实例上,具体内容见上期 elementui 中 loading 组件源码解析. 这一篇我们开始讲讲自定义指令 自定义指令 关于自 ...
- Unity中的UGUI源码解析之事件系统(2)-EventSystem组件
Unity中的UGUI源码解析之事件系统(2)-EventSystem组件 今天介绍我们的第一个主角: EventSystem. EventSystem在整个事件系统中处于中心, 相当于事件系统的管理 ...
- Unity中的UGUI源码解析之事件系统(8)-输入模块(中)
Unity中的UGUI源码解析之事件系统(8)-输入模块(中) 接上一篇文章, 继续介绍输入模块. Unity中主要处理的是指针事件, 也就是在2d平面上跟踪指针设备输入坐标的的事件, 这一类事件有鼠 ...
- Unity中的UGUI源码解析之事件系统(9)-输入模块(下)
Unity中的UGUI源码解析之事件系统(9)-输入模块(下) 接上一篇文章, 继续介绍输入模块. StandaloneInputModule类是上一篇文章介绍的抽象类PointerInputModu ...
最新文章
- php接口图片转二进制,怎么在php项目中对图片进行二进制转换
- c语言goto语句用法_C语言中的goto语句该不该使用?
- [architecture]-ARMV8的一些总结-一篇就够了
- VTK修炼之道78:交互与拾取_点拾取
- Java二维数组排序(按照某一列值大小)
- ubuntu下面解决mysqld_safe Directory '/var/run/mysqld' for UNIX socket file don't exists
- SpringCloud之RestTemplate,几种常见的请求方式
- html5 注册协议弹出层,js制作带有遮罩弹出层实现登录注册表单特效代码分享
- pytorch Embedding模块,自动为文本加载预训练的embedding
- 为什么我推荐你立刻使用Java 8 Stream?性能逆天了
- Java Web从前端到后台常用框架介绍
- python list相关知识
- 全球与中国处方太阳镜市场深度研究分析报告
- liunx 安装docker
- 华为HCNA之OSPF多区域配置实验
- Saber 能量激光描边光效插件
- 挖掘视频网站【优酷】上被截断的视频的地址--001
- Linux内核编译、安装
- 【苹果CMS技术教程】苹果CMSV10基础安装过程,如何拥有自己的视频网站
- STM32 ES8266上阿里云IOT MQTT实践【第一章】:物联网简介(什么是物联网)
热门文章
- 奥西tds300服务器维修,奥西Océ TDS300 驱动
- Python 真的好学吗?
- sql server 创建动态交叉表
- 什么叫反光识别读数识别_一网通办全攻略来啦!政务服务事项网上办 让您一次都不用跑!...
- 9*9数独——C++实现
- 我来回答人生百问^_^老姚
- Android图片加载框架 Glide 4 的用法
- 【笔记】Polygon mesh processing 读书笔记(3)
- air android 后台运行,AIR Android:使用模拟器运行程序
- 身份证号第18位(效验码)计算方法 用于检测身份证号是否正确