浏览器控制台里那一行刺眼的红字,做Web开发的人迟早都会撞上一次:
“Access to XMLHttpRequest at ‘http://api.example.com/data’ from origin ‘http://localhost:8080’ has been blocked by CORS policy…”
新手第一反应是怀疑后端接口挂了,后端同事看了一眼说接口没问题,最后排查半天发现是“跨域”。而跨域这件事,底层就是今天要聊的浏览器同源策略。它是Web安全体系里最基础、也最容易被忽略的一道防线,更是前后端联调、面试笔试、CTF比赛里绕不开的高频话题。
这篇文章我会把同源策略从头到尾拆开讲:它到底判断的是什么东西、限制了哪些行为、为什么非得限制、实际开发里有哪些合规的跨域手段,以及踩坑之后的调试套路。适合刚接触Web安全的前端/后端开发、准备安全方向面试的同学,也适合所有被CORS报错折磨过的人。
1. 同源策略到底是什么
先别急着背概念,我们从一次真实请求开始推演。
假设你现在打开了一个页面,地址是https://www.example.com/login。这个页面里的JavaScript发起了一个请求要访问https://www.example.com/api/userinfo,浏览器检查了一下,页面地址和请求地址的协议、域名、端口都一样,于是放行。但如果这个请求指向的是https://api.another.com/data,协议一样、域名不一样,浏览器就会启动拦截机制,把响应挡在JavaScript的读取范围之外。
这个“检查三个关键要素是否一致”的机制,就是同源策略(Same-Origin Policy,简称SOP)。
1.1 同源的精确判定规则
同源策略里的“源”,不是指一个完整URL,而是URL最前面的三元组:
- 协议(Protocol):http、https、file、chrome-extension等
- 主机(Host):域名或IP地址
- 端口(Port):http默认80,https默认443
只有当两个URL的协议、主机、端口完全一致时,才被认为是同源。我在带新人的时候经常让他们把下面这个表格抄一遍:
| 页面URL | 请求URL | 是否同源 | 原因 |
|---|---|---|---|
https://www.example.com/page | https://www.example.com/other | 同源 | 协议、域名、端口全一致 |
http://www.example.com/page | https://www.example.com/other | 不同源 | 协议不同(http vs https) |
https://www.example.com/page | https://api.example.com/other | 不同源 | 子域名不同 |
https://www.example.com:8080/page | https://www.example.com/other | 不同源 | 端口不同 |
https://www.example.com/page | https://www.example.com.cn/other | 不同源 | 域名完全不同 |
我自己早期犯过一个低级错误:把https://www.example.com和https://example.com当成同一个源。实际上带不带www,对浏览器来说就是完全不同的两个源,哪怕它们解析出来的IP一模一样。
1.2 几个容易踩的判定坑
默认端口带来的错觉
https://www.example.com和https://www.example.com:443是同源的,因为443是HTTPS的默认端口,浏览器会自动补全。同理http://www.example.com和http://www.example.com:80也是同源的。
但如果你写成了https://www.example.com:8443,即使这个服务确实是同一个应用,浏览器也会判定为跨域,因为端口不一致。
IP和域名是两回事
localhost、127.0.0.1、本机局域网IP(比如192.168.1.100)指向的可能是同一台机器,但在同源判定里它们是不同的host,因此是不同源。开发环境里最常见的跨域场景就是:前端跑在localhost:8080,后端跑在127.0.0.1:8081,看起来都在本机,但一个端口不同、一个主机表述不同,妥妥的跨域。
协议不同是硬伤
HTTP和HTTPS之间天然不同源。在HTTPS页面里要请求HTTP接口,除了跨域之外还会遇到混合内容(Mixed Content)限制,浏览器干脆直接阻止。反过来,HTTP页面里请求HTTPS接口虽然不会被混合内容规则拦,但依然受同源策略约束。
注意:同源判定只针对“浏览器环境”。在Node.js、Python、Go这类服务端环境里,并没有同源策略这个概念,服务端之间的请求不受SOP约束。这也是为什么跨域问题主要出现在浏览器前端联调阶段。
1.3 同源策略是一种“浏览器行为”
这里要强调一个容易被误解的点:同源策略是浏览器实现的安全策略,不是HTTP协议的一部分,也不是服务器必须执行的规则。
服务器并不知道请求是不是跨域的,它照样会处理并返回数据。拦截行为发生在浏览器这一侧——响应到达浏览器以后,浏览器检查发现跨域且没有合法的跨域头,就把响应“扣下来”,不让页面的JavaScript读取。所以你在开发者工具Network面板里,经常能看到请求状态码是200,但控制台照样报CORS错误,这就是“请求发出去了,响应也回来了,但浏览器不让读”的典型表现。
理解这一点很重要,后面讲CORS跨域方案时,你会深刻体会到什么叫“服务端配合,浏览器放行”。
2. 同源策略具体限制了哪些行为
很多初学者以为同源策略只是限制网络请求,这个理解太窄了。同源策略在浏览器里是一整套隔离机制,主要管三件事:DOM访问、Cookie和Storage隔离、网络请求读取。
2.1 DOM访问限制
一个页面里可以嵌入另一个页面,比如用iframe。如果没有同源策略,恶意页面就可以通过JavaScript读取被嵌入页面的DOM结构、修改表单数据、监听点击事件,甚至窃取用户在里面的输入内容。
比如我在自己的博客页面上用iframe嵌入了邮箱页面,如果两者不同源,我是无法拿到邮箱页面内部任何DOM元素的。强行访问的话,控制台会报错:
Uncaught DOMException: Blocked a frame from accessing a cross-origin frame.这条限制保护的是页面“内部结构”的不可见性。不同源的两个页面之间,只有Window对象上一些安全的属性(比如location的只读部分、window.length等)可以访问,其他一律禁止。
2.2 Cookie和Storage隔离
同源策略还管着浏览器本地存储的读写边界。Cookie在设置时虽然可以指定Domain属性来实现跨子域共享,但默认情况下,www.example.com写入的Cookie,api.example.com是拿不到的。localStorage和sessionStorage更是严格按源来隔离的,连子域之间都不能互相读,因为Storage规范里没有像Cookie那样提供共享开关。
这意味着什么?你登录了A网站,浏览器里存了A的会话Cookie,然后你又打开了恶意网站B。B的脚本想读取你存在A.com下的Cookie,浏览器不会给它这个权限。这就是Cookie隔离能阻止“直接偷登录态”的原因。
不过要注意:Cookie的隔离是“脚本读取层面”的隔离,不是“请求发送层面”的隔离。这句话是后面理解CSRF攻击的钥匙。
2.3 网络请求限制
网络请求这块最容易被混淆,严格来说同源策略限制的是“读取跨域响应”,而不是“发起跨域请求”。
页面上发一个跨域请求,能不能发出去?分情况:
<script src>、<img src>、<link href>、<form action>、<iframe src>这类标签形式的请求,可以正常发起,响应也能被浏览器接收,只不过接收方是标签自个儿,脚本不直接能读。这是历史遗留设计,早期Web就是靠这种方式引用外部资源。fetch、XMLHttpRequest(XHR)这类由JavaScript发起的网络请求,跨域时能否成功“读取”响应,取决于目标服务有没有返回正确的CORS响应头。注意是“读取”受限,不是“发送”受限。
打个比方:你家大门装了一道门禁,快递员(请求)可以走到门口(发送),门卫(浏览器)验明身份后,要么把快递交给你(放行读取),要么拒收退回(拦截响应)。在CORS配置缺失时,快递已经到了,但门卫直接跟你说“这快递不能收”。
2.4 同源策略不管什么
说完了限制,再说说不受同源策略管的部分,这部分是后面做跨域方案和攻击手法的“漏洞面”:
- 标签加载类请求不受SOP约束:
script、img、link、iframe、video、audio、form提交等。 - 重定向不算跨域拦截:浏览器地址栏里跳转到任意站点都是允许的。
window.open打开新页面、location.href跳转也不受SOP限制。
就是这些“不管”,衍生了JSONP跨域方案,也衍生了CSRF攻击、JSONP劫持这类Web安全漏洞。
3. 为什么要限制:跨域开放会出什么事
很多人觉得同源策略烦人,处处挡路。但你得先搞清楚:如果没有同源策略,整个Web的信任模型会瞬间崩塌。
3.1 没有SOP的世界会是怎样
做一个思想实验。假设你在网上银行页面里登录了账号,浏览器存了银行的会话Cookie,然后你又在另一个标签页打开了攻击者的网站。如果没有同源策略,攻击者网站里的恶意脚本就可以干两件大事:
第一,直接发一个跨域请求到你银行的转账接口,浏览器自动携带你登录银行站点时的Cookie,银行服务器一看请求里有合法会话,就执行转账操作。第二,恶意脚本还可以发起一个跨域请求读取你的银行余额和交易记录,因为同源限制没了,响应内容全部暴露。
第一种攻击形态就是CSRF(跨站请求伪造),第二种更狠,直接把数据偷走了。而有了同源策略之后,第二种被直接拦截,第一种虽然请求能发出,但恶意脚本也看不到响应结果,攻击者没法拿到有效数据,危害被大大降低。
3.2 CSRF和SOP的关系
这里有个经典的细节问题:既然SOP拦了跨域请求的“读”,为什么CSRF仍然存在?
原因是现代Web应用的很多“写操作”接口,恰恰允许通过跨域方式提交。比如用户修改个人信息、发帖、转账这类操作,往往只要请求能到服务器、Cookie能自动带上,服务器就可能执行。SOP只拦截了“跨域读取响应”,并没有拦截“跨域提交请求”。
所以CSRF防御思路从来不是靠SOP,而是靠服务端校验一个攻击者无法读取的秘密——比如CSRF Token、SameSite Cookie属性、校验Origin/Referer头。这也是我在面试中特别喜欢问的一个点:同源策略能防CSRF吗?答案是不能完全防,它只让攻击者“看不到结果”,但攻击可能已经发生了。
3.3 合法业务为什么需要跨域
既然跨域这么危险,为什么不做死?因为互联网业务形态决定了跨域是刚需:
- 前后端分离架构:前端页面部署在CDN或Web服务器上,后端API独立部署,两者域名天然不同。
- 第三方开放平台:比如小程序、开放平台上的第三方应用,需要调用平台API。
- 多子域体系:当一个公司在
example.com下运行了login.example.com、mail.example.com、api.example.com等多个子系统时,系统之间可能需要共享登录态或互调接口。
同源策略的本质是“默认拒绝、按需放行”,而不是“一刀切禁止”。既然业务有跨域需求,浏览器和Web标准就设计出了一套可控的放行机制——这就是CORS。
4. 实际开发中如何合法地跨域
跨域方案我列一个总览表,你写代码前先对照着选型:
| 方案 | 适用场景 | 原理 | 安全性 |
|---|---|---|---|
| JSONP | 只支持GET请求的老旧接口 | 借助script标签不受SOP限制 | 较低,可能被劫持 |
| CORS | 现代API的标准方案 | 服务端通过响应头声明放行规则 | 较高的灵活性和可控性 |
| postMessage | iframe或窗口之间的跨域通信 | 通过事件机制传递消息 | 需自己校验消息来源 |
| document.domain | 同一主域下的子域互访 | 降低域到相同主域 | 只适用于子域场景 |
| 代理转发 | 前端开发环境或生产隐藏API | 请求发到同源地址,由服务端转发 | 较好,且不影响原有架构 |
4.1 JSONP方案
JSONP的诞生比CORS早很多,当时浏览器还没有CORS标准,前端为了解决跨域想出的野路子。
它的核心思路是:<script>标签加载外部JS文件不受同源限制,那我干脆用<script>标签的src去请求接口,把数据塞在一个回调函数里返回。前端提前定义一个全局函数,脚本加载后自动执行这个函数,数据就“传”过来了。
前端代码像这样:
<script> function handleData(data) { console.log('获得的用户信息:', data); } </script> <script src="https://api.example.com/userinfo?callback=handleData"></script>后端收到请求后,不返回普通JSON,而是返回一段可执行的JavaScript:
handleData({"name": "admin", "id": 1001});浏览器加载这段脚本后,自动执行handleData函数,参数就是服务端拼好的数据对象。前端就这样绕过了同源限制。
JSONP的痛点也很明显,我在项目里基本不用它做新功能:
- 只支持GET请求,没法用POST、PUT、DELETE。
- 用全局函数容易造成命名冲突和XSS注入风险。
- 没有标准的错误处理机制,请求失败了很难排查。
- 现在CORS已经覆盖所有主流浏览器,JSONP只剩兼容老系统的价值。
4.2 CORS方案(重点)
CORS全称是Cross-Origin Resource Sharing,跨源资源共享。它是W3C标准,也是目前跨域方案的主流。
CORS的设计思路和JSONP完全不同:不是绕过SOP,而是在SOP基础上建立一套“白名单机制”。服务端在HTTP响应头里告诉浏览器:我这个资源允许哪些来源访问、允许哪些方法、允许哪些请求头。
核心响应头就这几个:
Access-Control-Allow-Origin: https://www.example.com Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS Access-Control-Allow-Headers: Content-Type, Authorization Access-Control-Allow-Credentials: true Access-Control-Max-Age: 600Access-Control-Allow-Origin:最重要,指定允许的来源。可以是具体域名,也可以是通配符*(表示所有来源)。Access-Control-Allow-Methods:允许的HTTP方法。Access-Control-Allow-Headers:允许的请求头(预检请求时需要)。Access-Control-Allow-Credentials:是否允许携带Cookie。Access-Control-Max-Age:预检请求结果可以缓存的时长,单位秒。
简单请求和预检请求
CORS把跨域请求分成两类:简单请求(Simple Request)和预检请求(Preflighted Request)。
满足以下所有条件才算简单请求:
- 请求方法只能是GET、HEAD、POST。
- 请求头只能包含常规字段(如Accept、Content-Type、Accept-Language)以及部分安全字段。
- Content-Type只能是
application/x-www-form-urlencoded、multipart/form-data、text/plain这三种之一。
如果你的请求用了application/json作为Content-Type,或者带了Authorization这种自定义头,或者用了PUT/DELETE方法,浏览器会先发一个OPTIONS预检请求去问服务器“你这个接口允许我这样请求吗”,服务器返回明确的允许规则后,浏览器才发真正的业务请求。
这也是很多人踩坑的地方:后端只处理了业务接口,没处理OPTIONS请求,导致浏览器预检阶段就失败,业务请求根本没发出去。
一个典型的预检请求长这样:
OPTIONS /api/userinfo HTTP/1.1 Host: api.example.com Origin: https://www.example.com Access-Control-Request-Method: PUT Access-Control-Request-Headers: Content-Type服务器要针对OPTIONS请求返回正确的响应头:
HTTP/1.1 204 No Content Access-Control-Allow-Origin: https://www.example.com Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS Access-Control-Allow-Headers: Content-Type, Authorization Access-Control-Max-Age: 600服务端到底该怎么配
以Java的Spring Boot为例,可以写一个CORS过滤器统一处理:
@Component public class CorsFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletResponse resp = (HttpServletResponse) response; resp.setHeader("Access-Control-Allow-Origin", "https://www.example.com"); resp.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS"); resp.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization"); resp.setHeader("Access-Control-Allow-Credentials", "true"); if ("OPTIONS".equalsIgnoreCase(((HttpServletRequest) request).getMethod())) { resp.setStatus(HttpServletResponse.SC_NO_CONTENT); return; } chain.doFilter(request, response); } }用Nginx反代时,也可以直接在代理层配置CORS头:
location /api/ { add_header Access-Control-Allow-Origin https://www.example.com; add_header Access-Control-Allow-Methods 'GET, POST, PUT, DELETE, OPTIONS'; add_header Access-Control-Allow-Headers 'Content-Type, Authorization'; add_header Access-Control-Allow-Credentials true; if ($request_method = 'OPTIONS') { return 204; } proxy_pass http://backend_server; }4.3 带Cookie的CORS配置
跨域请求要带Cookie时,坑最多。前端要设置credentials,后端也要打开对应开关。
前端fetch:
fetch('https://api.example.com/userinfo', { method: 'GET', credentials: 'include' });注意,credentials: 'include'是跨域携带Cookie的开关,如果只写credentials: 'same-origin',跨域请求是不带Cookie的。
服务端响应头必须同时满足两个条件:
Access-Control-Allow-Origin: https://www.example.com Access-Control-Allow-Credentials: true而且此时Access-Control-Allow-Origin不能使用通配符*,必须指定具体的来源域名。如果服务端返回的是*,浏览器会直接拒绝这次请求,报错信息里一般会写:“The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'。”
我在项目里曾经直接把Allow-Origin写成*,结果无论怎么调前端都不带Cookie,排查了半小时才反应过来是这个通配符的问题。
4.4 postMessage跨域通信
如果你只是想在一个页面里和不同源的iframe通信,用CORS反而不方便,window.postMessage是更直接的手段。
父页面发送消息:
const iframe = document.getElementById('childFrame'); iframe.contentWindow.postMessage('父页面发来消息', 'https://child.example.com');子页面监听消息:
window.addEventListener('message', function (event) { // 这里一定要校验来源,否则任何网站发的消息都能收到 if (event.origin !== 'https://parent.example.com') { return; } console.log('收到父页面消息:', event.data); event.source.postMessage('子页面收到', event.origin); });postMessage的安全点全在接收方:必须校验event.origin,否则攻击者可以给自己的页面嵌入一个恶意iframe,向你的窗口发消息,诱导你的代码执行危险动作。这属于“没有身份验证的消息通道”,要用好它就得把每条消息当成陌生人的话来对待。
4.5 document.domain降域
这个方案适用范围很窄,只能用于同一个主域下的不同子域。比如aaa.example.com和bbb.example.com,它们天然不同源,但两个页面都执行:
document.domain = 'example.com';之后它们互相访问时,就被视为同源。
必须提醒一下,document.domain只能设置为当前域名本身或它的父级域名,不能设置为一个无关的域名。而且这个机制只对嵌套iframe之间的DOM访问有效,对XHR/fetch的跨域限制没有帮助。在实际安全评估里,document.domain赋值经常被攻击者用来配合iframe加载子域页面进行会话固定攻击,所以我个人建议能用CORS解决就不要用这个方案。
4.6 代理转发:开发环境和生产实战的首选
代理转发思路特别简单:我让浏览器只请求同源地址,由同源的服务器代替前端去请求真正的目标接口,然后把响应原样返回。
前端调/api/userinfo,Nginx收到后反向代理到http://api.example.com:8080/userinfo,浏览器全程不知道自己跨域了。这样根本没有跨域请求发生,也就不存在CORS问题了。
开发环境用Vite做代理:
// vite.config.js export default { server: { proxy: { '/api': { target: 'http://api.example.com:8080', changeOrigin: true, rewrite: (path) => path.replace(/^\/api/, '') } } } };用webpack开发服务器的话,在webpack.config.js里配置:
// webpack.config.js module.exports = { devServer: { proxy: { '/api': 'http://api.example.com:8080' } } };这个方案的好处是前端代码不用写任何跨域逻辑,生产环境也能用Nginx统一处理,还能顺便隐藏后端真实地址、做负载均衡和请求日志。缺点是所有流量都经过代理层,代理服务器自身要保证高可用。
5. 常见问题与排查技巧实录
跨域问题看着复杂,排查起来其实有固定套路。我把实战里最常见的几个问题整理成速查表,你直接照着对照。
5.1 经典报错对照速查
| 报错信息片段 | 原因 | 解决方向 |
|---|---|---|
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present | 服务端没有在响应头里返回CORS头 | 服务端必须配置Access-Control-Allow-Origin等字段 |
must not be the wildcard '*' when the request's credentials mode is 'include' | 前端开了credentials,后端却返回* | 后端把Allow-Origin改成具体来源域名 |
Response to preflight request doesn't pass access control check | 预检请求(OPTIONS)没有通过 | 确保服务端正确处理OPTIONS并返回允许的Method/Header |
Blocked a frame from accessing a cross-origin frame | 跨域iframe之间访问DOM被拦截 | 改由postMessage通信或同源部署 |
Invalid 'Access-Control-Allow-Origin' header | 响应头里的Allow-Origin值格式错误 | 检查是否有多个值或包含换行符、空格 |
5.2 排查第一步:看Network面板而不是光看console
控制台报错往往只给结论,给不了证据。我接到跨域相关的问题后,第一件事永远是打开DevTools的Network面板,重新触发一次请求,然后看两个东西:
第一个是请求是否真的发出去了。跨域请求如果被CORS拦截,你依然能在Network里看到它(状态码可能是200或403),说明不是请求的问题,而是响应校验的问题。如果连请求的影子都没有,那就去看是不是被预检卡住了。
第二个是点击该请求,看响应头(Response Headers)里有没有Access-Control-Allow-Origin这一项。没有就是服务端压根没配;有但值不对,就看具体值和页面Origin对不对得上。
另外注意预检请求。当你使用了application/json或自定义头时,Network面板里会先出现一条OPTIONS请求。选中它,重点看响应头的Access-Control-Allow-Methods和Access-Control-Allow-Headers里有没有包含正式请求要用的方法和请求头。
5.3 服务端怎么快速验证CORS配置是否生效
不用麻烦前端同学联调,你在服务器上直接用curl就能验证:
curl -i -X OPTIONS http://api.example.com/userinfo \ -H "Origin: https://www.example.com" \ -H "Access-Control-Request-Method: PUT" \ -H "Access-Control-Request-Headers: Content-Type"看返回的头里有没有预期放行规则。再测一个GET请求:
curl -i -X GET http://api.example.com/userinfo \ -H "Origin: https://www.example.com"重点看响应头里有没有Access-Control-Allow-Origin。如果第一条OPTIONS返回204、头齐全,第二条GET返回200且带上Allow-Origin,那服务端基本没问题。
5.4 我踩过的三个真实深坑
第一个坑是Nginx里重复的add_header。Nginx有个特性:如果一个location块里使用了add_header,那么它只会继承外层所有的add_header,如果你在内层又加了一个,外层的其他add_header会被吞掉。比如外层配了add_header Access-Control-Allow-Origin,内层又写了add_header X-Frame-Options,那外层那个跨域头可能就不生效了,真是血泪教训。
第二个坑是后端返回的Access-Control-Allow-Origin和当前请求不匹配。有个后端同事图省事,直接写死了某个测试域名,结果线上前端域名换了,所有跨域请求全部失效。排查到后面发现接口其实通了,就是Allow-Origin对不上。最好的实践是:由后端动态读取请求的Origin头,然后校验是否在项目白名单里,再回写同一个Origin值,这样既解决多域名问题,也不会暴露过大的跨域面。
第三个坑是自定义请求头导致的“隐式预检”。前端写了一个X-Requested-With: XMLHttpRequest,或者第三方SDK加了Authorization头,服务端没在Access-Control-Allow-Headers里声明,浏览器预检直接失败。你看到的现象就是:按钮点了没反应,控制台也没有明显的红色报错,因为有些框架把错误吞了,只在Network里能看到一条OPTIONS请求状态异常。这种问题排查起来最头疼,我是靠抓包看请求列表才发现多了一条OPTIONS。
5.5 CTF比赛里同源策略怎么考
这点顺带提一下,CTF的Web方向特别爱在同源策略上做文章。
最常见的是CSRF类型的题目:给你一个正常站点和一个攻击者页面,让你构造一个自动提交的跨域请求,配合受害者登录态去改密码、发帖、删数据。这类题的重点就是“请求能发出去、Cookie能自动带上去”,因为SOP拦的是响应读取,不拦提交。
第二种常见的是JSONP劫持。题目给一个返回callback参数的接口,服务端没有校验来源就返回用户敏感信息,你要能利用script标签跨域加载这个接口,把数据传到自己的服务器上。这考的其实就是“script标签不受SOP限制”这条特性。
第三种是SOP绕过技巧。某些场景下,攻击者通过window.name、location.href跳转后的历史记录、postMessage配置不当等途径,间接读取跨域页面的信息。本质上都是在挑战“浏览器把哪些信息暴露给了跨域页面”的边界。
对CTF感兴趣的读者,学完同源策略之后建议去做几道CSRF和JSONP劫持的入门题,能瞬间把这些知识点串起来。
最后再分享一个我自己带新人时常用的测试习惯:写前端之前,先打开浏览器开发者工具,在Console里执行一个最简单的fetch请求,确认目标接口的跨域策略再动手,可以省掉后面大半的联调时间:
fetch('http://api.example.com/userinfo', { method: 'GET', credentials: 'include' }).then(r => r.json()).then(console.log).catch(console.error);如果这行代码在目标页面里能跑通,CORS这一关就已经过了;如果报错,直接从Network面板看响应头缺什么,按上面那张速查表逐项排查就行。同源策略本身不难,难的是把浏览器、服务端、代理层这三个环节里各自的职责边界搞清楚。这也是我把它放在Web安全系列第一篇的原因:后面聊XSS、CSRF、点击劫持这些攻击手法时,本质上都是在跟这一套“默认拒绝、按需放行”的浏览器策略打交道。