那些年背过的题:Spring Security- CSRF源码分析

在使用 Spring Security 时,CSRF(跨站请求伪造,Cross-Site Request Forgery)保护是一个非常重要的安全特性。它可以防止恶意网站通过冒充用户请求来执行未授权操作。Spring Security 默认启用了 CSRF 保护,但你也可以根据需要进行配置。

1. CSRF 基本原理

CSRF 攻击通过诱使已认证用户在目标网站上执行不希望的操作。例如,当用户登录到某个网站后,攻击者可能会诱使用户点击链接或访问某个页面,从而在不知情的情况下发送恶意请求。CSRF 保护通过在每个状态改变的请求(如POST、PUT、DELETE等)中加入一个随机令牌来防止这种攻击。

2. Spring Security 中的 CSRF 配置

在 Spring Security 中,CSRF 保护默认是开启的。如果你想自定义 CSRF 的行为,可以在你的 WebSecurityConfigurerAdapter 中进行配置。例如:

 

java

代码解读

复制代码

import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .csrf() .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) .and() .authorizeRequests() .antMatchers("/public/**").permitAll() .anyRequest().authenticated(); } }

3. 获取和使用 CSRF 令牌

为了使 CSRF 保护生效,你需要在每个有状态变化的请求中包含 CSRF 令牌。通常,这可以通过以下几种方式实现:

表单提交

对于传统的基于表单的应用程序,Spring 提供了自动生成 CSRF 令牌的方法。在 Thymeleaf 模板中,可以使用如下代码:

 

html

代码解读

复制代码

<form th:action="@{/process}" method="post"> <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/> <!-- 其他表单字段 --> <button type="submit">提交</button> </form>

AJAX 请求

对于现代的 SPA 应用程序,通常通过 JavaScript 来发起 AJAX 请求。这时需要手动将 CSRF 令牌添加到请求头中。假设你已经将 CSRF 令牌放在页面的 meta 标签中:

这份面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题

需要全套面试笔记的【点击此处即可】即可免费获取

html

代码解读

复制代码

<meta name="_csrf" content="${_csrf.token}"/> <meta name="_csrf_header" content="${_csrf.headerName}"/>

然后在 JavaScript 中读取并添加到请求头:

 

javascript

代码解读

复制代码

const token = document.querySelector('meta[name="_csrf"]').getAttribute('content'); const header = document.querySelector('meta[name="_csrf_header"]').getAttribute('content'); fetch('/api/endpoint', { method: 'POST', headers: { [header]: token, 'Content-Type': 'application/json' }, body: JSON.stringify({ /* 数据 */ }) }) .then(response => response.json()) .then(data => console.log(data)) .catch(error => console.error('Error:', error));

4. 禁用 CSRF 保护

在某些情况下,例如开发环境或者需要对无状态的 RESTful API 进行特殊处理时,可能需要临时禁用 CSRF 保护。可以在 HttpSecurity 配置中禁用 CSRF 保护:

 

java

代码解读

复制代码

@Override protected void configure(HttpSecurity http) throws Exception { http .csrf().disable() .authorizeRequests() .antMatchers("/public/**").permitAll() .anyRequest().authenticated(); }

请注意,禁用 CSRF 保护会带来安全风险,因此应谨慎使用。

核心源码分析

Spring Security 的 CSRF 保护机制涉及多个关键组件,其中 CsrfFilter 和 CsrfTokenRepository 是核心部分。接下来,我将详细解析其源码实现的核心部分,包括各个组件之间如何协作来完成 CSRF 保护。

1. CsrfFilter

CsrfFilter 是负责处理 CSRF 校验的过滤器。它继承了 OncePerRequestFilter,确保每个请求只会执行一次过滤操作。核心逻辑分为几步:加载令牌、验证令牌、生成新令牌并保存。

 

java

代码解读

复制代码

public class CsrfFilter extends OncePerRequestFilter { private final CsrfTokenRepository tokenRepository; public CsrfFilter(CsrfTokenRepository tokenRepository) { Assert.notNull(tokenRepository, "tokenRepository cannot be null"); this.tokenRepository = tokenRepository; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { CsrfToken csrfToken = tokenRepository.loadToken(request); boolean missingToken = csrfToken == null; if (missingToken) { csrfToken = tokenRepository.generateToken(request); tokenRepository.saveToken(csrfToken, request, response); } request.setAttribute(CsrfToken.class.getName(), csrfToken); request.setAttribute(csrfToken.getParameterName(), csrfToken); if (shouldDoCsrf(request)) { String actualToken = request.getHeader(csrfToken.getHeaderName()); if (actualToken == null) { actualToken = request.getParameter(csrfToken.getParameterName()); } if (!csrfToken.getToken().equals(actualToken)) { throw new CsrfException("Invalid CSRF Token"); } } filterChain.doFilter(request, response); } private boolean shouldDoCsrf(HttpServletRequest request) { return !HttpMethod.GET.matches(request.getMethod()) && !HttpMethod.HEAD.matches(request.getMethod()) && !HttpMethod.TRACE.matches(request.getMethod()) && !HttpMethod.OPTIONS.matches(request.getMethod()); } }

2. CsrfTokenRepository

CsrfTokenRepository 是一个接口,用于管理 CSRF 令牌的生成、存储和加载。主要有两种常见实现:HttpSessionCsrfTokenRepository 和 CookieCsrfTokenRepository

HttpSessionCsrfTokenRepository

该类使用 HTTP 会话来存储 CSRF 令牌。其关键方法包括:

  • generateToken: 生成新的 CSRF 令牌。
  • saveToken: 将 CSRF 令牌保存到会话中。
  • loadToken: 从会话中加载 CSRF 令牌。
 

java

代码解读

复制代码

public class HttpSessionCsrfTokenRepository implements CsrfTokenRepository { static final String DEFAULT_CSRF_PARAMETER_NAME = "_csrf"; static final String DEFAULT_CSRF_HEADER_NAME = "X-CSRF-TOKEN"; 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; @Override public CsrfToken generateToken(HttpServletRequest request) { return new DefaultCsrfToken(this.headerName, this.parameterName, createNewToken()); } @Override public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response) { HttpSession session = request.getSession(false); if (session != null) { session.setAttribute(this.sessionAttributeName, token); } } @Override public CsrfToken loadToken(HttpServletRequest request) { HttpSession session = request.getSession(false); if (session != null) { return (CsrfToken) session.getAttribute(this.sessionAttributeName); } return null; } private String createNewToken() { return UUID.randomUUID().toString(); } }

CookieCsrfTokenRepository

该类使用 Cookie 来存储 CSRF 令牌。其关键方法包括:

  • generateToken: 生成新的 CSRF 令牌。
  • saveToken: 将 CSRF 令牌保存到 Cookie 中。
  • loadToken: 从 Cookie 中加载 CSRF 令牌。
 

java

代码解读

复制代码

public class CookieCsrfTokenRepository implements CsrfTokenRepository { private String parameterName = "_csrf"; private String headerName = "X-CSRF-TOKEN"; private String cookieName = "XSRF-TOKEN"; private boolean httpOnly = true; private String cookiePath = "/"; private boolean secure = false; private int cookieMaxAge = -1; @Override public CsrfToken generateToken(HttpServletRequest request) { return new DefaultCsrfToken(this.headerName, this.parameterName, createNewToken()); } @Override public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response) { if (token == null) { Cookie cookie = new Cookie(this.cookieName, ""); cookie.setMaxAge(0); cookie.setPath(this.cookiePath); response.addCookie(cookie); } else { String tokenValue = token.getToken(); Cookie cookie = new Cookie(this.cookieName, tokenValue); cookie.setSecure(this.secure); cookie.setPath(this.cookiePath); cookie.setHttpOnly(this.httpOnly); if (this.cookieMaxAge > -1) { cookie.setMaxAge(this.cookieMaxAge); } response.addCookie(cookie); } } @Override public CsrfToken loadToken(HttpServletRequest request) { Cookie[] cookies = request.getCookies(); if (cookies == null) { return null; } for (Cookie cookie : cookies) { if (this.cookieName.equals(cookie.getName())) { String token = cookie.getValue(); return new DefaultCsrfToken(this.headerName, this.parameterName, token); } } return null; } private String createNewToken() { return UUID.randomUUID().toString(); } // Getters and setters for the various properties can be added here... }

核心组件解析

1. CsrfFilter
  • 职责:负责拦截 HTTP 请求并执行 CSRF 令牌的验证。

  • 流程

    1. 加载请求中的 CSRF 令牌。
    2. 如果没有找到令牌,则生成一个新的令牌并保存。
    3. 将令牌设置到请求属性中,便于后续处理使用。
    4. 对非安全方法(如 POST、PUT、DELETE)进行 CSRF 校验。
    5. 若校验失败则抛出异常,否则继续过滤链。
2. CsrfTokenRepository
  • 职责:管理 CSRF 令牌的生命周期,包括生成、保存和加载令牌。

  • 常见实现

    • HttpSessionCsrfTokenRepository:通过会话存储令牌。
    • CookieCsrfTokenRepository:通过 Cookie 存储令牌。
  • 26
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值