SpringBoot + Sa-Token 实现 CSRF 令牌校验
前言
在前后端分离项目中,跨域请求与 CSRF 攻击一直是安全防护的重点。很多人只关注 Token 登录态,却忽略了浏览器层面的防护手段。本文将结合 Sa-Token 框架,手把手教你实现 4 种核心防御手段:CSRF 令牌、Referer/Origin 校验、Cookie SameSite/HttpOnly 配置,以及跨域请求拦截。
为什么这些配置很重要?
先快速回顾 CSRF 攻击的原理:攻击者通过诱导用户访问恶意网站,利用用户已登录的 Cookie,在目标网站执行未授权操作。防御的核心思路就是:
- 让攻击者无法拿到有效的身份凭证(Cookie/Token)
- 让浏览器拒绝携带凭证发起跨域请求
- 让服务器能识别并拦截非法来源的请求
基础配置:Sa-Token 与 Cookie 安全属性
Sa-Token 核心配置(适配前后端分离)
在 application.yml 中配置基础登录态规则,这里我们选择用 Header 传递 Token,同时开启 Cookie 安全属性(用于其他场景的防护):
sa-token:
token-name: Authorization
timeout: 28800
activity-timeout: 14400
is-concurrent: true
is-share: false
is-read-header: true
is-read-cookie: false # 前后端分离场景,不通过 Cookie 读 Token
token-prefix: "Bearer"
jwt-secret-key: system$2023-05CV-982131711
cookie:
same-site: Strict # 跨域请求不携带 Cookie
http-only: true # 禁止 JS 读取 Cookie,防 XSS 窃取
secure: false # 生产环境 HTTPS 必须设为 true
path: /2
3
4
5
6
7
8
9
10
11
12
13
14
15
关键属性解释
| 配置项 | 作用 | 安全意义 |
|---|---|---|
SameSite: Strict | 只有同站请求才会携带 Cookie | 直接阻断跨站请求的 Cookie 携带,从根源防 CSRF |
HttpOnly: true | 浏览器禁止 JS 读取 Cookie | 防止 XSS 攻击窃取会话 Cookie |
Secure: true | 仅在 HTTPS 环境下携带 Cookie | 防止中间人攻击窃取 Cookie |
第一道防线:CORS 跨域配置,拒绝非法来源
CORS 是浏览器层面的第一道拦截,配置正确的跨域规则,能让浏览器主动拒绝非白名单的跨域请求。
完整配置代码
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.Arrays;
import java.util.List;
@Slf4j
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Value("${spring.profiles.active:dev}")
private String activeProfile;
@Value("#{'${system.cors.origin_url:}'.split(',')}")
private List<String> allowedOrigins;
@Override
public void addCorsMappings(CorsRegistry registry) {
log.info("[CorsConfig] 当前环境:{},允许的 Origin:{}", activeProfile, allowedOrigins);
registry.addMapping("/**")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
var config = registry.getCorsConfigurations().get("/**");
// 测试/预发布/生产环境使用通配(仅内部环境,请勿对外暴露)
if (Arrays.asList("test", "stg", "prod").contains(activeProfile)) {
config.addAllowedOriginPattern("*");
} else {
// 开发环境严格校验白名单域名
if (allowedOrigins == null || allowedOrigins.isEmpty()) {
log.error("[CorsConfig] 开发环境允许的 Origin 为空,所有跨域请求将被拒绝");
config.setAllowedOrigins(List.of());
} else {
allowedOrigins.stream()
.map(String::trim)
.filter(s -> !s.isEmpty())
.forEach(config::addAllowedOrigin);
}
}
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
避坑指南
- 不要在生产环境使用
addAllowedOriginPattern("*"),会绕过浏览器的 CORS 校验。 allowCredentials(true)必须配合明确的allowedOrigins,否则浏览器会拒绝携带 Cookie。- 测试跨域时,工具请求(如 Apifox)默认不带 Origin 头,无法触发 CORS 校验,必须手动添加 Origin 头测试。
第二道防线:Sa-Token 拦截器,实现 Origin/Referer 校验
CORS 只能拦截浏览器发起的跨域请求,无法拦截工具请求。我们需要在 Sa-Token 拦截器中,手动校验请求来源,实现双重防护。
核心拦截逻辑
import cn.dev33.satoken.interceptor.SaInterceptor;
import cn.dev33.satoken.router.SaRouter;
import cn.dev33.satoken.stp.StpUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Arrays;
import java.util.List;
@Slf4j
@Configuration
public class SaTokenSecurityConfig {
@Value("${spring.profiles.active:dev}")
private String activeProfile;
@Value("#{'${system.cors.origin_url:}'.split(',')}")
private List<String> allowedOrigins;
@Bean
public SaInterceptor saInterceptor() {
return new SaInterceptor(handler -> {
// 1. 基础登录校验
StpUtil.checkLogin();
// 2. 开发环境 Origin/Referer 校验
if (!Arrays.asList("test", "stg", "prod").contains(activeProfile)) {
String origin = handler.getRequest().getHeader("Origin");
String referer = handler.getRequest().getHeader("Referer");
boolean isAllowed = false;
if (origin != null) {
isAllowed = allowedOrigins.stream().anyMatch(origin::startsWith);
} else if (referer != null) {
isAllowed = allowedOrigins.stream().anyMatch(referer::startsWith);
}
if (!isAllowed) {
log.warn("[安全拦截] 非法请求来源:Origin={}, Referer={}", origin, referer);
throw new RuntimeException("非法请求来源,拒绝访问");
}
}
});
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
基础防护效果验证
完成以上配置后,你的项目已经具备了基础的浏览器安全防护能力:
- 非白名单域名的跨域请求会被浏览器拦截。
- 工具请求不带 Origin/Referer 头时,会被服务器拦截。
- Cookie 已开启
HttpOnly和SameSite,降低 XSS 和 CSRF 风险。
我们将进阶实现 Sa-Token 自带的 CSRF 令牌校验,完成完整的安全防护闭环。
我们通过 CORS 配置和 Origin/Referer 校验,搭建了基础的浏览器安全防护。但这些手段无法防御同源下的 CSRF 攻击,也无法完全避免工具伪造请求。本文将结合 Sa-Token 自带的 SaCsrfUtil,实现随机、单次有效的 CSRF 令牌校验,完成安全防护的最后一环。
CSRF 令牌的核心原理
CSRF 令牌的防御逻辑非常简单:
- 用户登录后,服务器生成一个随机、一次性有效的 CSRF 令牌,返回给前端。
- 前端在发起敏感请求(POST/PUT/DELETE)时,必须携带这个令牌。
- 服务器校验令牌的有效性,校验通过后立即作废,确保令牌无法复用。
Sa-Token CSRF 令牌的使用
生成并返回 CSRF 令牌
在用户登录或页面加载时,生成令牌并返回给前端:
import cn.dev33.satoken.csrf.SaCsrfUtil;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/auth")
public class AuthController {
// 生成一次性 CSRF 令牌
@GetMapping("/csrf-token")
public String getCsrfToken() {
return SaCsrfUtil.createToken();
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
在 Sa-Token 拦截器中校验令牌
在之前的拦截器中,添加敏感请求的 CSRF 令牌校验逻辑:
@Slf4j
@Configuration
public class SaTokenSecurityConfig {
@Value("${spring.profiles.active:dev}")
private String activeProfile;
@Value("#{'${system.cors.origin_url:}'.split(',')}")
private List<String> allowedOrigins;
@Bean
public SaInterceptor saInterceptor() {
return new SaInterceptor(handler -> {
// 1. 基础登录校验
StpUtil.checkLogin();
// 2. 开发环境 Origin/Referer 校验
if (!Arrays.asList("test", "stg", "prod").contains(activeProfile)) {
// 校验逻辑同上一篇,此处省略
}
// 3. 敏感请求 CSRF 令牌校验
String method = handler.getRequest().getMethod();
if (Arrays.asList("POST", "PUT", "DELETE").contains(method)) {
SaCsrfUtil.checkToken();
}
});
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
前端如何携带 CSRF 令牌?
前端在发起敏感请求时,需要将令牌放入请求头中,Sa-Token 默认会从 X-CSRF-Token 头中读取令牌:
// 1. 页面加载时获取令牌
let csrfToken = "";
fetch("/auth/csrf-token")
.then(res => res.text())
.then(token => {
csrfToken = token;
});
// 2. 发起敏感请求时携带令牌
fetch("/api/user/update", {
method: "POST",
headers: {
"Authorization": `Bearer ${localStorage.getItem('token')}`,
"X-CSRF-Token": csrfToken,
"Content-Type": "application/json"
},
body: JSON.stringify({ name: "新名字" })
});2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
完整防护闭环:四种手段协同工作
现在我们已经实现了全部四种浏览器安全防护手段,它们各司其职,形成了完整的防护闭环:
| 防护手段 | 作用 | 防御场景 |
|---|---|---|
| SameSite/HttpOnly/Secure Cookie | 让浏览器不跨站携带 Cookie,且防止 JS 读取 | 跨站 CSRF、XSS 窃取 Cookie |
| CORS 跨域配置 | 让浏览器主动拒绝非白名单跨域请求 | 浏览器发起的跨域请求 |
| Origin/Referer 校验 | 服务器端校验请求来源,拒绝非法域名 | 工具伪造请求、绕过 CORS 的请求 |
| CSRF 令牌校验 | 敏感请求必须携带一次性有效令牌 | 同源 CSRF 攻击、工具重放请求 |
生产环境部署注意事项
- 强制 HTTPS:所有环境必须使用 HTTPS,否则
secure: true配置会导致 Cookie 无法携带。 - 令牌过期时间:可根据业务场景调整 CSRF 令牌的有效期,避免影响用户体验。
- 日志监控:对 Origin/Referer 校验失败、CSRF 令牌校验失败的请求,添加日志监控,及时发现异常攻击行为。
- 避免通配符配置:生产环境严禁使用
*作为跨域允许的 Origin,必须配置明确的白名单。
结语
浏览器安全防护不是靠某一种手段就能实现的,而是多种手段协同工作的结果。通过 Sa-Token 框架,我们可以快速实现 CORS、Origin/Referer 校验、Cookie 安全属性和 CSRF 令牌校验,为项目搭建起坚固的安全防线。
希望本文能帮你解决跨域与 CSRF 防护的痛点,写出更安全的前后端分离项目代码。
