前端开发者必防:CSRF攻击原理与实战防护指南

在这里插入图片描述

前端开发者必防:CSRF攻击原理与实战防护指南

前端开发者必防:CSRF攻击原理与实战防护指南

“哥,我就点了个点赞链接,怎么余额没了?”
——某位刚被 CSRF 教育过的同事,在工位上抱着咖啡杯怀疑人生。

如果你以为“登录成功=天下太平”,那就太小看浏览器的“热心肠”了。它会在背后默默帮你带饼干(Cookie),而坏人只需要一张“精心包装”的<img>标签,就能让浏览器把饼干连同你的转账请求一起送到银行——这就是 CSRF(Cross-Site Request Forgery,跨站请求伪造)。今天咱们不背名词,直接拆台:从浏览器为啥这么傻,到前端、后端、运维、测试怎么一起把 CSRF 按在地上摩擦,顺带奉上一车代码,能复制粘贴绝不让你手打。


你以为登录就安全了?小心 CSRF 悄悄替你转账

故事从一个表情包开始。

午休时,你把银行页面开着,Tab 图标安安静静地亮着“已登录”的小绿锁。隔壁组小姐姐发来一张“猫跳机械舞”的 GIF,你顺手点开,页面里除了猫,还有一行肉眼看不见的标签:

<img src="https://bank.example/transfer?to=Attacker&amount=10000">

浏览器一看:哟,这是 bank.example 的域名,我刚好有 Cookie,捎上!
于是请求大摇大摆地过去了,银行后端一验 Cookie:嗯,本人,过!
十秒后,你收到短信:【尊敬的用户,您已转出 10000 元】
你:???我手都没碰键盘!

这就是 CSRF 的骚操作——“借你之手,干我坏事”。它利用的是浏览器“自动带 Cookie”的潜规则,而不是 XSS 那种“先注入脚本再执行”的骚套路。换句话说,CSRF 攻击的请求百分之百来自真实用户,签名、Cookie、Session 样样齐全,就差在脑门上写“我是坏人”。


从“借刀杀人”理解跨站请求伪造的本质

把 CSRF 想成古装剧里的“借刀杀人”:

  1. 你是御前侍卫(用户),手里有御赐宝刀(Cookie/Session)。
  2. 坏人(攻击者)造了一把一模一样的刀鞘(请求参数),趁你不注意,把刀插进去(浏览器自动发请求)。
  3. 守卫(服务器)一看刀是真的,就放行。
  4. 人头落地,你还一脸懵:我刀怎么自己动了?

核心条件只有三条,记住它,面试能吹五分钟:

  • 目标站点仅按 Cookie 识别身份,不看其他“暗号”。
  • 用户刚好登录过,Cookie 还没过期。
  • 攻击者提前猜到请求参数(转账接口、点赞接口、删除接口……),并能诱导用户点链接/访问页面。

浏览器的“信任陷阱”:同源策略拦不住 CSRF

很多人对“同源策略”有误解,以为它是全能保镖。
其实同源策略只限制,不限制
只要请求是“跨域写”(POST、PUT、DELETE),浏览器就放行,只是不让脚本读响应而已。
CSRF 正好只需要“写”——把钱转走、把文章删了、把密码改了——读不读响应无所谓
于是同源策略在旁边吃瓜:你们忙,我不管。


点赞、转账、删账号,一个链接全搞定

下面这张表,建议截图当壁纸,下次需求评审直接甩产品脸上:

业务场景接口示例是否带 Cookie是否幂等是否可被 CSRF
查询余额GET /balance否(只读)
点赞文章POST /like?id=1
修改邮箱PUT /email
删除账号DELETE /account
上传头像POST /avatar(multipart)

结论:只要请求会“写”数据,且只认 Cookie,就有可能被 CSRF。
别再问“GET 请求会不会被 CSRF”——GET 理论上不该改数据,但架不住有人把“删除”写成 GET,于是悲剧。


防御基石:同步令牌(Synchronizer Token)机制详解

思路一句话

服务器给前端发一张“暗号票”(Token),前端写请求时把票带上,后端验票通过才执行。
因为票是随机且一次性,攻击者猜不到,也读不到(同源策略限制读),于是请求失效。

完整实战:Node(Express) + React

后端:发牌员(生成 Token)
// server/app.js
import express from 'express';
import csurf from 'csurf';
import cookieParser from 'cookie-parser';

const app = express();
app.use(cookieParser());
// 关键中间件:同步令牌
const csrfProtection = csurf({ cookie: true });
// 把令牌塞进接口,让前端自己拿
app.get('/api/csrf-token', csrfProtection, (req, res) => {
  res.json({ token: req.csrfToken() }); // 每次返回新 Token
});
// 需要保护的写接口
app.post('/api/transfer', csrfProtection, (req, res) => {
  // 只有 Token 校验通过才能到这儿
  console.log('转账成功', req.body);
  res.json({ ok: 1 });
});
app.listen(3001);
前端:出牌员(把 Token 塞请求头)
// src/utils/request.js  统一封装
import axios from 'axios';

// 先拿 Token,再发业务请求
export async function safePost(url, data) {
  // 1. 每次业务请求前,先拿最新 Token
  const { data: { token } } = await axios.get('/api/csrf-token');
  // 2. 通过自定义头把 Token 带过去,名字任意,前后端对齐即可
  return axios.post(url, data, {
    headers: { 'x-csrf-token': token },
    withCredentials: true // 别忘了 Cookie
  });
}

// 使用处
import { safePost } from '@/utils/request';
async function handleLike() {
  await safePost('/api/transfer', { to: 'Alice', amount: 1 });
  alert('点赞成功');
}
注意细节
  • Token 可以存内存,也可以放 Redux、Pinia,千万别放 Cookie——Cookie 会自动带,就白忙活了。
  • 对 SPA 来说,首屏渲染前先异步拉 Token,否则用户手速快会 403。
  • 表单 SSR 场景,直接把 Token 渲染成 <input type=hidden name=_csrf value=XXX>,后端验 name=_csrf 字段即可。

SameSite Cookie:现代浏览器给 CSRF 设下的路障

如果你不想折腾 Token,又想让老项目一夜“免疫”80% CSRF,SameSite 是最低成本的大招。

原理一句话

Cookie 加 SameSite=StrictLax 属性后,浏览器会根据“跨站”情况决定要不要带这枚饼干
跨站 POST、PUT、DELETE 直接被浏览器拦截,请求都发不出去,还谈什么伪造?

操作示范:一行代码的事

// server/login.js
res.cookie('sessionid', sessId, {
  httpOnly: true,
  secure: true,
  sameSite: 'Strict' // 或 'Lax'
});
SameSite 取值场景举例是否带 Cookie兼容性
Strict任何跨站都不带最严老浏览器不支持
Lax顶级导航 GET 可带,POST/iframe 不带折中主流 OK
None都带(必须同时加 Secure)最松所有

踩坑提醒

  1. 用了 SameSite=Strict 后,用户从邮件里点开你的链接会“未登录”,因为第一次请求不带 Cookie。
  2. 如果业务需要被第三方站点 iframe 嵌入,只能 SameSite=None; Secure,同时上 HTTPS。
  3. 老版本 Chrome(<67)不认识 SameSite,不能当作唯一防线,必须搭配 Token。

双重提交 Cookie:轻量又有效的防护策略实操

“双重提交”是 Token 的穷亲戚:不占用服务端内存,前端把 Cookie 值抄一份放请求头,后端验“两头是否一致”即可。
优点:无状态、易横向扩展;缺点:得自己实现、对 XSS 敏感(Cookie 被读就凉)。

实战:Spring Boot + Vue

后端:发 Cookie,同时验头
@GetMapping("/api/token")
public void genDsc(HttpServletResponse res){
    String random = UUID.randomUUID().toString();
    // 1. 种 Cookie,httpOnly=false,让前端 js 能读
    Cookie c = new Cookie("DSC", random);
    c.setPath("/");
    c.setHttpOnly(false);
    c.setSecure(true);
    res.addCookie(c);
    // 2. 同时也返到 body,方便前端直接拿
    res.getWriter().write("{\"dsc\":\""+random+"\"}");
}

@PostMapping("/api/del")
public Result del(@CookieValue("DSC") String cookieDsc,
                  @RequestHeader("X-DSC") String headerDsc){
    if(!cookieDsc.equals(headerDsc)) throw new RuntimeException("双重提交失败");
    return Result.ok();
}
前端:读 Cookie → 放 Header
// Vue3 + axios
import axios from 'axios';
import { getCookie } from '@/utils/cookie'; // 自己封装读 document.cookie

export function delPost(id: number){
  // 1. 先拿 DSC Cookie(后端已种下)
  const dsc = getCookie('DSC');
  return axios.post('/api/del', { id }, {
    headers: { 'X-DSC': dsc },
    withCredentials: true
  });
}

验证 HTTP Referer 和 Origin 头:别忽视请求的“来路证明”

Referer/Origin 是浏览器自带的“来路证明”。
后端直接拒绝“来路不明”的请求,简单粗暴。
但别当唯一防线:

  1. 企业内网网关、隐私插件会主动剥 Referer,容易误杀;
  2. 部分老旧浏览器(IE11 某些模式)不发 Origin
  3. HTTPS→HTTP 的降级请求Referer 会被浏览器掐掉

代码示例:Nginx 一层拦截

# 只允许来自自站的 POST
map $http_origin $allow {
    ~^https://my.com$  1;
    default            0;
}
server {
    location /api/ {
        if ($request_method = POST) {
            set $chk "${allow}0";
            if ($chk = "00") { return 403; }
        }
        proxy_pass http://backend;
    }
}

前后端协作防线:前端埋点 + 后端校验缺一不可

很多团队把安全全部推给后端,前端只画按钮,这是耍流氓
前端能做的不止“拉 Token”:

  • 敏感操作二次确认(弹窗、指纹、人脸识别),让“静默伪造”变“用户可感知”。
  • 把大接口拆成小接口 + 阶梯权限。比如“提现”拆成“申请提现”+“短信确认”,两次 Token 独立。
  • 上线前跑自动化脚本:用 Puppeteer 模拟跨站表单提交,断言返回 403,否则 CI 直接失败。

常见误区:仅靠验证码或 HTTPS就能防住 CSRF?

  • 验证码:只能防“重复提交”,不能防跨站第一次提交。攻击者可以先正常访问页面拿到验证码,再构造请求。
  • HTTPS:防的是“中间人”,不是“坏人借你的身份”。HTTPS 甚至让 CSRF 更难被网络层发现,因为流量被加密。
  • CORS 配置正确就不会 CSRF?CORS 控制的是“读”响应,CSRF 是“写”请求,两码事。

开发中的真实翻车现场:Token 没绑定用户、动态表单遗漏防护

翻车 1:Token 全局复用,没和用户绑定

// 错误示范:Token 存在全局变量,A 用户登录后 B 用户直接用
app.use((req, res, next) => {
  req.csrfToken = globalToken; // ❌ 所有人共用
});

正确姿势:csurf 中间件默认把 Token 存在 Session,Session 与用户绑定,别手贱改。

翻车 2:动态表单没更新 Token

// 错误示范:React 路由切换时没重新拉 Token
const [token, setToken] = useState(window.TOKEN_FROM_HTML);
function addRow(){
  // 用户 F5 后 Token 是新值,但 SPA 内跳转没刷新,导致 403
}

修复:路由切换后重新 GET /api/csrf-token,或把 Token 过期时间设短 + 自动刷新。


排查思路:如何快速判断一个漏洞是不是 CSRF?

  1. 打开 Burp → 右键 Engagement tools → Generate CSRF PoC → 生成 HTML。
  2. 换浏览器(或无痕)登录受害账号,保持 Cookie 有效。
  3. 用 Burp 自带的浏览器打开 PoC,如果请求 200 并且数据被改,基本坐实。
  4. 看后端日志:有没有自定义头/Token?Referer 是不是外站?SameSite 值?
  5. 如果接口返回 400 bad csrf token403 missing x-csrf-token,说明已有防护,但前端调用姿势不对

调试技巧:用浏览器开发者工具模拟和拦截可疑请求

  • Network 面板 → 右键请求 → Copy as fetch,改完 Origin 再回车,看后端是否拦截。
  • Sources → Overrides,把页面里 Token 改成 12345,刷新后看是否 403。
  • Application → Cookies,双击改 SameSite 值,实时验证不同策略。
  • Chrome 94+ 支持“CSRF 保护演示”:地址栏打开 chrome://flags/#network-service-csrf-protection,开启后可看浏览器级别的拦截日志。

进阶防护组合拳:Token + SameSite + 自定义请求头三重保险

单点防御总有“漏网之鱼”,小孩才做选择,大人全都要
推荐“三明治”配置:

  1. SameSite=Lax:挡掉 80% 广告条、邮件里的跨站 POST。
  2. 自定义头 + Token
    • 所有 JSON 请求统一带 X-Requested-With: XMLHttpRequest(axios 默认)。
    • 后端拒绝一切没有该头的 POST,因为浏览器<form><img> 无法自定义头,天然防住最原始的 CSRF
  3. 双重提交 Cookie:给无状态服务用,横向扩展无压力。
// axios 拦截器统一加标识
axios.interceptors.request.use(config => {
  config.headers['X-Requested-With'] = 'XMLHttpRequest';
  return config;
});

隐藏雷区:SPA 应用、第三方嵌入、API 网关下的 CSRF 新挑战

SPA:路由跳转不刷新,Token 怎么续?

  • 方案 A:Token 过期 15 min,每次路由跳转异步刷新
  • 方案 B:GraphQL 统一走 /graphql在 HTTP 头里带 Token,与 REST 互不干扰。

第三方嵌入:你的页面被 iframe 到 evil.com

  • 后端加 X-Frame-Options: SAMEORIGIN 或 CSP frame-ancestors 'self'
  • 如果必须允许嵌入,只能 SameSite=None; Secure + Token 双重保险,同时把敏感操作再包一层短信验证码

API 网关:Spring Cloud Gateway 转发请求,Token 存在哪?

  • 网关层不负责验 Token,只负责把 Cookie 里的 SessionID 转发到用户服务
  • 验 Token 放到下游微服务,防止“网关改代码,业务无感知”导致漏洞。

让安全成为习惯:在项目脚手架中内置 CSRF 防护模板

别再指望“上线前渗透测试”给你擦屁股,安全左移才是正道。
把下面这套模板写进公司 Yeoman/CLI 仓库,新项目一键生成,谁不用的 MR 直接打回:

├── template
│   ├── _package.json           // 依赖 csurf / spring-security
│   ├── src
│   │   ├── utils
│   │   │   └── safeRequest.js // 自带 Token 自动续期
│   │   └── hooks
│   │       └── useCsrf.tsx    // React Hook,页面挂载即拉 Token
│   └── nginx.conf              // 已配好 Referer 白名单

CI 再加一条:

# .gitlab-ci.yml
csrf-test:
  script:
    - npm run test:csrf  # 使用 OWASP ZAP 脚本,跑 5 分钟,失败就红 pipe

结语:把 CSRF 想象成“隔壁老王借你钥匙”

浏览器很热心,谁敲门都递钥匙;
服务器很憨厚,见钥匙就开门;
攻击者最狡猾,钥匙不用偷,只要让你“亲手”递过去。
前端、后端、运维、测试,人人都是保安
下次需求评审,把这篇文章甩出去,让产品经理知道:加一个“确认”按钮,不只是体验,更是安全

毕竟,代码可以复制粘贴,余额却不能

欢迎来到我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。

推荐:DTcode7的博客首页。
一个做过前端开发的产品经理,经历过睿智产品的折磨导致脱发之后,励志要翻身农奴把歌唱,一边打入敌人内部一边持续提升自己,为我们广大开发同胞谋福祉,坚决抵制睿智产品折磨我们码农兄弟!


专栏系列(点击解锁)学习路线(点击解锁)知识定位
《微信小程序相关博客》 持续更新中~结合微信官方原生框架、uniapp等小程序框架,记录请求、封装、tabbar、UI组件的学习记录和使用技巧等
《AIGC相关博客》 持续更新中~AIGC、AI生产力工具的介绍,例如stable diffusion这种的AI绘画工具安装、使用、技巧等总结
《HTML网站开发相关》 《前端基础入门三大核心之html相关博客》前端基础入门三大核心之html板块的内容,入坑前端或者辅助学习的必看知识
《前端基础入门三大核心之JS相关博客》前端JS是JavaScript语言在网页开发中的应用,负责实现交互效果和动态内容。它与HTML和CSS并称前端三剑客,共同构建用户界面。
通过操作DOM元素、响应事件、发起网络请求等,JS使页面能够响应用户行为,实现数据动态展示和页面流畅跳转,是现代Web开发的核心
《前端基础入门三大核心之CSS相关博客》介绍前端开发中遇到的CSS疑问和各种奇妙的CSS语法,同时收集精美的CSS效果代码,用来丰富你的web网页
《canvas绘图相关博客》Canvas是HTML5中用于绘制图形的元素,通过JavaScript及其提供的绘图API,开发者可以在网页上绘制出各种复杂的图形、动画和图像效果。Canvas提供了高度的灵活性和控制力,使得前端绘图技术更加丰富和多样化
《Vue实战相关博客》持续更新中~详细总结了常用UI库elementUI的使用技巧以及Vue的学习之旅
《python相关博客》持续更新中~Python,简洁易学的编程语言,强大到足以应对各种应用场景,是编程新手的理想选择,也是专业人士的得力工具
《sql数据库相关博客》持续更新中~SQL数据库:高效管理数据的利器,学会SQL,轻松驾驭结构化数据,解锁数据分析与挖掘的无限可能
《算法系列相关博客》持续更新中~算法与数据结构学习总结,通过JS来编写处理复杂有趣的算法问题,提升你的技术思维
《IT信息技术相关博客》持续更新中~作为信息化人员所需要掌握的底层技术,涉及软件开发、网络建设、系统维护等领域的知识
《信息化人员基础技能知识相关博客》无论你是开发、产品、实施、经理,只要是从事信息化相关行业的人员,都应该掌握这些信息化的基础知识,可以不精通但是一定要了解,避免日常工作中贻笑大方
《信息化技能面试宝典相关博客》涉及信息化相关工作基础知识和面试技巧,提升自我能力与面试通过率,扩展知识面
《前端开发习惯与小技巧相关博客》持续更新中~罗列常用的开发工具使用技巧,如 Vscode快捷键操作、Git、CMD、游览器控制台等
《photoshop相关博客》 持续更新中~基础的PS学习记录,含括PPI与DPI、物理像素dp、逻辑像素dip、矢量图和位图以及帧动画等的学习总结
日常开发&办公&生产【实用工具】分享相关博客》持续更新中~分享介绍各种开发中、工作中、个人生产以及学习上的工具,丰富阅历,给大家提供处理事情的更多角度,学习了解更多的便利工具,如Fiddler抓包、办公快捷键、虚拟机VMware等工具

吾辈才疏学浅,摹写之作,恐有瑕疵。望诸君海涵赐教。望轻喷,嘤嘤嘤

非常期待和您一起在这个小小的网络世界里共同探索、学习和成长。愿斯文对汝有所裨益,纵其简陋未及渊博,亦足以略尽绵薄之力。倘若尚存阙漏,敬请不吝斧正,俾便精进!

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

DTcode7

客官,赏个铜板吧

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值