CORS跨域配置避坑指南:从原理到白名单,别再用通配符*埋雷

发布时间:2026/9/30 5:34:34
CORS跨域配置避坑指南:从原理到白名单,别再用通配符*埋雷 如果你是一名前端或者全栈工程师看到 “has been blocked by cors policy: no access-control-allow-origin header is present on the requested resource” 这行报错大概率已经和浏览器纠缠半天了。很多人第一反应是搜索“CORS 怎么解决”然后顺手在后端加上一个Access-Control-Allow-Origin: *发现页面不报错了就以为自己搞定了。我劝你千万别这么干这个*号看着省事实际是在给线上项目埋雷。这篇博文我会从跨域原理讲起把Access-Control-Allow-Origin的正确配置方式、使用场景、白名单思路、配套 Cookie 处理、Nginx 网关方案全部过一遍。不是讲教科书概念而是结合我真实调试过的项目经验告诉你什么时候该用*、什么时候绝对不能碰以及报错信息到底在说什么。如果你是刚被 CORS 折磨过的新手建议从头看如果是老手可以直接跳到第 3 章和第 5 章的速查表。1. CORS问题不是玄学先搞懂它到底在做什么1.1 同源策略浏览器为什么“多管闲事”CORS 全称是 Cross-Origin Resource Sharing中文叫跨域资源共享。但要说清楚它得先从同源策略Same-Origin Policy讲起。浏览器有个安全机制默认情况下一个网页里的 JavaScript 只能读取“同源”的数据。同源的意思是协议https/http、域名example.com、端口8080/3000三者完全一致。比如你的前端跑在http://localhost:8080后端接口跑在http://localhost:8081这俩端口不同就是跨域浏览器会拦截响应数据。为什么浏览器要“多管闲事”因为如果没有这个限制你登录了银行网站再去打开一个恶意网站恶意网站的脚本就能偷偷向银行接口发请求然后读取你的账户信息。同源策略的本质是给“谁可以读我的数据”画了一条边界线。但实际开发中前后端分离早就成了主流前端部署在 CDN 或 Nginx后端是一组独立的 API 服务。两边域名天然不同你不可能不让它们通信。CORS 就是浏览器开放的一条“合法跨域通道”——服务端通过响应头告诉浏览器“这个外部来源可以访问我”浏览器校验通过后才把数据交给你页面的 JavaScript。1.2 跨域请求的两个阶段简单请求与预检请求CORS 请求分为两种。第一种是“简单请求”条件比较苛刻方法只能是GET、POST、HEADContent-Type 只允许application/x-www-form-urlencoded、multipart/form-data、text/plain而且不能带自定义头。简单请求会直接发出浏览器在拿到响应后检查响应头里有没有Access-Control-Allow-Origin没有就抛错。第二种是“预检请求”Preflight。只要请求带了Authorization、Content-Type: application/json这种自定义头或者用了PUT、DELETE、PATCH方法浏览器就会先发一个OPTIONS请求去“探路”问服务端我要用POSTapplication/json你允不允许服务端得通过Access-Control-Allow-Methods、Access-Control-Allow-Headers告诉它允许哪些方法和头浏览器再决定要不要发真正的请求。这个设计听起来严谨实际调试时特别容易出问题。我见过很多人只设置了Access-Control-Allow-Origin忘了处理OPTIONS请求结果接口明明在线前端却一堆预检报错。所以第 4 章我会专门写一个完整的OPTIONS处理示例。1.3 谁来配置Access-Control-Allow-Origin一定是服务端不是前端很多新手问我前端能不能在 request 里加Access-Control-Allow-Origin头把问题“堵回去”答案是绝对不行。这个头属于响应头只能由服务端在返回数据时携带。前端能做的只是确认Origin头是否正确发送以及在跨域模式下用fetch或XMLHttpRequest时把credentials设置成合适的值。记住一个原则跨域控制权永远在服务端也就是说这个场地的门禁卡由后端来发。你前端再怎么改请求配置也过不了浏览器这一关。2. 设置成*号一时爽后面全是坑——通配符方案的三大致命问题2.1 安全边界失效等于给所有网站开了数据后门Access-Control-Allow-Origin: *的含义是任意来源的网页都能读取这个接口的响应。对纯粹的公开信息接口——比如天气数据、公开新闻列表——这没什么问题。但凡接口涉及登录态、用户资料、订单信息、内部数据这就是灾难。假设你有一个接口https://api.example.com/user/info返回当前登录用户个人资料响应头设置成了*。那就意味着我在自己的恶意网站上部署一个页面用你浏览器残留的 Cookie 去请求这个接口浏览器会想服务端已经允许所有来源了行吧数据放大。你的姓名、手机号、收货地址就被我这个恶意页面读走了。这不是理论推断而是实际可以被构造的攻击场景叫 CORS-based attack。所以现在很多大厂的安全规范里直接写死“内部接口不允许配置Access-Control-Allow-Origin: *”。你可以把*理解为“不设防”只适合放公开数据不适合放任何需要凭证的接口。2.2 与携带凭证Cookie的组合直接冲突这可能是*最坑的一点。如果你需要在跨域请求里带上 Cookie最常见的场景是跨域单点登录、带 session id 的接口那么服务端不能把Access-Control-Allow-Origin设成*同时Access-Control-Allow-Credentials必须设为true。浏览器规范规定当请求模式是credentials: include即携带 Cookie时响应里的Access-Control-Allow-Origin必须是具体的源不能是*。如果你把*和Allow-Credentials: true一起配置浏览器直接报错The value of the Access-Control-Allow-Origin header in the response must not be the wildcard * when the requests credentials mode is include.翻译成人话你带凭证了我还知道你是谁呢*等于告诉浏览器“我谁都不认识”但你又要求带 Cookie规范不允许这种自相矛盾。所以你要么不带 Cookie要么就把具体域名写出来。我见过一个实际案例同事开发时把*配上了调试登录功能时一直报错最后发现是因为浏览器 Set-Cookie 跨域失效。Node 和 Java 项目里这种问题非常多根源都是用了通配符。2.3 遇到自定义头、特定HTTP方法时照样报错除了安全与凭证问题*还有一层尴尬有些场景下即使你设置了Access-Control-Allow-Origin: *请求依然报错。比如前端要带一个自定义头X-Trace-ID用于全链路追踪或者用Content-Type: application/json发PUT请求。这时浏览器会先发OPTIONS预检服务端必须在Access-Control-Allow-Headers里明确列出X-Trace-ID在Access-Control-Allow-Methods里明确列出PUT。如果服务端只配了Allow-Origin预检照样弹红。也就是说通配符*只能解决“最简单的 GET/POST 请求”的一部分问题一旦业务稍微复杂一点它根本兜不住。正确做法是完整配置 Allow-Origin、Allow-Methods、Allow-Headers 三件套缺一个都可能踩坑。3. 正确配置的三种落地方式按场景选型3.1 静态白名单适合前端域名固定的常规项目如果你的前端域名是固定的最稳妥、最优雅的方案就是静态白名单直接列出允许的来源。以 Node.js 的 Express 为例最简单的手写版本是这样的const allowedOrigins [https://admin.example.com, https://www.example.com]; app.use((req, res, next) { const origin req.headers.origin; if (allowedOrigins.includes(origin)) { res.setHeader(Access-Control-Allow-Origin, origin); res.setHeader(Access-Control-Allow-Credentials, true); res.setHeader(Access-Control-Allow-Methods, GET, POST, PUT, DELETE, OPTIONS); res.setHeader(Access-Control-Allow-Headers, Content-Type, Authorization); } if (req.method OPTIONS) { return res.sendStatus(204); } next(); });这段代码的关键逻辑是先判断请求的Origin是否在白名单里在的话才把这个具体的 Origin 原样回写。这样浏览器看到的是Access-Control-Allow-Origin: https://admin.example.com而不是*凭证模式也能正常工作。如果你用的是 NestJS 或 Express 的cors中间件配置更简单const cors require(cors); const app express(); app.use(cors({ origin: [https://admin.example.com, https://www.example.com], credentials: true, methods: [GET, POST, PUT, DELETE, OPTIONS], allowedHeaders: [Content-Type, Authorization], }));Spring Boot 项目也一样可以写一个全局的WebMvcConfigurerConfiguration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/api/**) .allowedOrigins(https://admin.example.com, https://www.example.com) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(Content-Type, Authorization) .allowCredentials(true); } }注意Spring Boot 里.allowedOrigins(*)和.allowCredentials(true)同时出现会启动报错Spring 从 5.3 开始已经对这个做了严格限制。这正是*不可行的又一个佐证。3.2 动态校验Origin适合多域名、子域名场景的通用做法有些项目不止一两个前端域名而是有一整套子域名体系比如user.example.com、shop.example.com、m.example.com。手写白名单数组也行但更好的是写一个动态校验函数用正则或后缀匹配判断Origin是否属于可信域名。我在一个多租户项目里用的方案大致是这样const trustedDomains [.example.com, .example.org]; function isTrustedOrigin(origin) { if (!origin) return false; try { const url new URL(origin); return trustedDomains.some((domain) { return url.hostname domain.slice(1) || url.hostname.endsWith(domain); }); } catch (e) { return false; } } app.use((req, res, next) { const origin req.headers.origin; if (isTrustedOrigin(origin)) { res.setHeader(Access-Control-Allow-Origin, origin); res.setHeader(Access-Control-Allow-Credentials, true); res.setHeader(Vary, Origin); } if (req.method OPTIONS) { return res.sendStatus(204); } next(); });这里我加了一行res.setHeader(Vary, Origin)很多人会忽略。它的作用是告诉缓存系统“响应的内容会因为 Origin 不同而不同”避免 CDN 或浏览器把 A 域名拿到的响应缓存住然后返回给 B 域名造成数据串台的诡异 bug。当你动态返回不同Access-Control-Allow-Origin时Vary: Origin是必须加的。3.3 在Nginx网关统一处理适合前后端分离的部署场景很多项目前端是静态资源部署在 Nginx后端 API 喝另一组地址。这种情况下与其在每个后端服务里各写一遍 CORS 配置不如在 Nginx 反向代理层统一处理逻辑更集中切换域名时只需改网关配置。一个标准配置片段长这样server { listen 443 ssl; server_name api.example.com; location /api/ { set $cors_origin ; if ($http_origin https://admin.example.com) { set $cors_origin $http_origin; } if ($http_origin https://www.example.com) { set $cors_origin $http_origin; } add_header Access-Control-Allow-Origin $cors_origin always; add_header Access-Control-Allow-Credentials true always; add_header Access-Control-Allow-Methods GET, POST, PUT, DELETE, OPTIONS always; add_header Access-Control-Allow-Headers Content-Type, Authorization always; add_header Vary Origin always; if ($request_method OPTIONS) { return 204; } proxy_pass http://backend_server; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }注意always参数。Nginx 默认只在响应码为 200、201、204 等情况下才添加add_header加上always后即使后端返回 302、400、500 也会带上 CORS 头。否则前端请求出错时浏览器看到的还是“No Access-Control-Allow-Origin header”你排查半天也不知道问题在响应头还是业务逻辑。这套方案特别适合一个 API 网关对应多个前端。前端域名有变化时改下 Nginx 配置 reload 一下就生效不需要重新部署后端。4. 从报错到修复一次完整的CORS排查实操4.1 读取报错信息先把浏览器给的信息翻译成人话CORS 报错文字虽然长但信息量其实很高。常见的几种CORS policy: No Access-Control-Allow-Origin header is present最常见。说明响应里根本没配置这个头或者配置被浏览器判定无效。CORS policy: The Access-Control-Allow-Origin header contains multiple values *, *说明你前后端各配了一遍或者 Nginx 和后端都加了 CORS 头响应头重复了。CORS policy: The Access-Control-Allow-Origin header must not be the wildcard * when the requests credentials mode is include你用了通配符又要求带凭证。CORS policy: Request header field authorization is not allowed by Access-Control-Allow-Headers in preflight response预检通过但带Authorization头时被拒绝说明服务端没在Access-Control-Allow-Headers里放行。拿到报错后第一步打开 DevTools 的 Network 面板找到失败的请求先看请求头里的Origin是什么再看响应头里有没有Access-Control-Allow-Origin。不要直接改代码先确认到底是哪一个头缺失方向错了后面都是在白忙。4.2 后端修复前后对比三个框架的配置示例我用一个实际场景把整套配置流程串起来。假设前端部署在https://myapp.example.com后端 API 部署在https://api.example.com前端需要带 Cookie 和Authorization头主要方法是GET、POST、PUT。前端代码里fetch 请求要这样写const response await fetch(https://api.example.com/api/user/profile, { method: GET, credentials: include, // 必须带否则 Cookie 不会发送 headers: { Content-Type: application/json, Authorization: Bearer ${token}, }, });后端以 Express 为例修复后应该包含完整的三件套。我推荐直接用cors中间件少写手写代码也不容易漏头const corsOptions { origin: function (origin, callback) { const allowedList [https://myapp.example.com]; if (!origin || allowedList.indexOf(origin) ! -1) { callback(null, true); } else { callback(new Error(Not allowed by CORS)); } }, credentials: true, methods: [GET, POST, PUT, DELETE, OPTIONS], allowedHeaders: [Content-Type, Authorization], maxAge: 3600, }; app.use(cors(corsOptions));注意这里maxAge: 3600意思是预检请求的响应结果可以缓存一小时。如果预检配置没问题一小时内浏览器不会重复发送OPTIONS请求能省不少网络开销也降低服务端压力。如果是 Django 后端配置逻辑一样但用django-cors-headers插件会更省事INSTALLED_APPS [ ... corsheaders, ] MIDDLEWARE [ corsheaders.middleware.CorsMiddleware, ... ] CORS_ALLOWED_ORIGINS [ https://myapp.example.com, ] CORS_ALLOW_CREDENTIALS True CORS_ALLOW_METHODS [ DELETE, GET, OPTIONS, POST, PUT, ] CORS_ALLOW_HEADERS [ content-type, authorization, ]修复完成后重启服务回到浏览器 DevTools清一下缓存再触发请求。Network 面板里如果能看到Access-Control-Allow-Origin: https://myapp.example.com和Access-Control-Allow-Credentials: true这个跨域链路就通了。4.3 用curl和浏览器验证Access-Control-Allow-Origin是否生效有时候后端修完了浏览器还是报错可能是因为浏览器缓存了旧的响应也可能 CDN 层缓存了响应头。这时候用 curl 验证最直接绕过浏览器缓存curl -i -X OPTIONS https://api.example.com/api/user/profile \ -H Origin: https://myapp.example.com \ -H Access-Control-Request-Method: POST \ -H Access-Control-Request-Headers: authorization,content-type \ -H Access-Control-Request-Private-Network: true注意我在请求里加了Origin和预检相关的头是模拟浏览器发出的 preflight。观察响应里有没有Access-Control-Allow-Origin: https://myapp.example.com Access-Control-Allow-Credentials: true Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS Access-Control-Allow-Headers: content-type, authorization只要这些头都在说明服务端配置没问题。再到浏览器里硬刷新一次一般就正常了。如果 curl 头都对了但浏览器还是报错那问题八成是缓存或代理别把服务端代码翻来覆去改。5. 常见问题与排查技巧实录5.1 CORS高频报错速查表我把日常遇到最多的报错整理成了一张表开发时可以直接对照排查。报错关键词可能原因解决办法No Access-Control-Allow-Origin header服务端没配置 CORS 头或配置被拦截检查后端和 Nginx 是否真正上了响应头multiple values *, *后端与 Nginx 各配了一次 CORS只保留一处配置去掉重复 add_headermust not be wildcard * with credentials mode include同时用了*和 credentials改用具体白名单并设置 credentialstrueauthorization is not allowed by Access-Control-Allow-Headers预检时没放行 Authorization 头在 Allow-Headers 加入 authorizationmethod PATCH not allowed by Access-Control-Allow-Methods预检时没放行对应方法在 Allow-Methods 加入所有需要的 HTTP 方法response to preflight request doesnt pass access control checkOPTIONS 请求未正确处理或缺少头在服务端统一响应 OPTIONS返回 204 和 CORS 头5.2 我自己踩过的几个坑第一个坑开发环境配好了线上环境突然全部 CORS 报错。排查了半天发现是 CDN 环节把响应头过滤了。国内有些云 CDN 为了“安全”默认会剥离Access-Control-Allow-Origin头。解决方法是到 CDN 控制台配置“自定义响应头”把 CORS 头在 CDN 层重新加上或者把 CDN 的“过滤参数”关掉。第二个坑项目里用了 Spring SecurityCORS 配置写在WebMvcConfigurer里但请求被拦在 Security 过滤链里跨域配置根本没执行。这是 Spring Boot 项目的典型问题解决方式是实现CorsFilter并注册到 Security 的过滤器链前面。你可以在SecurityFilterChain里调用http.cors()启用 CorsFilter而不是只写 MVC 配置。第三个坑预检OPTIONS请求本来不需要登录态但我的后端拦截器把所有请求都要求校验 Token结果 OPTIONS 直接返回 401浏览器连预检都过不了。这个问题很隐蔽因为前端看到的报错还是“No Access-Control-Allow-Origin header”实际是 OPTIONS 根本没走到 CORS 头那一步。所以现在我在所有项目里都会对OPTIONS请求提前放行并且确认响应头已经写入再进业务拦截器。第四个坑跨域 Cookie 的SameSite属性。即使你把Access-Control-Allow-Origin和Access-Control-Allow-Credentials都配对了Cookie 也可能发不出去因为浏览器要求跨域请求下的 Cookie 必须设置SameSiteNone; Secure。如果服务端返回的 Set-Cookie 里没有这两个属性Cookie 会被浏览器默默忽略。那次排查让我意识到CORS 问题往往不是单一原因而是一条链路任何一环掉链子都会表现为“跨域失败”。最后再分享一个调试技巧强烈建议在服务端代码里记录 CORS 相关的拦截日志把每个请求的Origin、Method、匹配到的Allow-Origin打成一行日志。线上环境问题定位时这个日志能帮你区分“浏览器压根没发请求”还是“服务端没返回正确头”少走很多弯路。根据我个人的项目经验CORS 配置错误里大概有一半可以通过日志排查法在五分钟内定位根本不需要抓包。