关于跨域...
今天整理一些有关于跨域的内容…
为什么需要跨域?
浏览器实施同源策略,这是一种基本的安全协议。该策略禁止来自不同域的JavaScript脚本与另一个域的资源进行交互。
其中,同源是指两个URL的协议、主机和端口号都一致
为什么浏览器需要这种限制呢?
“同源策略”是浏览器的安全机制,其目的是:
防止恶意网站读取你登录状态下的敏感信息(如银行、邮件、后台管理等)。
举个例子:
你在 bank.com 登录了账户,浏览器存有 cookie。如果没有同源限制,恶意网站 evil.com 可以伪造请求读取你的银行账户信息,这就是“跨站请求伪造(CSRF)”的根源之一。
跨域是指当前网页的源和要请求的目标接口的源不一致
由于浏览器同源策略的限制,将存在以下的跨域问题:
- 无法访问来自不同源网页的Cookie、LocalStorage和IndexedDB,也就是说不同源的网页之间不能共享存储数据
- 无法操作不同源网页的DOM,每个网页的DOM只能由其自己的脚本访问,不能被其他源的脚本操作。
- 无法向不同源地址发起AJAX请求,这限制了网页与不同源服务器之间的数据交互。
这些限制在确保Web应用安全性防止恶意网站访问其他网站的敏感数据,但同时也给开发跨域Web应用带来了挑战。
相应的解决方案有:
1.JSONP
浏览器对<script>
标签没有跨域限制,通过动态创建script标签,请求后端返回可执行的代码。
只支持GET请求,使用于一些旧项目,无法配置CORS的第三方服务。
前端代码
1 | <script> |
后端返回
1 | handleResponse({ name: "OpenAI", type: "AI" }); |
也就是在请求的时候不仅要写接口路径,还要加一个回调函数名,并将完整的路径写入script的src中,后端返回数据的时候将函数名与内容拼接,这样就可以直接对返回的内容进行操作了。
2.document.domain
两个页面可以设置相同的document.domain,浏览器是通过这个属性来检查两个页面是否同源,只要设置相同的document.domain,两个页面就可以共享Cookie。
但是仅限于主域相同,子域不同的跨应用场景。比如a.example.com和b.example.com
1 | // a.example.com 和 b.example.com |
此时两个页面就可以互相访问DOM和JS了。逐渐被淘汰了。
3.CORS
跨资源共享标准新增了一组HTTP标头字段,允许服务器声明哪些源站通过浏览器有权限访问哪些资源。
对于可能对服务器数据产生副作用的HTTP请求方法,浏览器必须先使用OPTIONS方法发起一个预检请求,从而获知服务器是否允许该跨源请求。服务器允许后,才发起实际的HTTP请求。
在预检请求的返回中,服务端也可以通知客户端是否需要携带身份凭证。
CORS请求失败会产生错误,但是为了安全,在JS代码层面无法获知到底具体是哪里出现了问题,只能在浏览器的控制台得知。
简单请求是指不会触发CORS预检请求的请求,
需要满足以下条件:
- GET、HEAD、POST
- 除了被用户代理自动设置的标头字段,允许人为设置的字段为Fetch规范定义的对CORS安全的标头字段集合(Accept、Accept-Language、Content-Language、Content-Type、Range)
- Content-Type标头所指定的媒体类型仅限于:text/plain、multipart/form-data、application/x-www-form-urlencoded
- 如果是使用
XMLHttpRequest
对象发出的请求,在返回的XMLHttpRequest.upload 对象属性上没有注册任何事件监听器 - 请求中没有使用 ReadableStream 对象。
假如说站点https://foo.example
的网页应用想要访问https://bar.other
的资源。这个时候,浏览器发送给服务器的请求报文可能如下所示:
1 | GET /resources/public-data/ |
其中的Origin表明该请求来源于https://foo.example
而服务器的响应如下所示:
1 | 200 OK |
其中,服务端返回的Access-Control-Allow-Origin: *
值表明,该资源可以被任意外源访问。
预检请求可以避免跨域请求对服务器的用户数据产生未预期的影响
与简单请求不同的是,“需预检的请求”要求必须首先使用 OPTIONS 方法发起一个预检请求到服务器,以获知服务器是否允许该实际请求。
比如,以下是一个需要执行预检请求的HTTP请求,该请求包含了一个非标准的HTTPX-PINGOTHRT
请求标头,请求的Content-Type为application/xml,所以要发起预检请求。
1 | const fetchPromise = fetch("https://bar.other/doc", { |
首先是预检请求/响应
1 | OPTIONS /doc |
其中OPTIONS请求中携带了两个标头字段
- Access-Control-Request-Method: POST,告知服务器实际请求将用POST方法。
- Access-Control-Request-Headers: X-PINGOTHER, Content-Type,告知服务器实际请求将携带两个自定义请求标头字段。
服务器的响应中写明了允许的源域、允许的请求方法、允许的标头,最后的Access-Control-Max-Age给定了该预检请求可供缓存的的事件长短,默认5秒,在有效时间内,浏览器不需要为同一请求再次发起预检请求。
浏览器自身维护一个最大有效时间,如果超过最大有效时间将不会生效。
预检请求完成之后,将发送实际请求:
1 | POST /doc |
但是如果预检请求发生了重定向,一部分浏览器会报错,解决办法有:
- 在服务端去掉对预检请求的重定向
- 将实际请求变成一个简单请求
- 发出一个简单请求判断真正的预检请求会返回什么地址,然后发送另一个请求在上一步通过response.url获得的URL
但是如果请求是由于存在Authorization字段而引发了预检请求,则这一方法无法使用
附带身份凭证的请求
一般而言,对于跨源的XMLHttpRequest或Fetch请求,浏览器不会发送身份凭证信息
,如果要发送,需要设置XMLHttpRequest对象的某个特殊标志位,或在构造Request对象时设置。
1 | const url = "https://bar.other/resources/credentialed-content/"; |
上面的代码构造了一个Request对象,并在构造器中将credentials选项设置为”include”,但是浏览器会拒绝任何不带Access-Control-Allow-Credentials: true
标头的响应,且不会把响应提供给调用的网页内容
预检请求不能包含凭据,预检请求的响应必须指定Access-Control-Allow-Credentials: true
来表明可以携带凭据进行实际的请求。
在响应附带身份凭证的请求时,
- 服务器不能将Access-Control-Allow-Origin 的值设为通配符“*”。
- 服务器不能将 Access-Control-Allow-Headers 的值设为通配符“*”。
- 服务器不能将 Access-Control-Allow-Methods 的值设为通配符“*”。
整理标头字段
响应标头字段:
- Access-Control-Allow-Origin
- Access-Control-Expose-Headers:指定标头放入允许列表中
- Access-Control-Max-Age
- Access-Control-Allow-Credentials
- Access-Control-Allow-Methods
- Access-Control-Allow-Headers
请求标头字段:
- Origin
- Access-Control-Request-Method
- Access-Control-Request-Headers
感觉这部分内容有一些难理解,做一个小小的类比…
你正在网站 https://foo.example 上写前端代码,你的代码像一个“游客”,想去另一个国家 https://bar.api.com
获取一些资源(比如用户数据、图片、JSON 数据等)。这就是一个跨域请求(因为两个域名不一样)。
浏览器是“边境警察”,负责检查这个请求是否合法。
🎬 第一幕:简单请求 —— 小事一桩,直接放行
你的小代码只是用 GET 或 POST 方法访问接口,也没有带什么奇怪的行李(特殊 header 或 JSON 内容),这时候警察觉得:
“嗯,你是个正常游客,看起来不危险。”
于是浏览器直接放你过去,但是提醒你一句:
“你可以去,但是我还得看看对面国家(服务器)愿不愿意接你。”
然后浏览器就在请求里加一个身份说明:
request
1 Origin: https://foo.example这就像告诉服务端:
“喂!我这边来了一位游客,是从 https://foo.example 出发的,你愿不愿意接待他?”
如果服务器回答说:
request
1 Access-Control-Allow-Origin: https://foo.example“欢迎欢迎,我们这儿允许你这位游客入境。”
那么浏览器就放心了,把服务器的响应结果传给网页里的 JS 用了!
但是如果服务器说:
request
1 Access-Control-Allow-Origin: *“我们谁都欢迎!”(广结善缘)
那么浏览器只会允许普通请求,不允许携带 cookie 的请求通过(因为太泛了,怕泄露隐私)。
🎬 第二幕:预检请求 —— 带刀剑的游客必须先查一查
如果你发的请求不再那么“简单”了,比如:
- 带了 Authorization、X-Custom-Header 这样的自定义头;
- 请求方法是 PUT、DELETE 等;
- 请求体是 application/json(不是默认的表单格式);
浏览器会马上警觉:
“这个游客行李太多、身份复杂,必须先发个探查请求去问一下那边边检站。”
于是浏览器会先发一个 预检请求(preflight request),这是一个 OPTIONS 请求,里面写着:
request
1
2
3
4 OPTIONS /api/data
Origin: https://foo.example
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Authorization这意思就是:
“你好,我打算从 https://foo.example 发一个 POST 请求,带上 Authorization,请问你同意吗?”
如果服务器说
request
1
2
3
4 Access-Control-Allow-Origin: https://foo.example
Access-Control-Allow-Methods: POST
Access-Control-Allow-Headers: Authorization
Access-Control-Allow-Credentials: true“我们查过啦,这个游客可以过关,请让他进来。”
浏览器才允许正式请求发出去!
如果服务器没回答清楚,比如:
- 没有 Access-Control-Allow-Origin
- 没有 Access-Control-Allow-Headers
- Allow-Origin: * 却又想接收 Cookie
那浏览器就会说:
“不行!你那边手续不全,我不让这个请求出去。”
🎬 第三幕:请求携带身份凭证 —— 需要贴身安检!
如果你的请求想带 cookie、session 或者 Authorization 这种凭证,浏览器会特别小心。
你得主动告诉浏览器:
1
2
3 fetch("https://bar.api.com/data", {
credentials: "include"
})就像告诉警察:
“这个游客带着身份证(cookie),得贴身检查!”
这时浏览器只会接受那些:
- Access-Control-Allow-Origin 指定了你的网站(不能是 *)
- Access-Control-Allow-Credentials: true 明确说:我接收带身份的游客!
否则浏览器直接说:
“你带身份证来,但对方不欢迎,结果我就把这个回应扔掉,不给 JS 用。”
有时候即使你做对了所有 CORS 的设置,浏览器仍然不让你带 cookie!
这是因为:很多浏览器默认会禁止跨站设置或发送 cookie,比如 Chrome、Safari 都开始阻止所谓的“第三方 cookie”(防广告追踪)。
这时你要在服务器设置:
request
1 Set-Cookie: token=abc123; SameSite=None; Secure“这个 cookie 是允许跨站的,而且必须在 HTTPS 下使用!”
🎬 第四幕:重定向风波 —— 半路被拉去另一个国家
结果服务器 bar.api.com 回复说:
“哎哟,我不在这里办公啦,我们搬家了!请你去 https://api2.other.com/data。”
这时候就发生了 重定向(服务器返回 302/307)!
这时候浏览器会说:
“等会儿,我这是发预检请求,你怎么半路换个国家让我去?我可不干!”
4.Nginx反向代理
反向代理是一种服务器,它接收客户端的请求,并将这些请求转发给后端的真实服务器处理,然后将响应返回给客户端。
也就是说:
浏览器 → 反向代理(如 Nginx) → 后端服务
最终浏览器看到的是 Nginx,但 Nginx 在背后转发请求。
- 在 Nginx 配置文件中,为需要代理的每个服务设置一个特定的前缀。
- 配置Nginx将这些前缀的HTTP/HTTPS请求转发到对应的真实服务器。
- 通过这种方式,所有通过Nginx转发的URL都将具有相同的域名、协议和端口号,从而满足浏览器的同源策略要求。
由于所有 URL 都指向同一个服务器,浏览器将它们视为同源,从而避免了跨域访问的限制。实际上,这些 URL 背后是由不同的物理服务器提供服务。这样,服务器内部的JavaScript代码就可以自由地跨域调用这些服务器上的资源。
5.postMessage(跨窗口/iframe 通信)
在使用iframe或弹出窗口时,需要跨文档的消息传递。postMessage方法提供一种安全方式实现跨源通信,允许父窗口和子窗口之间进行消息交换。
发送方(iframe或子窗口)
1 | window.opener.postMessage('你好,主窗口!', 'http://example.com'); |
接收方(主窗口)
1 | window.addEventListener('message', (event) => { |
6.WebSocket
WebSocket是HTML5的一个持久化协议,实现了浏览器与服务器的全双工通信。初次握手虽然为HTTP,但是连接建立好了之后客户端与服务器的通信就基于WS协议,与HTTP无关了。
服务端需监听 WebSocket 协议连接。,应用场景一般为实时数据,比如在线聊天室、股票推送等。
7.window.name / location.hash
某些浏览器特性允许在跨页面之间通过window.name或location.hash传递数据。
- 页面 A 打开 B.html,B.html 设置 window.name = ‘数据’,再跳转回 A
- 设置 B 的 location.hash = ‘#data’,B 从 location.hash 中读取
仅限页面跳转数据传递,不适合 AJAX 场景。