Spring Security 中 CSRF 防御源码解析

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

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

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

  • 返回给前端的 _csrf 参数是如何生成的。
  • 前端传来的 _csrf 参数是如何校验的。

1.随机字符串生成

我们先来看一下 Spring Security 中的 csrf 参数是如何生成的。

首先,Spring Security 中提供了一个保存 csrf 参数的规范,就是 CsrfToken:

  1. public interface CsrfToken extends Serializable { 
  2.  String getHeaderName(); 
  3.  String getParameterName(); 
  4.  String getToken(); 
  5.  

这里三个方法都好理解,前两个是获取 _csrf 参数的 key,第三个是获取 _csrf 参数的 value。

CsrfToken 有两个实现类,如下:

默认情况下使用的是 DefaultCsrfToken,我们来稍微看下 DefaultCsrfToken:

  1. public final class DefaultCsrfToken implements CsrfToken { 
  2.  private final String token; 
  3.  private final String parameterName; 
  4.  private final String headerName; 
  5.  public DefaultCsrfToken(String headerName, String parameterName, String token) { 
  6.   this.headerName = headerName; 
  7.   this.parameterName = parameterName; 
  8.   this.token = token; 
  9.  } 
  10.  public String getHeaderName() { 
  11.   return this.headerName; 
  12.  } 
  13.  public String getParameterName() { 
  14.   return this.parameterName; 
  15.  } 
  16.  public String getToken() { 
  17.   return this.token; 
  18.  } 

这段实现很简单,几乎没有添加额外的方法,就是接口方法的实现。

CsrfToken 相当于就是 _csrf 参数的载体。那么参数是如何生成和保存的呢?这涉及到另外一个类:

  1. public interface CsrfTokenRepository { 
  2.  CsrfToken generateToken(HttpServletRequest request); 
  3.  void saveToken(CsrfToken token, HttpServletRequest request, 
  4.    HttpServletResponse response); 
  5.  CsrfToken loadToken(HttpServletRequest request); 

这里三个方法:

  1. generateToken 方法就是 CsrfToken 的生成过程。
  2. saveToken 方法就是保存 CsrfToken。
  3. loadToken 则是如何加载 CsrfToken。

CsrfTokenRepository 有四个实现类,在上篇文章中,我们用到了其中两个:HttpSessionCsrfTokenRepository 和 CookieCsrfTokenRepository,其中 HttpSessionCsrfTokenRepository 是默认的方案。

我们先来看下 HttpSessionCsrfTokenRepository 的实现:

  1. public final class HttpSessionCsrfTokenRepository implements CsrfTokenRepository { 
  2.  private static final String DEFAULT_CSRF_PARAMETER_NAME = "_csrf"
  3.  private static final String DEFAULT_CSRF_HEADER_NAME = "X-CSRF-TOKEN"
  4.  private static final String DEFAULT_CSRF_TOKEN_ATTR_NAME = HttpSessionCsrfTokenRepository.class 
  5.    .getName().concat(".CSRF_TOKEN"); 
  6.  private String parameterName = DEFAULT_CSRF_PARAMETER_NAME; 
  7.  private String headerName = DEFAULT_CSRF_HEADER_NAME; 
  8.  private String sessionAttributeName = DEFAULT_CSRF_TOKEN_ATTR_NAME; 
  9.  public void saveToken(CsrfToken token, HttpServletRequest request, 
  10.    HttpServletResponse response) { 
  11.   if (token == null) { 
  12.    HttpSession session = request.getSession(false); 
  13.    if (session != null) { 
  14.     session.removeAttribute(this.sessionAttributeName); 
  15.    } 
  16.   } 
  17.   else { 
  18.    HttpSession session = request.getSession(); 
  19.    session.setAttribute(this.sessionAttributeName, token); 
  20.   } 
  21.  } 
  22.  public CsrfToken loadToken(HttpServletRequest request) { 
  23.   HttpSession session = request.getSession(false); 
  24.   if (session == null) { 
  25.    return null
  26.   } 
  27.   return (CsrfToken) session.getAttribute(this.sessionAttributeName); 
  28.  } 
  29.  public CsrfToken generateToken(HttpServletRequest request) { 
  30.   return new DefaultCsrfToken(this.headerName, this.parameterName, 
  31.     createNewToken()); 
  32.  } 
  33.  private String createNewToken() { 
  34.   return UUID.randomUUID().toString(); 
  35.  } 

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

  1. saveToken 方法将 CsrfToken 保存在 HttpSession 中,将来再从 HttpSession 中取出和前端传来的参数做比较。
  2. loadToken 方法当然就是从 HttpSession 中读取 CsrfToken 出来。
  3. generateToken 是生成 CsrfToken 的过程,可以看到,生成的默认载体就是 DefaultCsrfToken,而 CsrfToken 的值则通过 createNewToken 方法生成,是一个 UUID 字符串。
  4. 在构造 DefaultCsrfToken 是还有两个参数 headerName 和 parameterName,这两个参数是前端保存参数的 key。

这是默认的方案,适用于前后端不分的开发,具体用法可以参考上篇文章

如果想在前后端分离开发中使用,那就需要 CsrfTokenRepository 的另一个实现类 CookieCsrfTokenRepository ,代码如下:

  1. public final class CookieCsrfTokenRepository implements CsrfTokenRepository { 
  2.  static final String DEFAULT_CSRF_COOKIE_NAME = "XSRF-TOKEN"
  3.  static final String DEFAULT_CSRF_PARAMETER_NAME = "_csrf"
  4.  static final String DEFAULT_CSRF_HEADER_NAME = "X-XSRF-TOKEN"
  5.  private String parameterName = DEFAULT_CSRF_PARAMETER_NAME; 
  6.  private String headerName = DEFAULT_CSRF_HEADER_NAME; 
  7.  private String cookieName = DEFAULT_CSRF_COOKIE_NAME; 
  8.  private boolean cookieHttpOnly = true
  9.  private String cookiePath; 
  10.  private String cookieDomain; 
  11.  public CookieCsrfTokenRepository() { 
  12.  } 
  13.  @Override 
  14.  public CsrfToken generateToken(HttpServletRequest request) { 
  15.   return new DefaultCsrfToken(this.headerName, this.parameterName, 
  16.     createNewToken()); 
  17.  } 
  18.  @Override 
  19.  public void saveToken(CsrfToken token, HttpServletRequest request, 
  20.    HttpServletResponse response) { 
  21.   String tokenValue = token == null ? "" : token.getToken(); 
  22.   Cookie cookie = new Cookie(this.cookieName, tokenValue); 
  23.   cookie.setSecure(request.isSecure()); 
  24.   if (this.cookiePath != null && !this.cookiePath.isEmpty()) { 
  25.     cookie.setPath(this.cookiePath); 
  26.   } else { 
  27.     cookie.setPath(this.getRequestContext(request)); 
  28.   } 
  29.   if (token == null) { 
  30.    cookie.setMaxAge(0); 
  31.   } 
  32.   else { 
  33.    cookie.setMaxAge(-1); 
  34.   } 
  35.   cookie.setHttpOnly(cookieHttpOnly); 
  36.   if (this.cookieDomain != null && !this.cookieDomain.isEmpty()) { 
  37.    cookie.setDomain(this.cookieDomain); 
  38.   } 
  39.  
  40.   response.addCookie(cookie); 
  41.  } 
  42.  @Override 
  43.  public CsrfToken loadToken(HttpServletRequest request) { 
  44.   Cookie cookie = WebUtils.getCookie(request, this.cookieName); 
  45.   if (cookie == null) { 
  46.    return null
  47.   } 
  48.   String token = cookie.getValue(); 
  49.   if (!StringUtils.hasLength(token)) { 
  50.    return null
  51.   } 
  52.   return new DefaultCsrfToken(this.headerName, this.parameterName, token); 
  53.  } 
  54.  public static CookieCsrfTokenRepository withHttpOnlyFalse() { 
  55.   CookieCsrfTokenRepository result = new CookieCsrfTokenRepository(); 
  56.   result.setCookieHttpOnly(false); 
  57.   return result; 
  58.  } 
  59.  private String createNewToken() { 
  60.   return UUID.randomUUID().toString(); 
  61.  } 

和 HttpSessionCsrfTokenRepository 相比,这里 _csrf 数据保存的时候,都保存到 cookie 中去了,当然读取的时候,也是从 cookie 中读取,其他地方则和 HttpSessionCsrfTokenRepository 是一样的。

OK,这就是我们整个 _csrf 参数生成的过程。

总结一下,就是生成一个 CsrfToken,这个 Token,本质上就是一个 UUID 字符串,然后将这个 Token 保存到 HttpSession 中,或者保存到 Cookie 中,待请求到来时,从 HttpSession 或者 Cookie 中取出来做校验。

2.参数校验

那接下来就是校验了。

校验主要是通过 CsrfFilter 过滤器来进行,我们来看下核心的 doFilterInternal 方法:

  1. protected void doFilterInternal(HttpServletRequest request, 
  2.   HttpServletResponse response, FilterChain filterChain) 
  3.     throws ServletException, IOException { 
  4.  request.setAttribute(HttpServletResponse.class.getName(), response); 
  5.  CsrfToken csrfToken = this.tokenRepository.loadToken(request); 
  6.  final boolean missingToken = csrfToken == null
  7.  if (missingToken) { 
  8.   csrfToken = this.tokenRepository.generateToken(request); 
  9.   this.tokenRepository.saveToken(csrfToken, request, response); 
  10.  } 
  11.  request.setAttribute(CsrfToken.class.getName(), csrfToken); 
  12.  request.setAttribute(csrfToken.getParameterName(), csrfToken); 
  13.  if (!this.requireCsrfProtectionMatcher.matches(request)) { 
  14.   filterChain.doFilter(request, response); 
  15.   return
  16.  } 
  17.  String actualToken = request.getHeader(csrfToken.getHeaderName()); 
  18.  if (actualToken == null) { 
  19.   actualToken = request.getParameter(csrfToken.getParameterName()); 
  20.  } 
  21.  if (!csrfToken.getToken().equals(actualToken)) { 
  22.   if (this.logger.isDebugEnabled()) { 
  23.    this.logger.debug("Invalid CSRF token found for " 
  24.      + UrlUtils.buildFullRequestUrl(request)); 
  25.   } 
  26.   if (missingToken) { 
  27.    this.accessDeniedHandler.handle(request, response, 
  28.      new MissingCsrfTokenException(actualToken)); 
  29.   } 
  30.   else { 
  31.    this.accessDeniedHandler.handle(request, response, 
  32.      new InvalidCsrfTokenException(csrfToken, actualToken)); 
  33.   } 
  34.   return
  35.  } 
  36.  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 请求到来时,下面这段代码都会执行:

  1. if (missingToken) { 
  2.  csrfToken = this.tokenRepository.generateToken(request); 
  3.  this.tokenRepository.saveToken(csrfToken, request, response); 

生成 CsrfToken 并保存,但实际上却没什么用,因为 GET 请求不需要 CSRF 攻击校验。

所以,Spring Security 官方又推出了 LazyCsrfTokenRepository。

LazyCsrfTokenRepository 实际上不能算是一个真正的 CsrfTokenRepository,它是一个代理,可以用来增强 HttpSessionCsrfTokenRepository 或者 CookieCsrfTokenRepository 的功能:

  1. public final class LazyCsrfTokenRepository implements CsrfTokenRepository { 
  2.  @Override 
  3.  public CsrfToken generateToken(HttpServletRequest request) { 
  4.   return wrap(request, this.delegate.generateToken(request)); 
  5.  } 
  6.  @Override 
  7.  public void saveToken(CsrfToken token, HttpServletRequest request, 
  8.    HttpServletResponse response) { 
  9.   if (token == null) { 
  10.    this.delegate.saveToken(token, request, response); 
  11.   } 
  12.  } 
  13.  @Override 
  14.  public CsrfToken loadToken(HttpServletRequest request) { 
  15.   return this.delegate.loadToken(request); 
  16.  } 
  17.  private CsrfToken wrap(HttpServletRequest request, CsrfToken token) { 
  18.   HttpServletResponse response = getResponse(request); 
  19.   return new SaveOnAccessCsrfToken(this.delegate, request, response, token); 
  20.  } 
  21.  private static final class SaveOnAccessCsrfToken implements CsrfToken { 
  22.   private transient CsrfTokenRepository tokenRepository; 
  23.   private transient HttpServletRequest request; 
  24.   private transient HttpServletResponse response; 
  25.  
  26.   private final CsrfToken delegate; 
  27.  
  28.   SaveOnAccessCsrfToken(CsrfTokenRepository tokenRepository, 
  29.     HttpServletRequest request, HttpServletResponse response, 
  30.     CsrfToken delegate) { 
  31.    this.tokenRepository = tokenRepository; 
  32.    this.request = request; 
  33.    this.response = response; 
  34.    this.delegate = delegate; 
  35.   } 
  36.   @Override 
  37.   public String getToken() { 
  38.    saveTokenIfNecessary(); 
  39.    return this.delegate.getToken(); 
  40.   } 
  41.   private void saveTokenIfNecessary() { 
  42.    if (this.tokenRepository == null) { 
  43.     return
  44.    } 
  45.  
  46.    synchronized (this) { 
  47.     if (this.tokenRepository != null) { 
  48.      this.tokenRepository.saveToken(this.delegate, this.request, 
  49.        this.response); 
  50.      this.tokenRepository = null
  51.      this.request = null
  52.      this.response = null
  53.     } 
  54.    } 
  55.   } 
  56.  
  57.  } 

这里,我说三点:

  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 组合。

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

  1. @Override 
  2. protected void configure(HttpSecurity http) throws Exception { 
  3.     http.authorizeRequests().anyRequest().authenticated() 
  4.             .and() 
  5.             .formLogin() 
  6.             .loginPage("/login.html"
  7.             .successHandler((req,resp,authentication)->{ 
  8.                 resp.getWriter().write("success"); 
  9.             }) 
  10.             .permitAll() 
  11.             .and() 
  12.             .csrf().csrfTokenRepository(new LazyCsrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())); 

4.小结

今天主要和小伙伴聊了一下 Spring Security 中 csrf 防御的原理。

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

生成 csrfToken 保存在 HttpSession 或者 Cookie 中。

请求到来时,从请求中提取出来 csrfToken,和保存的 csrfToken 做比较,进而判断出当前请求是否合法。

本文转载自微信公众号「江南一点雨」,可以通过以下二维码关注。转载本文请联系江南一点雨公众号。

 

文章来源网络,作者:运维,如若转载,请注明出处:https://shuyeidc.com/wp/131371.html<

(0)
运维的头像运维
上一篇2025-02-26 12:37
下一篇 2025-02-26 12:38

相关推荐

  • 个人主题怎么制作?

    制作个人主题是一个将个人风格、兴趣或专业领域转化为视觉化或结构化内容的过程,无论是用于个人博客、作品集、社交媒体账号还是品牌形象,核心都是围绕“个人特色”展开,以下从定位、内容规划、视觉设计、技术实现四个维度,详细拆解制作个人主题的完整流程,明确主题定位:找到个人特色的核心主题定位是所有工作的起点,需要先回答……

    2025-11-20
    0
  • 社群营销管理关键是什么?

    社群营销的核心在于通过建立有温度、有价值、有归属感的社群,实现用户留存、转化和品牌传播,其管理需贯穿“目标定位-内容运营-用户互动-数据驱动-风险控制”全流程,以下从五个维度展开详细说明:明确社群定位与目标社群管理的首要任务是精准定位,需明确社群的核心价值(如行业交流、产品使用指导、兴趣分享等)、目标用户画像……

    2025-11-20
    0
  • 香港公司网站备案需要什么材料?

    香港公司进行网站备案是一个涉及多部门协调、流程相对严谨的过程,尤其需兼顾中国内地与香港两地的监管要求,由于香港公司注册地与中国内地不同,其网站若主要服务内地用户或使用内地服务器,需根据服务器位置、网站内容性质等,选择对应的备案路径(如工信部ICP备案或公安备案),以下从备案主体资格、流程步骤、材料准备、注意事项……

    2025-11-20
    0
  • 如何企业上云推广

    企业上云已成为数字化转型的核心战略,但推广过程中需结合行业特性、企业痛点与市场需求,构建系统性、多维度的推广体系,以下从市场定位、策略设计、执行落地及效果优化四个维度,详细拆解企业上云推广的实践路径,精准定位:明确目标企业与核心价值企业上云并非“一刀切”的方案,需先锁定目标客户群体,提炼差异化价值主张,客户分层……

    2025-11-20
    0
  • PS设计搜索框的实用技巧有哪些?

    在PS中设计一个美观且功能性的搜索框需要结合创意构思、视觉设计和用户体验考量,以下从设计思路、制作步骤、细节优化及交互预览等方面详细说明,帮助打造符合需求的搜索框,设计前的规划明确使用场景:根据网站或APP的整体风格确定搜索框的调性,例如极简风适合细线条和纯色,科技感适合渐变和发光效果,电商类则可能需要突出搜索……

    2025-11-20
    0

发表回复

您的邮箱地址不会被公开。必填项已用 * 标注