嘿,刚入职场的你,是不是觉得“安全”这两个字离自己很远?毕竟咱们写代码主要是为了实现功能:按钮一点,数据保存,页面跳转,搞定收工。但今天我要跟你聊聊一个听起来很高深,实则每天都在发生、且能让你的公司损失惨重甚至面临法律风险的坑——CSRF(跨站请求伪造)。
别被这个缩写吓跑。简单来说,CSRF就是黑客伪装成你的浏览器,在你不知情的情况下,替你在银行转账、修改密码或者删除数据。这就像是你正在家里吃饭,隔壁老王偷偷拿着你的钥匙,把你家的电视卖了,还说是你自愿卖的。
作为新人,你不需要成为安全专家,但你必须知道如何构建“防御工事”。这篇指南不讲枯燥的理论堆砌,我们直接上干货,从最经典的Token校验方案,聊到现代浏览器的原生防护SameSite,手把手教你把这些漏洞补上。
为什么你的“正常”请求会被认为是“恶意”的?
首先,我们要理解CSRF攻击的核心前提:浏览器会自动携带Cookie。
假设你在银行网站A登录了,银行A会在你的浏览器里存一个Session ID的Cookie。当你访问黑客网站B时,网站B里藏了一个隐藏的表单,或者一段自动提交的JS代码,指向银行A的转账接口。
关键点来了:当你点击网站B上的某个看似无害的按钮(或者甚至什么都不点,自动触发),浏览器在向银行A发起请求时,会自动带上那个Session ID Cookie。对于银行A来说,这个请求看起来完全合法:“哦,这是一个拥有有效Session ID的用户发来的转账请求。”
这就是CSRF的恐怖之处:它利用了浏览器对同源策略的信任,混淆了“用户意愿”和“用户身份”。
第一道防线:传统的CSRF Token机制
这是目前企业级应用中最主流、兼容性最好的方案。它的核心思想是:每个表单或关键请求,都必须包含一个只有服务器和用户浏览器知道的随机字符串(Token)。黑客网站B无法读取你浏览器里存储的这个Token,所以它无法构造出合法的请求。
1. 后端实现逻辑
通常,Token的生成和验证流程如下:
- 用户首次访问页面时:后端生成一个唯一的、随机的CSRF Token,将其存入用户的Session中,同时将这个Token返回给前端,嵌入到HTML表单的隐藏字段中,或者放在HTTP Header里。
- 用户提交表单时:前端将这个Token一起发送给后端。
- 后端接收请求时:后端从请求中提取Token,并与Session中存储的Token进行比对。如果一致,放行;如果不一致或不存在,拒绝请求。
2. 代码实战:以Java Spring Boot为例
很多新人可能用的是Spring框架,这里给你一个完整的、可直接复用的示例。
第一步:引入依赖
确保你的pom.xml中有Spring Security CSRF支持(通常默认开启,但我们需要自定义处理):
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
第二步:配置WebSecurityConfigurerAdapter
在较新的Spring Boot版本中,推荐使用SecurityFilterChain Bean。我们需要确保CSRF保护是启用的,并自定义Token的获取方式。
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler;
import org.springframework.security.web.csrf.CsrfToken;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.util.function.Supplier;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf
// 使用自定义的Token处理器,将Token放入request attribute,方便前端获取
.csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler())
// 忽略某些不需要Token的请求,比如静态资源或第三方回调
.ignoringRequestMatchers("/api/public/**", "/callback/**")
)
.authorizeHttpRequests(auth -> auth
.anyRequest().authenticated()
);
return http.build();
}
}
第三步:前端如何获取并使用Token?
这里有个小技巧。Spring Security默认会将Token放在HTTP响应头X-CSRF-TOKEN中。我们可以写一个简单的JavaScript工具函数来自动拦截Ajax请求。
// csrf-helper.js
// 1. 从响应头中获取Token并保存到全局变量或localStorage
function getCsrfToken(response) {
const token = response.headers.get('X-CSRF-Token');
if (token) {
window.csrfToken = token;
}
}
// 2. 在发送请求前,自动添加Token
function addCsrfHeader(request) {
if (window.csrfToken) {
request.headers.set('X-CSRF-TOKEN', window.csrfToken);
}
}
// 3. 封装fetch调用示例
async function safePost(url, data) {
try {
// 先做一个GET请求来获取Token(如果之前没有的话)
// 注意:很多现代框架会自动处理这一步,但手动控制更透明
const initOptions = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
// 如果已经有缓存的Token,先加上
...(window.csrfToken ? { 'X-CSRF-TOKEN': window.csrfToken } : {})
},
body: JSON.stringify(data)
};
let response = await fetch(url, initOptions);
// 如果返回403,可能是Token过期或未获取,尝试重新获取Token后重试
if (response.status === 403) {
// 这里可以触发一个GET请求去刷新Token,具体取决于你的后端实现
// 简单起见,我们假设前端在初始化时已经通过GET /csrf-token 获取了Token
throw new Error("CSRF Token mismatch");
}
return response;
} catch (error) {
console.error("CSRF Request Failed:", error);
throw error;
}
}
给新人的建议:如果你使用的是Vue或React,通常会有Axios或Fetch的拦截器(Interceptor)。在拦截器的request阶段,检查是否存在CSRF Token,如果有,就把它加到Header里。这是最优雅的做法。
3. 为什么Token方案有效?
因为同源策略限制了JavaScript读取其他域名的Cookie。黑客网站B虽然能诱导你的浏览器向银行A发送请求,但它无法通过JS读取银行A返回的CSRF Token。因此,它构造的请求中缺少正确的Token,银行A的后端验证失败,攻击被阻断。
第二道防线:SameSite Cookie属性
如果你觉得每次都要生成Token、验证Token很麻烦,或者你想利用现代浏览器的原生能力,那么SameSite属性就是你的救星。
1. 什么是SameSite?
SameSite是一个HTTP响应头属性,用于控制Cookie在跨站请求时是否被发送。它有三个值:
Strict:Cookie绝不会在跨站请求中发送。这是最安全的,但可能会影响用户体验(比如你不能通过第三方链接登录)。Lax:Cookie仅在三种情况下发送:- 顶级导航(用户点击链接跳转到另一个网站)。
- GET方法请求。
- 预检请求(Preflight)。
- 关键点:POST请求、PUT、DELETE等非GET请求,如果是跨站的,不会携带Cookie。这足以防御大多数CSRF攻击,因为CSRF攻击通常需要改变状态(即非GET请求)。
None:Cookie会在所有跨站请求中发送。注意:设置为None时,必须同时设置Secure标志(即只能HTTPS传输)。
2. 如何配置?
Java/Spring Boot配置示例:
import org.springframework.boot.web.servlet.server.Cookie;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.boot.autoconfigure.web.servlet.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Bean
public Cookie cookie() {
Cookie cookie = new Cookie("JSESSIONID", "");
cookie.setHttpOnly(true); // 防止JS读取Cookie,防XSS
cookie.setSecure(true); // 仅HTTPS传输
cookie.setSameSite("Lax"); // 设置SameSite为Lax
return cookie;
}
}
Nginx配置示例:
如果你在前端反向代理层配置,可以在Nginx中添加:
location / {
proxy_pass http://backend_server;
# 修改Set-Cookie头
proxy_hide_header Set-Cookie;
add_header Set-Cookie "JSESSIONID=abc123; Path=/; HttpOnly; Secure; SameSite=Lax";
}
3. SameSite的优缺点分析
优点:
- 零代码侵入:不需要后端生成Token,前端不需要修改Ajax请求。
- 性能更好:减少了网络往返和计算开销。
- 现代化:Chrome、Firefox、Safari等主流浏览器都支持。
缺点/风险:
- 旧浏览器兼容性问题:虽然现在旧浏览器占比很小,但在一些企业内部系统(如使用IE11或老旧Android WebView)中可能不生效。
- 不能单独依赖:
Lax模式防御的是非GET请求的CSRF。如果你的业务允许通过GET请求执行敏感操作(比如/api/delete-user?id=123),那么SameSite=Lax是无法防御的,因为GET请求会携带Cookie。最佳实践是:永远不要使用GET请求执行写操作!
第三道防线:双重验证与行为分析(进阶)
对于金融级应用,仅仅靠Token或SameSite可能还不够。我们可以结合其他手段。
1. 自定义请求头(Custom Headers)
浏览器有一个特性:JavaScript不能设置自定义的HTTP请求头,除非通过XMLHttpRequest或fetch的headers参数显式设置,而跨域的fetch请求会触发CORS预检(OPTIONS请求)。
如果黑客网站B通过<img>标签或<form>标签发起跨站请求,浏览器不会自动添加自定义Header(如X-Requested-With: XMLHttpRequest)。
后端验证逻辑:
// 伪代码
if (isCrossOriginRequest(request)) {
String customHeader = request.getHeader("X-CSRF-TOKEN"); // 或者你定义的其他Header
if (customHeader == null || !validateToken(customHeader)) {
throw new AccessDeniedException("CSRF Attack Detected");
}
}
这种方法实际上和Token校验类似,但它利用了浏览器的CORS机制增加了攻击难度。
2. 检查Referer头
在HTTP请求头中,有一个Referer字段,表示请求的来源页面。
后端验证逻辑:
String referer = request.getHeader("Referer");
if (referer != null && referer.startsWith("https://yourbank.com")) {
// 允许请求
} else {
// 拒绝请求,可能是CSRF攻击
throw new AccessDeniedException("Invalid Referer");
}
注意:这种方法不推荐作为唯一的安全措施,因为Referer可以被用户隐私设置屏蔽,或者在某些代理环境下丢失。但它可以作为辅助验证手段。
给职场新人的避坑指南:常见错误
- 只依赖Referer校验:如上所述,Referer不可信。
- 忘记设置HttpOnly:如果你的Cookie没有设置
HttpOnly,那么XSS(跨站脚本攻击)就可以读取Cookie,进而窃取Session ID或CSRF Token,导致所有防御失效。记住:Cookie + HttpOnly + Secure + SameSite 是现代Web安全的黄金组合。 - 在GET请求中执行状态变更:这是架构设计的大忌。CSRF攻击往往针对POST/PUT/DELETE请求。如果你的业务允许通过GET请求删除用户,那么即使有CSRF Token,攻击者也可以通过构造一个图片链接
<img src="https://bank.com/delete-user?id=1">来触发攻击(因为图片加载是GET请求,且浏览器会自动携带Cookie)。解决方案:将所有写操作改为POST/PUT/DELETE,并配合Token或SameSite校验。 - Token绑定错误:有些实现将CSRF Token绑定到IP地址。但这在移动网络(NAT)下会导致问题,因为多个用户可能共享同一个公网IP。建议绑定到Session ID,而不是IP。
总结:如何构建你的防御体系?
作为新人,你不需要一次性实现所有高级特性。按照以下优先级逐步加固:
- 基础层:确保所有敏感操作不使用GET请求。
- 核心层:实施CSRF Token机制。这是目前最通用、最可靠的方案。无论前端用什么框架,后端验证Token都是底线。
- 增强层:配置Cookie的
SameSite=Lax属性。这能防御大部分非GET请求的CSRF,且无需额外代码。 - 加固层:设置Cookie的
HttpOnly和Secure标志,防止Cookie被窃取。 - 监控层:记录所有失败的CSRF验证请求,分析是否有异常流量。
最后,我想说,安全不是一次性的任务,而是一个持续的过程。当你下次看到403 Forbidden错误时,不要只觉得是权限问题,多想一想,是不是有人在试图绕过你的CSRF防护?
希望这份指南能帮你在职业生涯的起步阶段,建立起坚实的安全意识。记住,写出能运行的代码是及格,写出安全的代码才是优秀。加油!
