在使用 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 中进行配置。例如:
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 模板中,可以使用如下代码:
<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 标签中:
<meta name="_csrf" content="${_csrf.token}"/>
<meta name="_csrf_header" content="${_csrf.headerName}"/>
然后在 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 保护:
@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,确保每个请求只会执行一次过滤操作。核心逻辑分为几步:加载令牌、验证令牌、生成新令牌并保存。
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 令牌。
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 令牌。
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 存储令牌。