跳至正文
来两杯美式
返回

CORS 与 Web 安全实战白皮书

By 来两杯美式
发布于更新于

涵盖从本地开发到线上部署的全场景细节,可直接作为团队技术规范文档。

一、跨域的底层逻辑:浏览器在保护谁?

跨域拦截,拦截的从来不是”请求的发起”,而是”响应的接收”。

同源策略的严格定义

“同源”指的是三个要素完全一致:协议(Protocol) + 域名(Domain) + 端口(Port)。 只要有任何一个不同,浏览器的”同源策略(Same-Origin Policy)“就会生效。

跨域的本质:限制”读”,放任”写”

二、跨域生命周期细节:简单请求 vs 非简单请求

浏览器根据请求的特征,将其分为两类,处理流程完全不同。

简单请求

触发条件(必须全部满足):

  1. 方法GETPOSTHEAD
  2. Header:只能包含 CORS 安全列表字段(AcceptAccept-LanguageContent-LanguageContent-TypeRange)。其中 Content-TypeRange 有额外限制(见下)。
  3. Content-Type 只能是:text/plainmultipart/form-dataapplication/x-www-form-urlencoded
  4. Range 只能是单范围值(如 bytes=256-bytes=127-255),不支持多范围。
  5. 请求中的任何 XMLHttpRequestUpload 对象均没有注册任何事件监听器,且未使用 ReadableStream 对象。

执行细节:浏览器不发预检,直接发出真实请求。由于不需要通行证就能直接”触达”后端,如果后端没有专门防范,写操作(POST)极易被恶意第三方网站利用。

非简单请求与 OPTIONS 预检

触发条件:只要不满足简单请求的条件(最常见的是发送 application/json 数据,或者带了自定义 Token 请求头)。

执行细节与性能优化

三、全场景跨域解决方案与生产级配置

场景 A:本地开发阶段(Dev Server Proxy)

本地联调时,通常不启动 Nginx。前端框架自带的 Node.js 代理是解决跨域的最佳方案。

原理:前端代码请求本地 Node 服务(同源),Node 服务再把请求转发给测试环境的后端(服务端通信,无浏览器同源策略限制)。

Vite 配置示例 (vite.config.ts):

export default defineConfig({
  server: {
    proxy: {
      "/api": {
        target: "http://test-env-backend.com",
        changeOrigin: true, // 必须开启,将请求头中的 Origin 修改为目标 URL,避免后端校验失败
        rewrite: path => path.replace(/^\/api/, ""), // 路径重写
      },
    },
  },
});

场景 B:生产环境纯 Nginx 代理(网关层管控推荐)

⚠️ 规范铁律:如果决定把跨域交给 Nginx 处理,后端代码中请务必删除所有跨域注解(如 SpringBoot 的 @CrossOrigin),防止 Double CORS 冲突导致浏览器直接报错!

安全隐患警示:网络常见的 add_header Access-Control-Allow-Origin $http_origin; 属于盲返 Origin 漏洞。它等同于允许任意恶意网站跨域携带凭证访问后端,在代码审计与安全扫描中属于高危红线。必须使用 map 建立域名白名单。

生产级 Nginx 配置模板(安全白名单、204预检、5xx报错覆盖):

# 1. 在 nginx.conf 的 http 块中定义允许跨域的域名白名单
map $http_origin $cors_origin {
    default "";
    "~^https?://(localhost|127\.0\.0\.1)(:\d+)?$" $http_origin; # 本地开发
    "~^https://([a-z0-9-]+\.)?your-frontend\.com$"       $cors_origin; # 生产环境及子域名
}

server {
    listen 80;
    server_name www.your-backend.com;

    location /api/ {
        # 2. 拦截预检请求 (OPTIONS),直接在 Nginx 层面光速响应,不打扰后端
        if ($request_method = 'OPTIONS') {
            add_header Access-Control-Allow-Origin $cors_origin always;
            add_header Access-Control-Allow-Methods "GET, POST, OPTIONS, PUT, DELETE" always;
            # 根据业务实际补充允许前端携带的自定义 Header
            add_header Access-Control-Allow-Headers "Content-Type, Authorization, X-Requested-With, X-CSRF-Token" always;
            add_header Access-Control-Allow-Credentials true always;
            add_header Access-Control-Max-Age 86400 always; # 缓存预检结果1天
            return 204;
        }

        # 3. 正式请求的跨域头附加(always 确保即使后端报错 500/502 也能带上跨域头)
        add_header Access-Control-Allow-Origin $cors_origin always;
        add_header Access-Control-Allow-Credentials true always;

        # 4. 允许前端 JavaScript 能够读取到的自定义响应头
        # 默认前端只能读到 7 个基础响应头,若需读取分页数或 CSRF-Token 必须在此显式暴露
        add_header Access-Control-Expose-Headers "X-CSRF-Token, X-Total-Count" always;

        # 5. 转发给真实的业务后端
        proxy_pass http://127.0.0.1:8080/;
    }
}

四、Cookie 跨域与 SameSite 深度排雷

要实现跨域携带 Cookie(如 SSO 单点登录、Session 共享),必须达成严苛的”三方协议”。

前端:主动亮明态度

无论是 Axios 还是 Fetch,默认都会把跨域 Cookie 拦截。前端必须显式开启:

// Axios
axios.get("https://api.b.com/data", { withCredentials: true });

// Fetch API
fetch("https://api.b.com/data", { credentials: "include" });

后端/网关:严苛的 Origin 限制

带凭证(Cookie / Authorization)时的铁律

如果前端开启了凭证携带(withCredentials: true / credentials: 'include'),CORS 响应头有一条铁律:

带凭证时 * 失效的完整行为:

请求类型Cookie 到后端?前端拿到响应?原因
简单请求 + ACAO: * + 凭证✅ 会❌ 被拦请求已发出无法撤回,但浏览器拒绝交出响应
非简单请求 + ACAO: * + 凭证❌ 不会❌ 被拦预检就失败,真实请求根本不会发出

⚠️ 注意第一行:简单请求场景下,即使浏览器拦截了响应,后端已经收到了 Cookie 并执行了逻辑。CORS 限制的是”读”而非”写”——这是 CSRF 攻击的温床(详见§五)。

不带凭证时 * 能做什么

Access-Control-Allow-Origin: * 并非一无是处,它适用于公开 API、无需身份认证的数据接口

典型场景:微信小程序 H5 页面调用公开接口、CDN 字体文件跨域加载、开放数据平台的 Public API。

配置选择决策

你的跨域接口需要带 Cookie / 身份凭证吗?
├── 不需要 → Access-Control-Allow-Origin: * 即可(简单省事)
└── 需要   → 必须用白名单方案(见§三场景 B 的 Nginx map 模板)

浏览器:SameSite 终极审判

术语辨析:SameSite 策略基于 Site(eTLD+1,即注册域名),而非 Origin(协议+域名+端口)。例如 https://a.example.comhttps://b.example.com跨 Origin 但不跨 Site,SameSite 策略下不受限制。本文以下”跨站”均指跨 Site。

现代浏览器(Chrome 84+ 全面执行,91+ 移除回退开关)强制执行 SameSite 策略:

五、从 CORS 走向真正的 Web 安全:防御 CSRF 攻击

⚠️ 核心误区纠正CORS 绝对防不住 CSRF 攻击! 误以为”让接口接收 application/json 触发预检,预检不通过就能防住 CSRF”是极其危险的错误认知。

真正危险在于:CSRF 攻击使用的是简单请求(如传统 <form> 表单 POST),简单请求不触发预检,请求直接到达后端。攻击者用 <form> 无法发送 application/json(HTML form 的 enctype 不支持),只能发 application/x-www-form-urlencodedmultipart/form-data

后端是否真的会被攻击,取决于接口写法:

接口写法form POST 能否触发业务?原因
@RequestBody UserDto❌ 415 拒绝MappingJackson2HttpMessageConverter 只接受 application/json,form 数据无匹配 Converter
@RequestBody MultiValueMap✅ 可以FormHttpMessageConverter 专门处理 application/x-www-form-urlencoded
UserDto(不加 @RequestBody✅ 可以Spring MVC 数据绑定从 request parameters 填充对象,无论 Content-Type
@RequestParam✅ 可以直接读取 query string 或 form 字段

结论@RequestBody + JSON POJO 是天然 CSRF 防护(form 发不了 JSON,Spring 不接受 form 格式)。但不带 @RequestBody 的参数绑定才是真正的攻击面。即便如此,请求仍然到达了后端(只是碰巧返回 415 没执行业务),CORS 保护的是”数据的读”,CSRF 破坏的是”数据的写”——防御应该靠 CSRF Token,而非依赖 Content-Type 校验。

现代企业级 CSRF 防御体系

  1. 利用现代 Cookie 的 SameSite 属性(首选底线防御)

    对于核心业务 Cookie(如 SESSIONIDToken),在同源或同站业务下,确保将其属性设为 SameSite=LaxSameSite=Strict。这样,用户在第三方恶意网站点击链接或触发跨域 Ajax 时,浏览器绝不会自动带上该 Cookie。

  2. 签名双提交 Cookie / 自定义 Header 校验(推荐方案)

    要求前端在所有发往后端的非简单请求(写操作)中,除了依靠浏览器自动带 Cookie 外,还必须在 Request Header 里手动带上一个随机生成的 X-CSRF-Token(可由后端在登录时下发,并使用 HMAC 签名绑定用户会话)。

    • 原理:恶意脚本可以利用浏览器机制”自动带上 Cookie”,但受同源策略限制,攻击者在无 XSS 漏洞且域名安全的前提下无法用代码读取跨域 Cookie 并写入自定义 Header。后端网关层比对 Header 中的 Token 与 Cookie 中的 Token 是否一致(签名验证),不一致则直接拒绝请求。
    • ⚠️ 必须使用签名版:朴素版(Token 明文存 Cookie + Header)存在已知弱点——攻击者可通过子域名漏洞、DNS 接管或 HTTP Cookie 注入伪造匹配的 Cookie。签名版使用 HMAC 将 Token 与用户会话绑定,可有效防御这些绕过手段。

六、附录:跨域排障 SOP

遇到跨域报错,严禁盲目猜测修改代码,必须按下述步骤查阅浏览器 F12 的 Network 面板

1. 看颜色和状态码

2. 检查 OPTIONS 预检请求

3. 排查关键响应头(Response Headers)

更新记录


分享这篇文章:
通过邮件分享这篇文章✓ 链接已复制
查看系列全部文章
  1. 01.T3 Stack 开发规范
  2. 02.Next.js 并行路由完全指南
  3. 03.Web 会话与身份验证完整指南
  4. 04.Auth Guard 与 tRPC 中间件
  5. 05.T3 Stack 会话认证指南
  6. 06.Next.js 子域名、反向代理与分享链路最佳实践
  7. 07.CORS 与 Web 安全实战白皮书
  8. 08.Web 多主题架构最佳实践
  9. 09.Vercel AI SDK 自定义 OpenAI 模型接入
  10. 10.AI 驱动动态看板架构指南
  11. 11.VitePress 动态渲染全栈落地方案

上一篇
Web 多主题架构最佳实践
下一篇
Next.js 子域名、反向代理与分享链路最佳实践