在使用 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 令牌的验证。
-
流程:
- 加载请求中的 CSRF 令牌。
- 如果没有找到令牌,则生成一个新的令牌并保存。
- 将令牌设置到请求属性中,便于后续处理使用。
- 对非安全方法(如 POST、PUT、DELETE)进行 CSRF 校验。
- 若校验失败则抛出异常,否则继续过滤链。
2. CsrfTokenRepository
-
职责:管理 CSRF 令牌的生命周期,包括生成、保存和加载令牌。
-
常见实现:
HttpSessionCsrfTokenRepository
:通过会话存储令牌。CookieCsrfTokenRepository
:通过 Cookie 存储令牌。