跨域
浏览器的同源策略
同源策略是一个重要的安全策略,它用于限制一个origin的文档或者它加载的脚本如何能与另一个源的资源进行交互。它能帮助阻隔恶意文档,减少可能被攻击的媒介。所谓的同源是指协议、域名和端口号都相同,如果有一个不同都不算是同源。
浏览器限制脚本内发起的跨源 HTTP 请求。 例如,
XMLHttpRequest
和 Fetch API
遵循同源策略。这意味着使用这些 API 的 Web 应用程序只能从加载应用程序的同一个域请求 HTTP 资源,假如发送一个跨域请求,浏览器则会将响应结果丢弃,除非其符合CORS策略。跨域方案
图片探测
虽然xhr对象和fetch API会受到浏览器的同源策略的限制,但是HTML本身是不会受到同源策略的影响的,因此我们可以用img标签来发送一个GET请求,设置
display:none
来隐藏元素,可以通过监听onload和onerror事件来判断请求是否发送成功。利用img实现跨域要特别注意一点,图片是能够被缓存的,因此需要在图片的url后加上一个时间戳,例如:
img.src = url + '?t=' + new Date().getTime();
这种方法的缺点很明显,只能使用GET方法,且无法获取响应内容。
本地服务器代理
这是开发环境中最常用的一种跨域方法,在webpack或vite中进行相应的配置,然后由nodejs开发服务器代理请求转发给目标服务器,由于同源策略只在浏览器中起作用,从而绕开了同源策略的限制,开发服务器得到响应后再返回给浏览器。
// 以vite为例 export default defineConfig({ server: { proxy: { // 字符串简写写法 '/foo': 'http://localhost:4567', // 选项写法 '/api': { target: 'http://jsonplaceholder.typicode.com', changeOrigin: true, rewrite: (path) => path.replace(/^\/api/, '') }, // 正则表达式写法 '^/fallback/.*': { target: 'http://jsonplaceholder.typicode.com', changeOrigin: true, rewrite: (path) => path.replace(/^\/fallback/, '') }, // 使用 proxy 实例 '/api': { target: 'http://jsonplaceholder.typicode.com', changeOrigin: true, configure: (proxy, options) => { // proxy 是 'http-proxy' 的实例 } } } } })
反向代理
除了正向代理外,反向代理也可以解决跨域限制。反向代理解决跨域的思路是有两种
- 对反向代理服务器使用CORS,这种方法本质上仍然是CORS方法,但是它的意义在于设置好反向代理的跨域资源共享(CORS)后,所有目标服务器不用再做任何处理,只需要设置好反向代理即可。
- 利用反向代理将后端接口代理到与网站的同源路径下。
JSONP
JSONP全称是json with Padding,这是一种比较老的解决方案。
jsonp的思路是
- 首先定义好回调函数,后端的数据作为回调函数的参数。
- 动态创建script标签请求后端接口
- 后端返回回调执行语句,并将数据作为参数传过来。
// 服务器 nodejs const http = require("http"); const server = http .createServer((req, res) => { const data = 123 // 取数据 res.end(`cb(${data})`); // 数据作为参数 }) .listen("3333"); // 浏览器 <script> // 定义好回调函数,后端数据会当做参数传入 function cb(data) { console.log(`data是${data}`); } const scr = document.createElement("script"); scr.setAttribute("src", "http://localhost:3333"); document.documentElement.appendChild(scr); </script>
jsonp解决跨域的本质是利用了
script
等html标签所发送的http请求不受到同源策略的限制这一特性,script请求的内容会作为JavaScript代码直接执行,因此只需要和后端约定好回调函数名、要传回的数据等就可以使用jsonp完成跨域。jsonp的优点是兼容性好,缺点也很明显:使用起来繁琐;只支持get方法;不安全,容易被跨站脚本攻击等。
跨源资源共享(CORS)
CORS是生产环境下常用的一种解决跨域的方法,它使得
XMLHttpRequest
和 Fetch API
可以跳过同源策略的限制。CORS分为两种情况:简单请求和非简单请求。
简单请求
满足以下所有条件的才是简单请求,否则是非简单请求。
- 除了被用户代理自动设置的首部字段(例如
Connection
,User-Agent
)和在 Fetch 规范中定义为 禁用首部名称 的其他首部,允许人为设置的字段为 Fetch 规范定义的 对 CORS 安全的首部字段集合。该集合为:
Content-Type
的值仅限于下列三者之一:text/plain
multipart/form-data
application/x-www-form-urlencoded
- 请求中的任意
XMLHttpRequest
对象均没有注册任何事件监听器;XMLHttpRequest
对象可以使用XMLHttpRequest.upload
属性访问。
- 请求中没有使用
ReadableStream
对象。
简单请求仅使用
Origin
和Access-Control-Allow-Origin
字段进行控制,浏览器发送请求时会在请求头加上origin
字段,该字段的值是当前网站的源,得到服务器响应后检查相应头的Access-Control-Allow-Origin
字段。// 服务器 const http = require("http"); const server = http .createServer((req, res) => { res.setHeader("Access-Control-Allow-Origin", "*"); res.end("123"); }) .listen("3333", () => { console.log("port 3333"); }); // 客户端 <script> const xhr = new XMLHttpRequest(); const url = "http://localhost:3333/"; xhr.open("GET", url); xhr.send(); </script>
非简单请求
简单请求的判定条件比较苛刻,通常使用
XMLHttpRequest
和 Fetch
发送的http请求是非简单请求。与简单请求不同,非简单请求会先发送一个
options
方法的预检请求到服务器,以获知服务器是否允许该实际请求。"预检请求“的使用,可以避免跨域请求对服务器的用户数据产生未预期的影响。预检请求会携带三个请求头字段
Origin
、Access-Control-Request-Headers
、Access-Control-Request-Method: GET
。响应头也有对应的三个字段Access-Control-Allow-Headers
、Access-Control-Allow-Methods
、Access-Control-Allow-Origin
。只有三者都对应,才能实现跨域。// 客户端 <script> const xhr = new XMLHttpRequest(); const url = "http://localhost:3333/"; xhr.open("GET", url); xhr.setRequestHeader("Authorization", "123"); xhr.setRequestHeader("a", "111"); xhr.send(); </script> // 服务器 const http = require("http"); const server = http .createServer((req, res) => { res.setHeader("Access-Control-Allow-Origin", "*"); res.setHeader("Access-Control-Allow-Methods", " POST, GET, OPTIONS"); res.setHeader("Access-Control-Allow-Headers", "a,authorization"); res.end("123"); }) .listen("3333", () => { console.log("port 3333"); });
除了上面6个首部字段外,响应头还可以设置
Access-Control-Max-Age
,例如Access-Control-Max-Age: 86400
就表示在86400s内无需再发送预检请求。身份凭证
默认情况下跨域的fetch和xhr不会携带身份凭证(通常是cookie),如果要携带身份凭证则需要进行相应的设置,例如xhr要设置
xhr.withCredentials=true
,并且响应头还得满足:Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin
的值不能是*
如果是响应携带身份凭证,则
Access-Control-Allow-Headers
、Access-Control-Allow-Methods
、Access-Control-Allow-Origin
的值都不能是’*’。// 客户端 <script> const xhr = new XMLHttpRequest(); const url = "http://localhost:3333/"; xhr.open("GET", url); xhr.setRequestHeader("Authorization", "123"); xhr.withCredentials = true; xhr.send(); </script> // 服务器 const http = require("http"); const server = http .createServer((req, res) => { res.setHeader("Access-Control-Allow-Origin", "http://127.0.0.1:5500"); res.setHeader("Access-Control-Allow-Methods", " POST, GET, OPTIONS"); res.setHeader("Access-Control-Allow-Headers", "a,authorization"); res.setHeader("Access-Control-Allow-Credentials", "true"); res.end("123"); }) .listen("3333", () => { console.log("port 3333"); });
总结
请求头 | 响应头 | 作用 |
origin | access-control-allow-origin | 允许的源 |
access-control-request-headers | access-control-allow-headers | 允许的请求头 |
access-control-request-method | access-control-allow-method | 允许的方法 |
ㅤ | access-control-allow-credentials | 是否可携带凭证 |
ㅤ | access-control-max-age | 有效时间,在有效时间内不会再发送预检请求 |
document.domain
当同一域名下的两个子域名需要实现跨域时,可以将
document.domain
设置成父域名,然后就可以实现cookie共享。注意: 目前几乎所有主流浏览器都支持此特性,但是未来可能会被废弃。
PostMessage
window.postMessage() 是一种可以安全地实现跨源通信的方法。
使用PostMessage首先需要获取到另一个窗口的引用,然后调用
otherWindow.postMessage(message, targetOrigin, [transfer]);
otherWindow
其他窗口的一个引用,比如 iframe 的 contentWindow 属性、执行window.open返回的窗口对象、或者是命名过或数值索引的window.frames。
message
targetOrigin
通过窗口的 origin 属性来指定哪些窗口能接收到消息事件,其值可以是字符串"*"(表示无限制)或者一个 URI。在发送消息的时候,如果目标窗口的协议、主机地址或端口这三者的任意一项不匹配 targetOrigin 提供的值,那么消息就不会被发送;只有三者完全匹配,消息才会被发送。这个机制用来控制消息可以发送到哪些窗口;防止数据被恶意的第三方截获。
transfer
可选是一串和 message 同时传递的
Transferable
对象。这些对象的所有权将被转移给消息的接收方,而发送一方将不再保有所有权。而对于接收方,则需要通过
window.addEventListener('message',function(res){})
来接收消息。其中res参数有三个很重要的属性。origin
调用
postMessage
时消息发送方窗口的 origin,这个字符串由 协议、“://“、域名、“ : 端口号”拼接而成。接收方跨域通过该属性避免接收不明网站发送的信息。data
postMessage发送的消息。
source
对发送消息的窗口对象的引用; 您可以使用此来在具有不同 origin 的两个窗口之间建立双向通信。
示例
// http://127.0.0.1:5500/a1.html <script> const w1 = window.open("http://127.0.0.1:5500/a2.html"); setTimeout(() => { // 避免a2页面还没加载完成就发送消息 w1.postMessage("123", "http://127.0.0.1:5500/a2.html"); }, 1000); </script> // http://127.0.0.1:5500/a2.html <script> window.addEventListener( "message", function (res) { // 确认发送方身份 if (res.origin === "http://127.0.0.1:5500") { } }, false ); </script>
注意 如果不需要接收message消息,则应该直接避免监听message消息,如果需要接收message消息,则应当始终使用 origin 和 source 属性验证发件人的身份,防止跨站脚本攻击。