Title/ 浏览器跨域(CrossOrigin)请求的原理, 以及解决方案详细指南 #flight.Archives011
序:
最近看到又有一波新的创作活动了, 官方给出的话题中有一个"为什么XHR不能跨域请求资源", 看起来像一道很简单的面试题哈哈哈
作为前端爱好者, 不妨借此机会研究一下跨域, 总结一篇文章出来. 于是我就安排了flight.A011的FocusList#写作计划.简介:
一篇最简洁高效的跨域请求指南 made with by忘我思考
注:
- 本文中XHR表示
XMLHttpRequest
对象.- CORS表示
Cross-origin resource sharing
, 译为跨域资源共享.- 请求头即 Request Headers, 响应头即 Response Headers.
Tag/ 浏览器跨域(CORS)限制介绍: 为什么要限制XHR跨域请求, 如何触发跨域限制
大佬们可以直接看下一章解决方案, 这里会解释得详细一点方便新手入门(谁都曾是萌新嘛), 不了解CORS的来这补补课吧~
跨域, 就是站A的请求要访问站B的服务器.
如果 协议
(protocol), 域名
(domain), 端口
(port) 三者有任意一处不同, 浏览器就认为这是两个不同的网站, 会触发跨域安全限制.
详细的说, 就是:
[https://][example.com][:80]/path?query#heading-1
只有打方括号内的内容完全一致才是"同源", 也就是说同源请求URL中只有端口后的内容是可以不一致的, 否则就是跨域
请注意: https协议的默认端口是443, http协议默认端口是80. 默认端口可以省略不写.
也就是说 https://example.com:443/... 和 https://example.com/... 是同源, http同理.
->> 为什么浏览器要有跨域限制
设想这样的场景:
我是不知名小Blog主, 你现在在访问我的博客网站, 然后我突发奇想, 想要用自动JS程序发送请求到 weibo 的服务器, 让你自动关注我的微博.
如果这能实现, 那浏览器就太不安全了, 网站主可以在后台做很多危险请求(如获取你在 weibo 的登录信息等).
所以浏览器都有一个跨域限制, 阻止不安全的第三方内容请求.
举个例子:
如果我在Segmentfault官网的Console访问我的博客园首页, 就会触发XHR跨域限制, 禁止读取返回内容.
而另一个情景:
你是一个小站长, 在网站使用Vue.js, 于是添加了一个CDN脚本(如 unpkg
等)
结果浏览器错误的因为跨域限制自动拦截请求导致你无法使用CDN脚本.
这显然是不合理的, 所以现在的浏览器都会智能根据对方(网站B)的 响应头
中的 Access-Control-Allow-Origin
属性, 来判断你是否可以读取对方返回的内容.
比如 unpkg
就把 Access-Control-Allow-Origin
属性设为了 *
, 方便外域调用CDN.
像React也建议你验证CDN的请求头属性, 避免使用出现跨域限制问题.
也就是说, 你能不能访问第三方资源, 其实是由对方(响应头)决定的(这其实也是合理的, 按道理请求别人的东西就要经过允许呀).
对方服务器可以通过你的 cookie
referer
等 请求头
参数来判断请求的安全性, 决定是否返回对应内容.
注意: 纯前端是不能纂改 referer
请求头的, 否则会报错.
->> 浏览器对请求的详细划分方法: 简单请求和复杂请求
不过浏览器的跨域限定其实有详细的划分, 这里有必要提到一下:
请求分为 简单请求
和 复杂请求
两种.
对于 简单请求
, 要满足以下所有条件("与"关系).
- 请求方法是
GET
,POST
,HEAD
之一. - 人为设置的请求头不超过以下属性:
Accept
,Accept-Language
,Content-Language
. -
Content-Type
属性值是text/plain
,multipart/form-data
,application/x-www-form-urlencoded
之一. - 请求中的任意
XMLHttpRequestUpload
对象均没有注册任何事件监听器;XMLHttpRequestUpload
对象可以使用XMLHttpRequest.upload
属性访问. - 请求中没有使用
ReadableStream
对象.
其中前3点比较常用. 如果请求不满足以上5个条件的任意之一, 则是复杂请求.
浏览器区分 简单请求
和 复杂请求
是为了兼容表单(form), 因为历史上表单一直可以发出跨域请求.
->> XHR请求在浏览器里是如何被发出去的? 浏览器对简单请求和复杂请求的不同处理方式
这里我觉得阮一峰的文章已经写得很好了(在文末有链接), 所以, 如有雷同(不对, 就是雷同)... 不要觉得我是在抄袭, 只是按照他的
创意共享3.0许可证
合法借鉴哈.
-
浏览器对简单请求的处理方式!
如果一个XHR满足简单请求的所有判定, 那它会被这样发出去:// 直接发送包含Origin请求头的XHR请求到对方服务器
浏览器会自动在头信息之中, 添加一个Origin字段.
如: Origin: http://api.gold.xitu.io (现在掘金被字节收取就变味了, 好怀念Ming在的时间啊...)
Origin是"起源"的意思, 包含协议 + 域名 + 端口, 会告诉对方请求是从哪个源发出的.
如果Origin指定的源不在许可范围内(比如在cnblogs中添加访问Bilibili的代码), 对方会返回一个不包含Access-Control-Allow-Origin的响应头,
浏览器就知道出错了, 从而抛出一个错误, 被XMLHttpRequest的onerror回调函数捕获.
注意: 这种错误无法通过状态码识别, 因为HTTP回应的状态码有可能是200. 如果Origin指定的域名在许可范围内, 服务器返回的响应, 会多出几个头信息字段:
Access-Control-Allow-Origin: http://api.bbb.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: FooBar
上面这些以 Access-Control- 开头的字段, 都表示对方服务器对请求的访问限制. 下面介绍一下这三个属性:
1. Access-Control-Allow-Origin(必须属性): 可以是URL, 表示对请求时Origin字段的值的限制, 也可以是 *, 表示接受任意域名的请求, 像CDN代码存储服务之类的.
2. Access-Control-Allow-Credentials(可选): 一个布尔值, 只能为true, 表示是否允许发送Cookie. 默认情况下(不填该属性), 则Cookie不包括在跨域请求之中.
3. Access-Control-Expose-Headers(可选): 跨域请求时, XHR对象的getResponseHeader()方法只能拿到6个基本字段: Cache-Control, Content-Language, Content-Type, Expires, Last-Modified, Pragma. 如果想拿到其他字段, 必须在Access-Control-Expose-Headers中指定. 上面的例子指定getResponseHeader('FooBar')可以返回FooBar字段的值. ->> Details: withCredentials 属性
上面说到, 跨域请求默认不发送Cookie和HTTP认证信息. 如果要把Cookie发到服务器, 一方面要服务器同意, 指定Access-Control-Allow-Credentials字段为true.
另一方面, 网站主必须在AJAX请求中打开withCredentials属性. 像这样: var xhr = new XMLHttpRequest();
xhr.withCredentials = true; 否则, 即使服务器同意发送Cookie, 浏览器也不会发送. 这种情况下服务器即使要求设置Cookie, 浏览器也不会处理. 但是, 如果省略withCredentials设置, 有的浏览器还是会一起发送Cookie. 这时, 可以显式关闭withCredentials:
xhr.withCredentials = false; 需要注意的是, 如果要发送Cookie, Access-Control-Allow-Origin就不能设为星号, 必须指定明确的, 与请求网页一致的域名. 同时, Cookie依然遵循同源政策, 只有用服务器域名设置的Cookie才会上传, 其他域名的Cookie并不会上传, 且(跨域的)原网页代码中的document.cookie也无法读取对方服务器域名下的Cookie. -
浏览器对复杂请求的处理方式!
// 先发送预检请求(preflight), 请求通过后, 发送XHR到对方服务器
浏览器会先询问服务器, 当前网页所在的域名是否在服务器的许可名单之中, 以及可以使用哪些HTTP动词和头信息字段. 只有得到肯定答复, 浏览器才会发出正式的XMLHttpRequest请求, 否则就报错. 来了, 这是一段JS代码!
var url = 'http://api.aaa.com/cors';
var xhr = new XMLHttpRequest();
xhr.open('PUT', url, true);
xhr.setRequestHeader('X-Custom-Header', 'value');
xhr.send(); 可以看到使用了 PUT 方法, 还人为设置了X-Custom-Header请求头, 所以这是个复杂请求qwq... 所以浏览器会发送一个预检请求, 预检请求的请求头像这样:
OPTIONS /cors HTTP/1.1
Origin: http://api.bbb.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
Host: api.aaa.com
...(以及一些更多的无关请求头) 预检请求使用了 OPTIONS 方法, 表示这个请求是用来询问的. 请求头中, 关键字段是Origin, 表示请求来自哪个源.
而 Access-Control-Request-Method 和 Access-Control-Request-Headers 你一看就明白是什么了, 分别是请求方法和人为设置了哪些属性. 服务器受到预检请求后, 判断请求头是否OK, 如果OK就可以做出回应啦!
响应头中关键信息是这几个:
Access-Control-Allow-Origin: http://api.bbb.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header 解释一下~
1. Access-Control-Allow-Origin: 表示允许的请求源, 也可以是*表示随便什么网站.
2. Access-Control-Allow-Methods: 就是允许的方法啊!
3. Access-Control-Allow-Headers: 允许设置的请求头参数. 好了, 那如果服务器不同意预检请求怎么办呢~ 会返回一个正常的HTTP回应, 但是没有任何CORS相关的头信息字段. 这时,浏览器就认为, 服务器不同意预检请求.
因此触发一个错误, 被XMLHttpRequest对象的onerror回调函数捕获.
控制台会打印出如下的报错信息: XMLHttpRequest cannot load http://api.aaa.com.
Origin http://api.bbb.com is not allowed by Access-Control-Allow-Origin. 服务器回应的其他CORS相关字段如下:
Access-Control-Allow-Methods: GET, POST, PUT //所有支持的方法
Access-Control-Allow-Headers: X-Custom-Header //如果浏览器请求包括Access-Control-Request-Headers字段, 则Access-Control-Allow-Headers字段是必需的, 表示支持的所有请求头字段.
Access-Control-Allow-Credentials(可选): true //也是表示Cookie
Access-Control-Max-Age(可选): 1728000 //表示本次预检请求的有效期, 单位为秒. 比如这个就设定了本次请求有效期是20天(1728000秒), 期间不再次发送请求.
->> 案例: 解决一个博客音乐播放器的跨域问题
浏览器的安全限制其实也不是完美无缺的, 前端的话如果不涉及验证码, 登录, 加密之类的复杂操作, 通过一些技术手段想要突破跨域限制, 也不是没有可能.
无后端支持也是可以实现跨域的, 不过一般要在用户知情的情况下配合操作.
其实就是你可以实现一些跨域请求, 但是是否能够获取请求内容, 本来是由对方服务器请求头决定的, 现在决定权在于用户.
比如你想要在你的博客中添加一个音乐播放器, 播放QQ音乐的内容.
而众所周知, 腾讯那边的盗链(可以理解为跨域)限制是很强的!
所以一般我的实现是配合后端, 也就是访问QQ音乐网址, 把音乐通过浏览器控制台Network栏获取请求资源URL下载保存到自己的服务器.
然后直接访问自己的服务器获取音乐资源(QQ音乐一般是m4a, 网易一般是mp3, 不过顺便一提网易取消了盗链保护, 所以一般可以直接用网易服务器, 只是有的音乐有版权保护就不能外域播放了).
而如果要纯前端实现跨域音乐播放则可以使用 <iframe>
, Chrome参数纂改, Chrome插件等方法来实现盗链.
这些《神奇》的方法也就是下一章会详细介绍的内容啦~ [奸笑]
Tag/ 重头戏开始了! 突破跨域限制的9种解决方案
-
使用
iframe
实现iframe
标签可以将跨域内容嵌入你的网页, 比如在博客中添加一个Bilibili视频 或者 Codepen Demo的iframe
.
不过iframe
有两个缺点:- 很多服务器会直接拒绝跨域
iframe
请求, - 跨域访问
iframe
中的内容会遭到限制qwq.
放几张图体会一下:
如果在阮一峰的Blog中插入 iframe(cnblogs博文), 会被拒绝qwq...
而如果在站内插入iframe则没有问题, 看来博客园为了安全考虑目前关闭了跨域请求.
而Bilibili就比较开放了, 目前可以插入iframe.
不过如果你想通过iframe读取或者篡改Bilibili网页的内容也是不可以的啦~
所以这终究只是一个向用户"展示内容"的方法而已, 限制很多, 如果要读取/篡改内容, 还是得同源才行~
- 很多服务器会直接拒绝跨域
-
使用CORS方法(跨域资源共享)
这个方法也就是第一章中介绍的, 很详细了. 不用再重复一遍啦.但是这种方法也是受到对方服务器的种种限制, 不能为所欲为qwq...
-
搞一个浏览器插件配合(终极大招, 直接无视跨域限制)
嘿嘿嘿! 没想到吧! 竟然还有这种方法?!
网上这方面的文章一般都不会想到有这种神奇也是非常强大的方法, 不过这也是非常实用的.
如果要我来, 很可能会采用这种方法, 可以不用后端支持进行跨域!
如果你的网站面向的对象主要是爱好破解的极客/有探索精神的玩家, 可以向他们推荐你的Chrome插件, 或者是一段油猴脚本.
比如让用户简单的copy&paste添加一段你的油猴(TemperMonkey)脚本, 就可以实现跨域请求啦!
(现在油猴/Chrome插件在极客/开发者手里也已经很普及了吧~, 想想你装了多少插件...
(当然一个都没装的勿喷!不过现在Chrome插件那么多, 可是你... 能不能独立开发一个扩展插件呢? 恐怕很难吧...
(其实如果你懂前端, 插件开发也不难, 只要了解Chrome插件专门的一些方法就行了.
那么这里就简单介绍一下实现方法, 如果你想开发一个完整实用的插件, 网上也有不少文档了. (Chrome Developer中也有详细的介绍不如先学习一下别人是怎么玩的. 我们随便打开一个别人开发的Chrome插件的文件夹(我打开的是Tempermonkey), 是这幅景象:
其中有一个
manifest.json
文件, 和你的插件支不支持跨域密切相关, 比如油猴的长这样:{
"background": {
"page": "background.html"
},
"browser_action": {
"default_icon": {
"16": "images/icon_grey16.png",
"19": "images/icon_grey19.png",
"24": "images/icon_grey24.png",
"32": "images/icon_grey32.png",
"38": "images/icon_grey38.png"
},
"default_popup": "action.html",
"default_title": "Tampermonkey"
},
"commands": {
"open-dashboard": {
"description": "Open dashboard"
},
"open-dashboard-with-running-scripts": {
"description": "Open dashboard with the current tab's URL used as filter"
},
"open-new-script": {
"description": "Open new script tab"
},
"toggle-enable": {
"description": "Toggle enable state"
}
},
"content_scripts": [ {
"all_frames": true,
"js": [ "rea/common.js", "content.js" ],
"matches": [ "file:///*", "http://*/*", "https://*/*" ],
"run_at": "document_start"
} ],
"content_security_policy": "script-src 'self'; object-src 'self';",
"default_locale": "en",
"description": "The world's most popular userscript manager",
"differential_fingerprint": "1.2ae827c5b75f9e167b3ed0bf65d0c330028dd0acf93a84a8f0f2d7e096024ba3",
"icons": {
"128": "images/icon128.png",
"32": "images/icon.png",
"48": "images/icon48.png"
},
"incognito": "split",
"key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCwuYtg7kY2YyNieOkV9pK/qcXwUXu0CFUO0zU6DLAGAJZK7zxrHlwg9a+zFH7CXqgH7zSfRce9KiYOHJaLPBXM66uPCliiQ6Q+bFaNZx1FxLXkZTFnlyPh8kkwwohLJeSQ9NQXqEfeTepDj5BRufAR48az0MC5aUTEj+fFXbzX7QIDAQAB",
"manifest_version": 2,
"minimum_chrome_version": "64.0.0.0",
"name": "Tampermonkey BETA",
"offline_enabled": true,
"optional_permissions": [ "downloads" ],
"options_page": "options.html",
"options_ui": {
"chrome_style": false,
"open_in_tab": true,
"page": "options.html"
},
"permissions": [ "notifications", "unlimitedStorage", "tabs", "idle", "webNavigation", "webRequest", "webRequestBlocking", "storage", "contextMenus", "chrome://favicon/", "clipboardWrite", "cookies", "declarativeContent", "\u003Call_urls>" ],
"short_name": "TM BETA",
"update_url": "https://clients2.google.com/service/update2/crx",
"version": "4.14.6142"
}这个是Chrome插件的灵魂, 设置了各种属性, 包括Chrome插件的图标, 点击Chrome插件图标打开的popup页面等...
这里和跨域相关的只有一个: 就是permissions
属性.这个属性设置了插件有哪些权限, 比如可以随意访问哪些网站.
比如油猴的, 就有一个
"\u003Call_urls>"
在permissions
属性里.
其中\u003C
其实就是<
的Unicode编码, =<all_urls>
, 也就是这个插件定义的JS代码可以随意访问各个网站而在用户眼中就是这幅景象:
关于Chrome插件配合网站实现跨域的文档, 可以参考这篇 https://wizardforcel.gitbooks.io/chrome-doc/content/23.html, 总结得很完整了.
而我搜索的时候, 惊喜的发现, 还有人开发了这样一个插件(不是我开发的啊):
名字叫 "Allow CORS", 挺好用的, 初衷是帮助开发者跨域调试网站.用户评价也挺不错的.
网址就是 https://chrome.google.com/webstore/detail/allow-cors-access-control/lhobafahddgcelffkeicbaginigeejlf
用途当然就是和名字一样, 可以解除跨域限制. 所以你只要让用户轻轻一点去装上这个插件, 就可以无视跨域限制啦!
不过Chrome因为Google原因, 在国内有限制, 面对一般的用户Chrome也支持上传本地包来添加插件, 也很简单,下载->一点打开开发者模式->一拖添加插件
就OK啦!好了这个方法是真的tql.
配合客户端本地代理VPN(终极大招)
看完第三个方法, 我觉得其他的都要退下了, 毕竟第3个方法应该是最简单的了.
第四个原理差不多, 不过开发起来是真的麻烦啊.
要让用户装一个系统应用, 本地VPN, 就像Charles, Fiddler这样.
然后在本地自动修改 Referer 和 Origin 等请求头, 假装一个正常的用户请求.
总之不是一个优秀的解决方案吧.-
配合后端(终极大招)
本文着重纯前端开发, 我也不太懂后端(基本的php除外, 用来建博客要用到)
这个方法也是很实用的, 但是不详细介绍了.
就是针对不涉及用户在浏览器中cookie的情况, 通过后端语言(如Java)正常请求来获取资源.
再返回到前端, 请求一个本站资源(其实转自对方站).比如第一章中提到的音乐播放器, Github上就有人用配合后端实现了! 还有1.9k个star!
不过这个解析下载音乐因为侵权了, 所以2018年起暂停维护了, 不过下载下来依然可以使用.不过只能下载免费音乐, 付费听的音乐会ErrorHappensQwQ.
因为只是后端搜索解析一下, 音乐资源依然是来自对方服务器(所以QQ音乐盗链保护还是生效, 不能听啊... 网易就没事, 毕竟网易一向开放)
想了解一下的, 可以啪的点进去: https://github.com/maicong/music -
JSONP方法
对于script
标签的资源, 浏览器是没有跨域限制的! (对方按照referer/origin属性不返回数据的情况除外)比如网上就有这样的实现:
<!-- 原生JS实现 -->
<script type="text/javascript">
window.jsonpCallback = function(res) {
console.log(res);
};
</script>
<script
src="http://localhost:80/api/jsonp?&cb=jsonpCallback"
type="text/javascript"
></script>
<!-- JQuery Ajax实现 -->
<script src="https://cdn.bootcss.com/jquery/3.5.0/jquery.min.js"></script>
<script>
$.ajax({
url: "http://localhost:80/api/jsonp",
dataType: "jsonp",
type: "get",
data: {
msg: "hello"
},
jsonp: "cb",
success: function(data) {
console.log(data);
}
});
</script>嗯, 就是这样用的, 很简单获取Script内容, 不过也只支持Script qwq...
-
WebSocket实现
WebSocket是HTML5的一个特性, 可以让浏览器化被动为主动发送内容, 适合实时直播等场景(比如字节的掘金直播就用到了这种方法)WebSocket其实虽然有用(本来就没有CORS的跨域限制), 但其实也没有办法获取HTTP资源QAQ.
所以这个方法也不推荐吧!
-
window.postMessage() 方法
这个方法可以安全地实现跨源通信!不了解这个方法, 可以去这里补补课~ ->> https://developer.mozilla.org/zh-CN/docs/Web/API/Window/postMessage
在《HTML5CSS3权威指南》中也有介绍, 但是在实战中这个方法使用也比较少哈~
这个方法可以进行不同界面的消息传递:
比如多窗口之间, 网页和其iframe之间.比如QQ音乐就用到了这种方法.
如果你开启了多个播放页面, QQ音乐会通过这个方法多界面之间通信, 自动暂停之前的播放页, 避免同时播放多个音乐.当然正因为它可以 "安全地" 实现跨源通信, 所以这个通信方法也是需要对方服务器认可的. 所以如果要做不正经的事(对方不允许的情况下强行获取资源)也没有用啊...
-
使用"允许跨域"方法打开浏览器
既然是浏览器搞的限制, 能不能关闭这个限制呢?
是可以的!就以这种方法打开(Windows, xxx是你的data目录):
C:/.../.../path/chrome.exe --disable-web-security --user-data-dir=xxxx
Mac也可以! 也是一样的.
--disable-web-security --user-data-dir=~/Downloads/chrome-data
就是这样, 不过要重启Chrome才行, 用户可能不愿意, 所以还是插件这个方法我最推荐.
当然, 如果不正当获取了第三方内容是可能会有律师函警告的~
所以说, 本文只是对跨域的介绍和技术上的探索与研究, 仅此而已. (你懂的[doge]
->> Details
附录 - CORS请求与JSONP的比较
CORS与JSONP的使用目的相同, 但是比JSONP更强大.
JSONP只支持GET请求, CORS支持所有类型的HTTP请求. JSONP的优势在于支持老式浏览器, 以及可以向不支持CORS的网站请求数据.
->> Reference link
MDN中文文档 https://developer.mozilla.org/zh-CN/docs/Web/HTTP/CORS
MDN 英文文档 https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
阮一峰 https://www.ruanyifeng.com/blog/2016/04/cors.html
秋风的笔记 https://segmentfault.com/a/1190000022398875
思否 - 安静de沉淀 https://segmentfault.com/a/1190000011145364
掘金 - 小铭子 https://juejin.cn/post/6844903882083024910
掘金 - JackySummer https://juejin.cn/post/6861553339994374157
Chrome Developers https://developer.chrome.com/docs/extensions/mv3/xhr/
Html5Rocks https://www.html5rocks.com/en/tutorials/cors//
博客园 - 小火柴的蓝色理想 https://www.cnblogs.com/xiaohuochai/p/6036475.html
知乎 - 单纯前端能否解决跨域问题? https://www.zhihu.com/question/302245173
MDN - XHR介绍 https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest
MDN - XHR介绍(英文) https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest
MDN - Access-Control-Allow-Origin 属性介绍 https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Access-Control-Allow-Origin
MDN - Access-Control-Allow-Origin 属性介绍(英文) https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin
* - How does Access-Control-Allow-Origin header work? https://*.com/questions/10636611/how-does-access-control-allow-origin-header-work
* - CORS with XMLHttpRequest not working https://*.com/questions/25296455/cors-with-xmlhttprequest-not-working
W3 - CORS Enabled https://www.w3.org/wiki/CORS_Enabled
->> Version History
现在版本为V1.0
详见 Github(@flightmakers)2021.8.23 发布V1.0