Origin检查机制在前端安全防护中的实战应用与常见陷阱
谁更灵活?谁更省事?Origin检查这三招我试了个遍
Origin检查这事,我踩过两次坑才彻底清醒:第一次是后端同事说“你前端加个 Origin 校验就行”,结果我傻乎乎在 fetch 里手动比对 window.location.origin,上线第二天就被绕过了;第二次是改用 Nginx 做校验,结果 OPTIONS 预检没处理好,所有跨域 POST 直接 403,调试了三小时才发现 header 没透传。所以这次我拉上后端一起,把常见方案全跑了一遍——不是为了写论文,是真想搞清楚:哪招能让我少改代码、少看日志、少被 QA 打断吃饭。
我试过的三种主流方案
就三个:纯前端 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: true,Access-Control-Allow-Origin必须是具体域名,不能是*,这点没商量
以上是我的对比总结,有更优的实现方式欢迎评论区交流。比如你用 Rust/Tide 或 Go/Fiber 做 Origin 校验,也欢迎贴代码,我最近正琢磨换技术栈……
