1. 概述
本文将介绍跨站请求伪造(CSRF)攻击的原理,并演示如何通过 Spring Security 实现有效的防护机制。
2. 两种常见的 CSRF 攻击
CSRF 攻击形式多样,我们来看最常见的两种。
2.1. GET 请求攻击示例
假设用户已登录银行系统,执行如下 GET 请求转账:
GET http://bank.com/transfer?accountNo=1234&amount=100
攻击者若想将钱转到自己的账户(如 5678
),只需诱使用户触发如下请求:
GET http://bank.com/transfer?accountNo=5678&amount=1000
实现方式有:
✅ 链接诱骗:诱导用户点击链接:
<a href="http://bank.com/transfer?accountNo=5678&amount=1000">
Show Kittens Pictures
</a>
✅ 图片标签攻击:使用 <img>
标签自动加载恶意请求:
<img src="http://bank.com/transfer?accountNo=5678&amount=1000"/>
2.2. POST 请求攻击示例
若转账请求为 POST:
POST http://bank.com/transfer
accountNo=1234&amount=100
攻击者则需要构造如下请求:
POST http://bank.com/transfer
accountNo=5678&amount=1000
此时 <a>
和 <img>
不再适用,需使用表单:
<form action="http://bank.com/transfer" method="POST">
<input type="hidden" name="accountNo" value="5678"/>
<input type="hidden" name="amount" value="1000"/>
<input type="submit" value="Show Kittens Pictures"/>
</form>
配合 JavaScript 自动提交:
<body onload="document.forms[0].submit()">
<form>
...
2.3. 实战模拟
我们使用 Spring 构建一个简单的控制器来模拟攻击场景:
@Controller
public class BankController {
private Logger logger = LoggerFactory.getLogger(getClass());
@RequestMapping(value = "/transfer", method = RequestMethod.GET)
@ResponseBody
public String transfer(@RequestParam("accountNo") int accountNo,
@RequestParam("amount") final int amount) {
logger.info("Transfer to {}", accountNo);
...
}
@RequestMapping(value = "/transfer", method = RequestMethod.POST)
@ResponseStatus(HttpStatus.OK)
public void transfer2(@RequestParam("accountNo") int accountNo,
@RequestParam("amount") final int amount) {
logger.info("Transfer to {}", accountNo);
...
}
}
原始页面(正常操作):
<html>
<body>
<h1>CSRF test on Origin</h1>
<a href="transfer?accountNo=1234&amount=100">Transfer Money to John</a>
<form action="transfer" method="POST">
<label>Account Number</label>
<input name="accountNo" type="number"/>
<label>Amount</label>
<input name="amount" type="number"/>
<input type="submit">
</form>
</body>
</html>
攻击者页面(恶意):
<html>
<body>
<a href="http://localhost:8080/transfer?accountNo=5678&amount=1000">Show Kittens Pictures</a>
<img src="http://localhost:8080/transfer?accountNo=5678&amount=1000"/>
<form action="http://localhost:8080/transfer" method="POST">
<input name="accountNo" type="hidden" value="5678"/>
<input name="amount" type="hidden" value="1000"/>
<input type="submit" value="Show Kittens Picture">
</form>
</body>
</html>
⚠️ 关键前提:用户必须在原站已登录,携带 session cookie。
3. Spring MVC 应用防护
3.1. Spring Security 配置
在 Spring Security 4.x 之后,CSRF 防护默认开启。系统会自动将 CSRF token 添加到 _csrf
属性中。
禁用 CSRF(不推荐):
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf(AbstractHttpConfigurer::disable);
return http.build();
}
3.2. 客户端配置
前端需将 _csrf
中的 token 信息提交回服务器。
HTML 表单中添加隐藏字段:
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
JSON 请求则需设置 HTTP Header:
先在页面中添加 meta 标签:
<meta name="_csrf" content="${_csrf.token}"/>
<meta name="_csrf_header" content="${_csrf.headerName}"/>
然后使用 JavaScript 设置 header:
var token = $("meta[name='_csrf']").attr("content");
var header = $("meta[name='_csrf_header']").attr("content");
$(document).ajaxSend(function(e, xhr, options) {
xhr.setRequestHeader(header, token);
});
4. 无状态 Spring API 的防护
4.1. 后端配置
对于无状态 API,若使用 JWT 等 token 认证机制,则无需开启 CSRF 防护。
若仍使用 session cookie,则需启用并配置如下:
@Configuration
public class SecurityWithCsrfCookieConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf(csrf -> csrf.csrfTokenRepository
(CookieCsrfTokenRepository.withHttpOnlyFalse()));
return http.build();
}
}
这将返回一个名为 XSRF-TOKEN
的 cookie,前端可通过 JS 获取。
4.2. 前端配置
从 document.cookie
中提取 token:
const csrfToken = document.cookie.replace(/(?:(?:^|.*;\s*)XSRF-TOKEN\s*\=\s*([^;]*).*$)|^.*$/, '$1');
每次请求需在 header 中携带:
fetch(url, {
method: 'POST',
body: /* data to send */,
headers: { 'X-XSRF-TOKEN': csrfToken },
})
5. CSRF 禁用下的测试
当 CSRF 被禁用时,攻击者可以轻松发起请求:
@Test
public void givenAuth_whenAddFoo_thenCreated() throws Exception {
mvc.perform(
post("/foos").contentType(MediaType.APPLICATION_JSON)
.content(createFoo())
.with(testUser())
).andExpect(status().isCreated());
}
✅ 请求成功,无额外校验。
6. CSRF 启用后的测试
开启 CSRF 后,请求必须携带 token:
@Test
public void givenNoCsrf_whenAddFoo_thenForbidden() throws Exception {
mvc.perform(
post("/foos").contentType(MediaType.APPLICATION_JSON)
.content(createFoo())
.with(testUser())
).andExpect(status().isForbidden());
}
@Test
public void givenCsrf_whenAddFoo_thenCreated() throws Exception {
mvc.perform(
post("/foos").contentType(MediaType.APPLICATION_JSON)
.content(createFoo())
.with(testUser()).with(csrf())
).andExpect(status().isCreated());
}
❌ 无 token 则请求失败
✅ 使用 csrf()
方法可模拟 token
7. 总结
本文通过示例讲解了 CSRF 攻击的原理,并演示了如何在 Spring Security 中通过 CSRF token 机制进行有效防护。
✅ 要点回顾:
- CSRF 攻击利用用户已登录状态伪造请求
- Spring Security 默认启用 CSRF 防护
- MVC 应用通过
_csrf
属性注入 token - 无状态 API 若使用 session cookie 仍需防护
- 前端必须配合后端将 token 正确传递
完整示例代码可参考 GitHub 项目。