在这里插入图片描述

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

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

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

“我明明只是点了条私信里的链接,银行卡里的钱怎么就飞走了?”——客服小姐姐带着哭腔的这句话,让我第一次真切体会到 CSRF 的杀伤力。那天凌晨两点,我一边远程帮她挂失银行卡,一边在心里把“跨站请求伪造”这六个字默念了八百遍。它不像 XSS 那样大张旗鼓地往页面里插脚本,而是像个隐形人,悄悄替你完成“自愿”操作:给奇怪的文章点赞、把收货地址改成尼日利亚、甚至顺手转个账。最关键的是——你确实“登录过”,所以一切看起来都合法。今天我们就把这位隐形人拖到聚光灯下,从原理、场景到代码实现,一层层扒光它的衣服,再给它套上七重铠甲,让攻击者只能原地跺脚。

什么是 CSRF:从用户视角看“被操作”的恐怖瞬间

想象这样一个早晨:你喝着咖啡,打开邮箱,发现“网易官方”给你发了封“中秋抽奖”邮件。你兴冲冲地点了链接,页面跳转到“网易抽奖”(其实是攻击者的域名),背景放着喜庆的月亮动画。五秒后,页面自动关闭,你耸耸肩继续写代码。十分钟后,收到银行短信:已向陌生账户转出 5200 元。你整个人瞬间清醒——我啥都没干啊?!可服务器日志里清清楚楚写着:POST /transfer 带着你的 Cookie,带着你的 Session,甚至 User-Agent 都和平时一模一样。这就是 CSRF 的精髓:利用浏览器“请求自动带凭证”的贴心小棉袄,让恶意站点冒充你发起业务请求。你确实没输入密码,但浏览器已经帮你把身份证递过去了。

CSRF 攻击的典型场景还原:点赞、转账、改密码一个不落

为了让你有身临其境的酸爽感,我搭了一个“迷你微博”演示站,代码仓库放在 GitHub(别问地址,问就是内部项目)。下面三段代码,分别对应三种日常到令人发指的场景。

场景一:点赞按钮的“被”点击

<!-- 攻击者页面 evil.html -->
<body>
  <!-- 用 img 伪装 GET,实际后端用了 POST,这里只是演示 -->
  <img src="https://weibo.com/like?id=9527" />
  <!-- 或者更隐蔽的表单自动提交 -->
  <form id="f" action="https://weibo.com/like" method="POST">
    <input type="hidden" name="id" value="9527" />
  </form>
  <script>
    document.getElementById('f').submit();
  </script>
</body>

只要你在微博保持登录,哪怕只是扫了眼这张页面,浏览器也会带着你的 Cookie 把赞点了。后端一看:Cookie 有效,用户“主动”点赞,OK,入库!于是你莫名其妙给“买粉丝送鸡蛋”的营销号点了个大 heart。

场景二:转账接口的“被”调用

<!-- 攻击者诱导你点击的链接 -->
<a
  href="https://bank.example/transfer?to=attacker&amount=5200"
  target="_blank"
  style="display:block;width:100%;height:100%;position:fixed;top:0;left:0;z-index:9999;">
</a>

如果银行接口真敢用 GET 做转账,那基本等于裸奔。更常见的还是 POST,于是攻击者准备了一份隐藏表单:

<form action="https://bank.example/transfer" method="POST" id="t">
  <input name="to" value="attacker" />
  <input name="amount" value="5200" />
</form>
<script>
  // 3 秒后自动提交,给你足够时间切到后台刷个微博
  setTimeout(() => t.submit(), 3000);
</script>

场景三:修改密码的“被”操作

某些后台把“修改密码”做成 RESTful 风格,PUT /user/password 搭配 JSON 体。攻击者只需在恶意页面里发一段 fetch:

fetch('https://admin.example/api/user/password', {
  method: 'PUT',
  credentials: 'include', // 关键:带 Cookie
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ pwd: 'hacked666' })
});

你一边看动画,一边密码就被改成 hacked666。等你下次登录失败想找回密码时,发现预留邮箱也被改了,哭都来不及。

浏览器同源策略为何拦不住 CSRF?深入理解请求自动携带凭据机制

很多人以为“同源策略”是尚方宝剑,其实它只管读不管写:它阻止的是恶意脚本读取返回结果,却丝毫不限制“跨源请求”本身。只要请求是“简单请求”(Simple Request),浏览器会先放行,再带上你的 Cookie、HTTP Basic 认证、客户端证书,一路绿灯到服务器。服务器返回 200,浏览器只是不让你读 response——但转账、点赞、改密码这类“写操作”根本不在乎回包长啥样。于是,攻击者只需确保“请求发出去”就够了,同源策略只能在一旁干瞪眼。

主流防御方案全景图:Token、SameSite、双重 Cookie 全解析

防御 CSRF 的思路只有一条:让攻击者“发得出请求却带不对凭证”。具体实现拆成三大流派:

  1. 同步令牌派(Synchronizer Token):后端在页面里埋随机令牌,前端回发时带回来校验。
  2. SameSite Cookie 派:把 Cookie 的 SameSite 属性设成 Lax 或 Strict,让浏览器拒绝跨站带 Cookie。
  3. 双重提交 Cookie 派:把令牌同时写进 Cookie 和请求参数/头,后端比较两者是否一致。

三者可叠加,互为备胎。下面挨个拆给你看。

同步令牌(Synchronizer Token)模式详解:前后端如何配合生成与校验

后端:Node + Express 示例

// app.js
import csrf from 'csurf';
import cookieParser from 'cookie-parser';
import session from 'express-session';

const app = express();
app.use(cookieParser());
app.use(session({ secret: 'keyboard cat', resave: false, saveUninitialized: true }));

// 初始化 CSRF 中间件,默认把令牌写进 req.csrfToken()
app.use(csrf());

// 全局中间件,把令牌塞进所有模板
app.use((req, res, next) => {
  res.locals.csrfToken = req.csrfToken();
  next();
});

// 渲染表单
app.get('/publish', (req, res) => {
  res.send(`
    <form action="/publish" method="POST">
      <input type="hidden" name="_csrf" value="${res.locals.csrfToken}" />
      <input name="title" placeholder="文章标题" />
      <button>发布</button>
    </form>
  `);
});

// 处理提交
app.post('/publish', (req, res) => {
  // csurf 中间件已自动校验 _csrf 字段
  res.json({ msg: '发布成功,令牌比对通过!' });
});

前端:纯 HTML 表单

上面那段 res.send 已经演示了“后端直出”模式,前端啥都不用干,直接收令牌即可。但如果你是前后端分离,Vue/React 自行调接口,那就得把令牌回写:

// React 例子:组件加载时请求令牌
const [token, setToken] = useState('');
useEffect(() => {
  fetch('/api/csrf-token', { credentials: 'include' })
    .then((r) => r.json())
    .then((d) => setToken(d.csrfToken));
}, []);

// 发业务请求时带上
const submit = () =>
  fetch('/publish', {
    method: 'POST',
    credentials: 'include',
    headers: { 'Content-Type': 'application/json', 'CSRF-Token': token },
    body: JSON.stringify({ title: 'Hello CSRF' })
  });

后端对应接口:

app.get('/api/csrf-token', (req, res) => {
  res.json({ csrfToken: req.csrfToken() });
});

校验失败怎么办?

csurf 默认抛 403,你可以自定义错误页:

app.use((err, req, res, next) => {
  if (err.code !== 'EBADCSRFTOKEN') return next(err);
  res.status(403).send('兄弟,令牌不对,怕不是被 CSRF 了吧?');
});

SameSite Cookie 属性实战配置:Lax 还是 Strict?别再搞混了

SameSite 有三个值:None、Lax、Strict。一句话总结:

  • Strict:完全禁止跨站,任何情况下都不带 Cookie。适合后台管理系统,但用户体验差:点外链跳进来就得重新登录。
  • Lax:默认平衡策略,只允许“安全”的 GET/HEAD/OPTIONS 且是顶级导航(地址栏变)时带 Cookie。POST 跨站会被拦。
  • None:彻底躺平,必须同时指定 Secure,即 HTTPS。

配置示例:

// 登录接口返回
res.cookie('sessionid', sessionId, {
  httpOnly: true,
  secure: true,
  sameSite: 'Lax' // 或者 'Strict'
});

Lax 能挡住 90% 的 POST 型 CSRF,但拦不住“预加载”+“GET 接口写操作”这种奇葩组合。所以千万别单押它,最好和 Token 双保险。

双重提交 Cookie 技巧:轻量但需注意的边界情况

思路简单:后端把随机值写进 Cookie,前端读取后再回写到请求头/参数,后端比较两者。示例:

// 后端设置 Cookie
app.use((req, res, next) => {
  if (!req.cookies.doubleSubmit) {
    const ds = crypto.randomBytes(16).toString('hex');
    res.cookie('doubleSubmit', ds, { httpOnly: false, sameSite: 'Lax' });
    req.dsCookie = ds;
  } else {
    req.dsCookie = req.cookies.doubleSubmit;
  }
  next();
});

// 前端读取并回写
fetch('/api/doSomething', {
  method: 'POST',
  credentials: 'include',
  headers: { 'X-Double-Submit': getCookie('doubleSubmit') } // 自己封装读取
});

// 后端校验
app.post('/api/doSomething', (req, res) => {
  if (req.headers['x-double-submit'] !== req.dsCookie) {
    return res.status(403).send('双重提交失败');
  }
  res.json({ ok: 1 });
});

注意:子域名可以覆盖 Cookie,所以如果你的业务允许 *.example.com 共享登录态,得把域名锁死,避免兄弟域名使坏。

验证 HTTP Referer 头:靠谱吗?哪些场景会失效?

Referer 是最省事的“穷办法”:只要请求来源不是本站域名,直接 403。但坑也最多:

  1. 隐私模式或 HTTPS→HTTP 时浏览器会藏 Referer。
  2. 企业网关、代理软件有时会剥 Referer。
  3. 用户手动敲地址、收藏夹打开时 Referer 为空。

代码示例:

const ALLOWED = ['https://example.com', 'https://www.example.com'];
app.use((req, res, next) => {
  const ref = req.get('Referer');
  if (!ref || !ALLOWED.some((d) => ref.startsWith(d))) {
    return res.status(403).send('Referer 不合法');
  }
  next();
});

结论:Referer 只能当“锦上添花”的辅助校验,千万别当主防线。

前端在 CSRF 防御中的角色:不只是等后端保护,这些事你也得做

很多前端同学觉得“CSRF 是后端的事”,结果掉坑里了。其实前端至少能做三件正经事:

  1. 主动在 AJAX 统一拦截器里加 Token,别每个接口手写。
  2. 对“零表单”场景(如上传、富文本)动态插入 Token。
  3. 配合浏览器新特性 Fetch Metadata,把可疑请求提前掐掉。

统一拦截器示例(axios):

import axios from 'axios';

// 请求拦截:统一加令牌
axios.interceptors.request.use((config) => {
  config.headers['X-CSRF-Token'] = localStorage.getItem('csrfToken');
  return config;
});

// 响应拦截:发现 403 且是 CSRF 错误,自动刷新令牌并重试
axios.interceptors.response.use(
  (res) => res,
  async (err) => {
    if (err.response?.status === 403 && err.response.data === 'CSRF') {
      const { data } = await axios.get('/api/csrf-token', { retry: 0 });
      localStorage.setItem('csrfToken', data.csrfToken);
      // 把原请求的令牌换掉再发一次
      err.config.headers['X-CSRF-Token'] = data.csrfToken;
      return axios(err.config);
    }
    return Promise.reject(err);
  }
);

Fetch Metadata 则是浏览器在请求头里加一行 Sec-Fetch-Site: cross-site,前端可配合后端做“提前枪毙”:

// 前端做不了判断,但可以提示用户
if (navigator.sendBeacon) {
  navigator.sendBeacon('/log', JSON.stringify({ hint: '疑似跨站请求' }));
}

常见误区大揭秘:比如“用了 HTTPS 就安全”“API 不需要防 CSRF”

  1. HTTPS 只防窃听和篡改,不防“冒充”。CSRF 请求本身就是合法加密流量。
  2. “我们的接口是 RESTful + JSON,表单发不过来。”——攻击者可以用 fetch 发 JSON,带 credentials 就行。
  3. “用了 JWT 就高枕无忧。”——只要 JWT 存在 Cookie 里,依旧会被自动携带。
  4. “SPA 路由跳转不会触发 CSRF。”——路由跳不跳无所谓,只要恶意页面能发请求就够。

开发中踩过的坑:Token 未刷新、AJAX 未带凭证、跨域配置冲突

坑一:Token 只生成一次,用户开一晚电脑,第二天继续操作,结果令牌过期被 403。解决:Token 可以设置滑动过期,每次请求后更新 Cookie。

坑二:axios 忘了 withCredentials: true,导致 Cookie 带不过去,后端以为你没登录,直接 302 到登录页,前端还一脸懵。

坑三:CORS 把 Access-Control-Allow-Origin 写成 * 却又要带 Cookie,浏览器直接报错。正确写法是指定明确域名:

res.header('Access-Control-Allow-Origin', 'https://spa.example.com');
res.header('Access-Control-Allow-Credentials', 'true');

调试 CSRF 问题的排查路线图:从浏览器 Network 面板到服务端日志联动分析

  1. 复现步骤:用 curl 或者 Postman 直接发不带令牌的请求,看能否重现 403。
  2. Network 面板:确认请求头里有没有 X-CSRF-Token,Cookie 里有没有 doubleSubmit
  3. 服务端日志:打印 req.headersreq.cookies,对比令牌值。
  4. 如果本地难复现,把 RefererOrigin 日志也打出来,看是不是跨站被拦。
  5. 打开 Chrome chrome://net-export/,抓底层网络包,确认 SameSite 拦截记录。

自动化测试 CSRF 防护是否生效:用 Postman 或 curl 模拟恶意请求

Postman 测试集合:

  1. 新建请求,URL 填转账接口,Method 选 POST,Body 写 JSON。
  2. Tests 标签里断言返回 403:
pm.test('CSRF blocked', () => {
  pm.response.to.have.status(403);
});
  1. 再把 Token 加进 Header,跑第二遍,断言 200。

curl 一行流:

# 不带令牌,期望 403
curl -b "session=xxx" -X POST https://example/transfer -d "to=attacker&amount=5200"

# 带令牌,期望 200
curl -b "session=xxx" -H "X-CSRF-Token: yyyy" -X POST https://example/transfer -d "to=attacker&amount=5200"

集成到 CI:用 GitHub Actions 跑 nightly,令牌错误就发钉钉告警,让漏洞不过夜。

进阶技巧:结合 CSP 和 Fetch Metadata 进一步加固防线

CSP 的 form-action 指令可以限制页面只能向指定域名提交表单:

res.header(
  'Content-Security-Policy',
  "default-src 'self'; form-action 'self' https://safe.example.com;"
);

Fetch Metadata 则让后端做更细粒度的来源判断:

const ALLOW = ['same-origin', 'same-site'];
if (!ALLOW.includes(req.headers['sec-fetch-site'])) {
  return res.status(403).send('Cross-site request denied');
}

双剑合璧,能把大部分“水坑”型攻击提前拒之门外。

别让 CSRF 成为你的“背锅侠”:上线前这份检查清单请收好

  1. 所有写操作接口是否强制校验 Token?
  2. Cookie 是否标记 SameSite=LaxStrict
  3. CORS 是否错配 * 且允许凭证?
  4. 前端拦截器是否统一加 Token?
  5. 文件上传、富文本、GraphQL 等特殊接口是否漏网?
  6. 有没有写自动化测试, nightly 跑一遍?
  7. 生产环境日志是否记录 RefererSec-Fetch-Site 方便回溯?
  8. 安全响应预案:一旦收到异常转账,30 分钟内能否回滚?

把这张清单贴到 Confluence,发版前打钩,谁漏项谁请全组喝奶茶。毕竟,钱被转走可以追,头发掉了可长不回来。愿各位前端小伙伴写完这篇,都能睡个安稳觉,不再被凌晨两点的客服电话惊醒。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等工具

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

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

在这里插入图片描述

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐