今日干货

刚刚发表查看:66666回复:666

公众号后台回复 ssm,免费获取松哥纯手敲的 SSM 框架学习干货。

上篇文章松哥和大家聊了什么是 CSRF 攻击,以及 CSRF 攻击要如何防御。主要和大家聊了 Spring Security 中处理该问题的几种办法。

今天松哥来和大家简单的看一下 Spring Security 中,CSRF 防御源码。

本文是本系列第 19 篇,阅读本系列前面文章有助于更好的理解本文:

  1. 挖一个大坑,Spring Security 开搞!
  2. 松哥手把手带你入门 Spring Security,别再问密码怎么解密了
  3. 手把手教你定制 Spring Security 中的表单登录
  4. Spring Security 做前后端分离,咱就别做页面跳转了!统统 JSON 交互
  5. Spring Security 中的授权操作原来这么简单
  6. Spring Security 如何将用户数据存入数据库?
  7. Spring Security+Spring Data Jpa 强强联手,安全管理只有更简单!
  8. Spring Boot + Spring Security 实现自动登录功能
  9. Spring Boot 自动登录,安全风险要怎么控制?
  10. 在微服务项目中,Spring Security 比 Shiro 强在哪?
  11. SpringSecurity 自定义认证逻辑的两种方式(高级玩法)
  12. Spring Security 中如何快速查看登录用户 IP 地址等信息?
  13. Spring Security 自动踢掉前一个登录用户,一个配置搞定!
  14. Spring Boot + Vue 前后端分离项目,如何踢掉已登录用户?
  15. Spring Security 自带防火墙!你都不知道自己的系统有多安全!
  16. 什么是会话固定攻击?Spring Boot 中要如何防御会话固定攻击?
  17. 集群化部署,Spring Security 要如何处理 session 共享?
  18. 松哥手把手教你在 SpringBoot 中防御 CSRF 攻击!so easy!

本文主要从两个方面来和大家讲解:

  1. 返回给前端的 _csrf 参数是如何生成的。
  2. 前端传来的 _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);}

这里三个方法:

  1. generateToken 方法就是 CsrfToken 的生成过程。
  2. saveToken 方法就是保存 CsrfToken。
  3. 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(); }}

这段源码其实也很好理解:

  1. saveToken 方法将 CsrfToken 保存在 HttpSession 中,将来再从 HttpSession 中取出和前端传来的参数做笔记。
  2. loadToken 方法当然就是从 HttpSession 中读取 CsrfToken 出来。
  3. generateToken 是生成 CsrfToken 的过程,可以看到,生成的默认载体就是 DefaultCsrfToken,而 CsrfToken 的值则通过 createNewToken 方法生成,是一个 UUID 字符串。
  4. 在构造 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() { } @Override public CsrfToken generateToken(HttpServletRequest request) {  return new DefaultCsrfToken(this.headerName, this.parameterName,    createNewToken()); } @Override public 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); } @Override public 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);}

这个方法我来稍微解释下:

  1. 首先调用 tokenRepository.loadToken 方法读取 CsrfToken 出来,这个 tokenRepository 就是你配置的 CsrfTokenRepository 实例,CsrfToken 存在 HttpSession 中,这里就从 HttpSession 中读取,CsrfToken 存在 Cookie 中,这里就从 Cookie 中读取。
  2. 如果调用 tokenRepository.loadToken 方法没有加载到 CsrfToken,那说明这个请求可能是第一次发起,则调用 tokenRepository.generateToken 方法生成 CsrfToken ,并调用 tokenRepository.saveToken 方法保存 CsrfToken。
  3. 大家注意,这里还调用 request.setAttribute 方法存了一些值进去,这就是默认情况下,我们通过 jsp 或者 thymeleaf 标签渲染 _csrf 的数据来源。
  4. requireCsrfProtectionMatcher.matches 方法则使用用来判断哪些请求方法需要做校验,默认情况下,"GET", "HEAD", "TRACE", "OPTIONS" 方法是不需要校验的。
  5. 接下来获取请求中传递来的 CSRF 参数,先从请求头中获取,获取不到再从请求参数中获取。
  6. 获取到请求传来的 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 { @Override public CsrfToken generateToken(HttpServletRequest request) {  return wrap(request, this.delegate.generateToken(request)); } @Override public void saveToken(CsrfToken token, HttpServletRequest request,   HttpServletResponse response) {  if (token == null) {   this.delegate.saveToken(token, request, response);  } } @Override public 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;  }  @Override  public 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. generateToken 方法,该方法用来生成 CsrfToken,默认 CsrfToken 的载体是 DefaultCsrfToken,现在换成了 SaveOnAccessCsrfToken。
  2. SaveOnAccessCsrfToken 和 DefaultCsrfToken 并没有太大区别,主要是 getToken 方法有区别,在 SaveOnAccessCsrfToken 中,当开发者调用 getToken 想要去获取 csrfToken 时,才会去对 csrfToken 做保存操作(调用 HttpSessionCsrfTokenRepository 或者 CookieCsrfTokenRepository 的 saveToken 方法)。
  3. LazyCsrfTokenRepository 自己的 saveToken 则做了修改,相当于放弃了 saveToken 的功能,调用该方法并不会做保存操作。

使用了 LazyCsrfTokenRepository 之后,只有在使用 csrfToken 时才会去存储它,这样就可以节省存储空间了。

LazyCsrfTokenRepository 的配置方式也很简单,在我们使用 Spring Security 时,如果对 csrf 不做任何配置,默认其实就是 LazyCsrfTokenRepository+HttpSessionCsrfTokenRepository 组合。

当然我们也可以自己配置,如下:

@Overrideprotected 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 防御的原理。

整体来说,就是两个思路:

  1. 生成 csrfToken 保存在 HttpSession 或者 Cookie 中。
  2. 请求到来时,从请求中提取出来 csrfToken,和保存的 csrfToken 做比较,进而判断出当前请求是否合法。

好啦,不知道小伙伴们有没有 GET 到呢?如果觉得有收获,记得点个在看鼓励下松哥哦~

今日干货

刚刚发表查看:13500回复:135

公众号后台回复 SpringBoot,免费获取 274 页SpringBoot修炼手册。

springsecurity sessionregistry session共享_要学就学透彻!Spring Security 中 CSRF 防御源码解析...相关推荐

  1. 要学就学透彻!Spring Security 中 CSRF 防御源码解析

    上篇文章松哥和大家聊了什么是 CSRF 攻击,以及 CSRF 攻击要如何防御.主要和大家聊了 Spring Security 中处理该问题的几种办法. 今天松哥来和大家简单的看一下 Spring Se ...

  2. springsecurity sessionregistry session共享_不用 Spring Security 可否?试试这个小而美的安全框架...

    写在前面 在一款应用的整个生命周期,我们都会谈及该应用的数据安全问题.用户的合法性与数据的可见性是数据安全中非常重要的一部分.但是,一方面,不同的应用对于数据的合法性和可见性要求的维度与粒度都有所区别 ...

  3. java计算机毕业设计黑格伯爵国际英语贵族学校官网MyBatis+系统+LW文档+源码+调试部署

    java计算机毕业设计黑格伯爵国际英语贵族学校官网MyBatis+系统+LW文档+源码+调试部署 java计算机毕业设计黑格伯爵国际英语贵族学校官网MyBatis+系统+LW文档+源码+调试部署 本源 ...

  4. 基于JAVA线上远程教学及自主学平台的设计与实现计算机毕业设计源码+系统+mysql数据库+lw文档+部署

    基于JAVA线上远程教学及自主学平台的设计与实现计算机毕业设计源码+系统+mysql数据库+lw文档+部署 基于JAVA线上远程教学及自主学平台的设计与实现计算机毕业设计源码+系统+mysql数据库+ ...

  5. python flask源码解析_用尽洪荒之力学习Flask源码

    [TOC] 一直想做源码阅读这件事,总感觉难度太高时间太少,可望不可见.最近正好时间充裕,决定试试做一下,并记录一下学习心得. 首先说明一下,本文研究的Flask版本是0.12. 首先做个小示例,在p ...

  6. Laravel核心解读--Session源码解析

    Session 模块源码解析 由于HTTP最初是一个匿名.无状态的请求/响应协议,服务器处理来自客户端的请求然后向客户端回送一条响应.现代Web应用程序为了给用户提供个性化的服务往往需要在请求中识别出 ...

  7. openxr runtime Monado 源码解析 源码分析:CreateSwapchain 画布 HardwareBuffer共享纹理 渲染线程 xrEndeFrame comp_renderer

    monado系列文章索引汇总: openxr runtime Monado 源码解析 源码分析:源码编译 准备工作说明 hello_xr解读 openxr runtime Monado 源码解析 源码 ...

  8. java计算机毕业设计共享单车使用满意度评价系统MyBatis+系统+LW文档+源码+调试部署

    java计算机毕业设计共享单车使用满意度评价系统MyBatis+系统+LW文档+源码+调试部署 java计算机毕业设计共享单车使用满意度评价系统MyBatis+系统+LW文档+源码+调试部署 本源码技 ...

  9. Spring5源码 - 13 Spring事件监听机制_@EventListener源码解析

    文章目录 Pre 概览 开天辟地的时候初始化的处理器 @EventListener EventListenerMethodProcessor afterSingletonsInstantiated 小 ...

最新文章

  1. java安全技术-Base64编码与解码
  2. java keytool 代码_JDK keytool证书工具功能代码解析_java_脚本之家
  3. linux之Vim使用
  4. NOIP练习赛题目5
  5. k8s 手动恢复redis 集群_高工面试之:redis的几种集群方式你都熟悉吗?
  6. Qt工作笔记-undefined reference to `vtable for MyObject'及对moc文件的进一步理解
  7. 【6】使用dockerfile打包/运行微服务项目,并上传到私服harbor
  8. 学习HttpClient,从两个小例子开始
  9. Jmeter 获取、读取token 供其他 HTTP 请求调用
  10. 2020-5-9 开始阅读深入理解java虚拟机
  11. 触动精灵mysql用法_[触动精灵]触动精灵官方手册流水账2
  12. Python必知必会:Classethod与Staticmethod方法
  13. 部署到gcp_Linux基础架构学习 - 使用GCP托管云解决方案 - Day09
  14. android逆向去广告,教程]安卓逆向去广告教程送给大家(
  15. 随机数字表法计算机分配,随机数字表法
  16. Ubuntu18中添加中文输入法
  17. 听云SDK影响其他项目报Caused by: java.lang.NoClassDefFoundError: Failed resolution of: Lcom/networkbench/agent
  18. 毕业设计 : 题目:基于深度学习的水果识别 设计 开题 技术
  19. 金融行业软件测试面试题(含答案)
  20. 信息安全密码学实验二:序列密码的设计与实现

热门文章

  1. jQuery的push
  2. c语言的查询功能,求C语言实现查询功能(如果选择3,如何实现查询)
  3. php 过滤特殊字符和sql防注入代码以及xss攻击等
  4. win上mysql忘记root密码_MySQL数据库之windows下mysql忘记root密码的解决方法
  5. jsp访问java方法_JSP基础知识之访问JavaBean的方法
  6. php excel中解析显示html代码_骑士cms从任意文件包含到远程代码执行漏洞分析
  7. PHP常用 header函数设置HTTP头部示例
  8. SpringBoot整合Shiro(认证+授权)
  9. mysql通配符查询 性能_使用mysql5.7新特性解决前通配符查询性能问题
  10. docker ip地址_理解 Docker 网络(番外) -- 《Docker 源码分析》勘误