Origin检查机制在前端安全防护中的实战应用与常见陷阱

Des.艳鑫 安全 阅读 2,125
赞 26 收藏
二维码
手机扫码查看
反馈

谁更灵活?谁更省事?Origin检查这三招我试了个遍

Origin检查这事,我踩过两次坑才彻底清醒:第一次是后端同事说“你前端加个 Origin 校验就行”,结果我傻乎乎在 fetch 里手动比对 window.location.origin,上线第二天就被绕过了;第二次是改用 Nginx 做校验,结果 OPTIONS 预检没处理好,所有跨域 POST 直接 403,调试了三小时才发现 header 没透传。所以这次我拉上后端一起,把常见方案全跑了一遍——不是为了写论文,是真想搞清楚:哪招能让我少改代码、少看日志、少被 QA 打断吃饭。

Origin检查机制在前端安全防护中的实战应用与常见陷阱

我试过的三种主流方案

就三个:纯前端 JS 判断、服务端中间件(Node.js Express)、反向代理层(Nginx)。没提 Cloudflare Rules 或 CDN 边缘函数,因为咱小团队没那运维权限,也不打算为这点事去学 Lua。下面挨个说,带真实代码,不糊弄。

方案一:前端 JS 硬判断(别用,真别用)

这是我最早写的,也是最天真的一版:

// ❌ 千万别这么干!
fetch('https://jztheme.com/api/submit', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ data: 'xxx' })
}).then(res => {
  if (res.status === 403 && res.headers.get('X-Origin-Check') === 'failed') {
    alert('非法来源,拒绝请求');
  }
});

然后后端加了个 middleware:

// Express 中间件(伪代码,实际危险)
app.use('/api/*', (req, res, next) => {
  const origin = req.headers.origin;
  if (origin && !['https://my-app.com', 'https://staging.my-app.com'].includes(origin)) {
    return res.status(403).set('X-Origin-Check', 'failed').json({ error: 'Origin not allowed' });
  }
  next();
});

问题在哪?Origin 是客户端传的,随便 curl -H ‘Origin: https://evil.com’ 就能绕过。我后来用 Postman 测试,5 秒就 bypass 成功。更搞笑的是,这个逻辑还让本地开发环境(http://localhost:3000)永远 403,因为没加 localhost 白名单,改来改去最后发现:这玩意儿根本不是防攻击的,只是防手误。我删掉它那天,感觉像卸下了个假肢。

方案二:Node.js 层做 Origin 校验(我目前主力用)

这才是正经干活的地方。我把校验逻辑提到 Express 的入口层,严格区分预检和真实请求,且只信任 req.headers.origin ——但前提是它来自真实浏览器(即有 Origin 头),没 Origin 的请求(比如 curl 直连、Postman 默认不带)按需放行或拦截。

const ALLOWED_ORIGINS = [
  'https://my-app.com',
  'https://staging.my-app.com',
  'https://dev.jztheme.com'
];

app.use('/api/', (req, res, next) => {
  const origin = req.headers.origin;

  // 预检请求必须带 Origin,且必须白名单内
  if (req.method === 'OPTIONS') {
    if (!origin || !ALLOWED_ORIGINS.includes(origin)) {
      return res.sendStatus(403);
    }
    res.set({
      'Access-Control-Allow-Origin': origin,
      'Access-Control-Allow-Methods': 'GET,POST,PUT,DELETE,PATCH',
      'Access-Control-Allow-Headers': 'Content-Type,Authorization,X-Requested-With',
      'Access-Control-Allow-Credentials': 'true'
    });
    return res.sendStatus(204);
  }

  // 实际请求:只校验有 Origin 的,没 Origin 的走其他鉴权(如 token)
  if (origin && !ALLOWED_ORIGINS.includes(origin)) {
    return res.status(403).json({ error: 'Invalid Origin' });
  }

  next();
});

优点很明显:逻辑清晰、可 debug、能配合业务逻辑(比如某些接口允许任意 Origin,某些只允许可信域名)。我还在里面加了日志埋点,出问题直接查哪天哪个 Origin 被拦了。缺点?就是得 Node.js 接口层扛着,如果你们是 PHP 或 Java 后端,就得对应改。另外注意一点:Access-Control-Allow-Origin: *Access-Control-Allow-Credentials: true 不能共存,我踩过一次,导致登录态跨域失效,折腾半天才发现是这俩冲突。

方案三:Nginx 层硬拦截(适合静态资源或简单 API)

如果你们后端不归你管,或者压根没 Node 中间件(比如纯 PHP + Nginx 架构),那就直接上 Nginx。我配过两版,现在用的是这个:

# nginx.conf 片段
location /api/ {
  # 只允许指定 Origin,且必须存在(空值或缺失都拒)
  if ($http_origin !~ ^(https?://(my-app.com|staging.my-app.com|dev.jztheme.com)$)) {
    return 403;
  }

  # 预检响应头
  if ($request_method = 'OPTIONS') {
    add_header Access-Control-Allow-Origin $http_origin;
    add_header Access-Control-Allow-Methods "GET, POST, OPTIONS, PUT, DELETE";
    add_header Access-Control-Allow-Headers "Content-Type, Authorization, X-Requested-With";
    add_header Access-Control-Allow-Credentials "true";
    add_header Access-Control-Max-Age 86400;
    add_header Content-Length 0;
    add_header Content-Type text/plain;
    return 204;
  }

  # 实际请求头
  add_header Access-Control-Allow-Origin $http_origin;
  add_header Access-Control-Allow-Credentials "true";

  proxy_pass http://backend;
}

这个方案的优点是:快、稳、不依赖后端语言。我们有个老系统 PHP + Nginx,加完这条规则,Origin 拦截立刻生效,连重启都不用,reload 就行。但它也有硬伤:正则写错一个字符,整个 /api/ 下所有请求 403,而且 Nginx 错误日志里只报 “client sent invalid request”,根本看不出是正则问题。我上次少写了一个反斜杠,查了 40 分钟配置文件才定位到。还有就是没法做动态逻辑(比如根据用户角色放开 Origin),纯靠字符串匹配。

我的选型逻辑

结论很直接:只要后端是你能改的,一律选 Node.js 层校验。它可控、可测、可打日志、可配合业务流。Nginx 方案我只留给那种“老板说下周上线,但后端同事正在休产假”的极端场景。至于前端校验?早删了,现在它只活在我的 git commit 记录里,标题叫 “revert: frontend origin check (useless)”。

顺带提一句:如果你用 Next.js App Router,别想着在 middleware.ts 里做 Origin 校验——它不处理 OPTIONS 请求,预检直接 405。我试过,失败告终,最后还是回退到底层 Express 或 Vercel Edge Functions(但后者要付费)。

踩坑提醒:这三点一定注意

  • 本地开发时,Chrome 有时会把 http://localhost:3000 的 Origin 发成 http://localhost:3000/(带斜杠),而 Nginx 正则匹配不带斜杠的,导致 403。建议白名单统一加 / 或用 ^https?://.../?$ 写法
  • 某些安卓 WebView 或旧版微信内置浏览器会发空 Origin 或 null Origin,得单独兼容,不然用户点不动按钮
  • 如果你用 withCredentials: trueAccess-Control-Allow-Origin 必须是具体域名,不能是 *,这点没商量

以上是我的对比总结,有更优的实现方式欢迎评论区交流。比如你用 Rust/Tide 或 Go/Fiber 做 Origin 校验,也欢迎贴代码,我最近正琢磨换技术栈……

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论
一国曼
一国曼 Lv1
读完之后收获颇丰,会推荐给更多人看。
点赞 2
2026-02-17 15:25