【笔记】遇见跨域

2020-10-17 15:38:59 蜻蜓队长

你们都不看的总集篇: 从零开始的大前端筑基之旅(深入浅出,持续更新~)
万字长目录,觉得不错就顺手点个赞吧~

不管你有没有遇到过,但我相信你肯定听说过跨域。实际项目中,涉及到跨域的问题非常多。
下面分文档间跨域通信前后台跨域通信分别讲解一下几种常用方法。

如果左边没到20个赞,麻烦好心点一下好么~

什么是跨域

简单来讲,当一个域下的文档或执行脚本,想要获取另一个域下的资源或者与另一个域进行通信,就会发生跨域。例如、

要了解跨域,首先要了解什么是域。

什么是域

与域名不同,跨域的域是由浏览器同源策略限定的一种概念。

同源策略/SOP(Same origin policy)是一种约定,由 Netscape 公司1995年引入浏览器,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,浏览器很容易受到 XSS、CSFR 等攻击。

同源指的是资源或目标地址的 协议、域名、端口 三者相同,即便是不同的域名指向同一个ip地址,也不同源。

当一个请求url的协议、域名、端口三者之间任意一个与当前页面url不同即为跨域

当前页面url 被请求页面url 是否跨域 原因
www.test.com/ www.test.com/index.html 同源(协议、域名、端口号相同
www.test.com/ www.test.com/index.html 跨域 协议不同(http/https)
www.test.com/ www.baidu.com/ 跨域 主域名不同(test/baidu)
www.test.com/ blog.test.com/ 跨域 子域名不同(www/blog)
www.test.com:8080/ www.test.com:7001/ 跨域 端口号不同(8080/7001)

非同源限制

  1. 无法读取非同源网页的 Cookie、LocalStorage 和 IndexedDB
  2. 无法接触非同源网页的 DOM
  3. 无法向非同源地址发送 AJAX 请求

如何解决跨域问题

对于文档之间的通信

1. 设置document.domain解决无法读取非同源网页的 Cookie问题

因为浏览器是通过document.domain属性来检查两个页面是否同源,因此只要通过设置相同的document.domain,两个页面就可以共享Cookie(此方案仅限主域相同,子域不同的跨域应用场景。)

// 两个页面都设置
document.domain = 'test.com';
复制代码

ps: 种cookie的时候,基本都是种在主域名下,保证同域名的页面可以共享token等信息,同时,打开新的同域页面会自动附带当前域名下cookie

2. 跨文档通信 API:window.postMessage()

该方法可以安全地实现跨源通信,只要正确的使用,这种方法就很安全。

它可用于解决以下方面的问题:

  • 页面和其打开的新窗口的数据传递
  • 多窗口之间消息传递
  • 页面与嵌套的iframe消息传递
  • 上面三个场景的跨域数据传递

一个窗口可以获得对另一个窗口的引用(比如 targetWindow = window.opener),然后在窗口上调用 targetWindow.postMessage() 方法分发一个 MessageEvent 消息。接收消息的窗口可以根据需要自由处理此事件。

语法

otherWindow.postMessage(message, targetOrigin, [transfer]);
复制代码

其中

  • otherWindow 其他窗口的一个引用,比如iframe的contentWindow属性、执行window.open返回的窗口对象、或者是命名过或数值索引的window.frames。

  • message 将要发送到其他 window的数据。它将会被结构化克隆算法序列化。这意味着你可以不受什么限制的将数据对象安全的传送给目标窗口而无需自己序列化。[1]

  • targetOrigin 通过窗口的origin属性来指定哪些窗口能接收到消息事件,其值可以是字符串"*"(表示无限制)或者一个URI。在发送消息的时候,如果目标窗口的协议、主机地址或端口这三者的任意一项不匹配targetOrigin提供的值,那么消息就不会被发送;

  • transfer 可选 是一串和message 同时传递的 Transferable 对象. 这些对象的所有权将被转移给消息的接收方,而发送一方将不再保有所有权。

/*
 * A窗口的域名是<http://example.com:8080>,以下是A窗口的script标签下的代码:
 */

var popup = window.open(...popup details...);

// 这行语句没有发送信息出去,因为targetOrigin设置不对
popup.postMessage("The user is 'bob' and the password is 'secret'",
                  "https://secure.example.net");

// 这条语句会成功添加message到发送队列中去(targetOrigin设置对了)
popup.postMessage("hello there!", "http://example.org");

// 接收函数,处理子页面传递的消息
function receiveMessage(event)
{
  // 对信源做判断,如果不是可信的,则忽略,这点非常重要
  if (event.origin !== "http://example.org")
    return;

  // event.source 是我们通过window.open打开的弹出页面 popup
  // event.data 是 popup发送给当前页面的消息 "hi there yourself!  the secret response is: rheeeeet!"
}
// 添加对 message 事件的监听
window.addEventListener("message", receiveMessage, false);
复制代码
/*
 * 弹出页 popup 域名是<http://example.org>,以下是script标签中的代码:
 */

//当A页面postMessage被调用后,这个function被addEventListener调用
function receiveMessage(event)
{
  // 判断信源
  if (event.origin !== "http://example.com:8080")
    return;

  // event.source 就当前弹出页的来源页面
  // event.data 是 "hello there!"

  // 假设你已经验证了所受到信息的origin (任何时候你都应该这样做), 一个很方便的方式就是把event.source
  // 作为回信的对象,并且把event.origin作为targetOrigin
  event.source.postMessage("hi there yourself!  the secret response " +
                           "is: rheeeeet!",
                           event.origin);
}

window.addEventListener("message", receiveMessage, false);
复制代码

提示

  1. 如果是通过iframe嵌入的页面,获取iframe元素后,通过iframe.contentWindow可以向子页面postMessage
  2. 对于异步调用,我们可以在子页面中使用window.top来获得赴页面的引用

注意

  1. 无法检查origin和source属性会导致跨站点脚本攻击。
  2. 与任何异步调度的脚本(超时,用户生成的事件)一样,postMessage的调用者不可能检测到侦听由postMessage发送的事件的事件处理程序何时抛出异常。
  3. 分派事件的origin属性的值不受调用窗口中document.domain的当前值的影响。

对于网站前后台的通信

3. JSONP跨域

<script> 标签的 src 属性并不被同源策略所约束,所以可以获取任何服务器上脚本并执行它。
JSONP的核心思想就是通过添加一个<script>元素,向服务器请求 JSON 数据,服务器收到请求后,将数据放在一个指定名字的回调函数的参数位置传回来。

<script src="http://test.com/data.php?callback=dosomething"></script>
// 向服务器test.com发出请求,该请求的查询字符串有一个callback参数,用来指定回调函数的名字
 
// 处理服务器返回回调函数的数据
<script type="text/javascript">
    function dosomething(res){
        // 处理获得的数据
        console.log(res.data)
    }
</script>
复制代码

jquery也支持jsonp的实现方式

$.ajax({
    url: 'http://www.test.com:8080/login',
    type: 'get',
    dataType: 'jsonp',  // 请求方式为jsonp
    jsonpCallback: "handleCallback",    // 自定义回调函数名
    data: {}
});
复制代码

最后,JSONP 只支持 GET 请求。JSONP的优势在于支持老式浏览器,以及可以向不支持CORS的网站请求数据。

4. CORS跨域

CORS 是跨域资源分享(Cross-Origin Resource Sharing)的缩写。它是 W3C 标准,属于跨源 AJAX 请求的根本解决方法。它允许浏览器向跨源服务器,发出 XMLHttpRequest 请求,从而克服了 AJAX 只能同源使用的限制。

整个CORS通信过程,都是浏览器自动完成,不需要用户参与。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感知。

1. 普通跨域请求:只需服务器端设置Access-Control-Allow-Origin
2. 带cookie跨域请求:前后端都需要进行设置
复制代码

浏览器会将 CORS 请求分成两类,简单请求(simple request)和非简单请求(not-so-simple request),浏览器对这两种请求的处理,是不一样的。

简单请求

满足以下两个条件的请求,就是简单请求

  1. 请求方法是 HEAD、GET、POST 三种方法之一
  2. HTTP的头信息不超出以下几种字段
    • Accept
    • Accept-Language
    • Content-Language
    • Last-Event-ID
    • Content-Type(只限于三个值application/x-www-form-urlencodedmultipart/form-datatext/plain

浏览器会在这个请求的头信息中,自动添加一个 Origin 字段来说明本次请求的来源(协议 + 域名 + 端口),而后服务器会根据这个值,决定是否同意这次请求。

如果Origin指定的源,不在许可范围内,服务器会返回一个正常的HTTP回应。 浏览器发现,这个回应的头信息没有包含Access-Control-Allow-Origin字段,就知道出错了,从而抛出一个错误,被XMLHttpRequest的onerror回调函数捕获。

非简单请求

除了上面的简单请求,剩下的都是非简单请求。

对于非简单请求,浏览器会在正式通信之前,做一次查询请求,叫预检请求(preflight),也叫 OPTIONS 请求,因为它使用的请求方式是 OPTIONS。

在OPTIONS 请求里,头信息除了有表明来源的 Origin 字段外,还会有一个Access-Control-Request-Method 字段和 Access-Control-Request-Headers 字段,它们分别表明了本次 CORS 请求用到的 HTTP 请求方法和请求会额外发送的头信息字段

只有得到肯定答复,浏览器才会发出正式的 XMLHttpRequest 请求,否则就会报跨域错误

比如,有时候我们会手动将token放进header头信息里

Origin: http://example.org
Access-Control-Request-Headers: content-type,token
Access-Control-Request-Method: GET
复制代码

如果Origin指定的域名在许可范围内,服务器返回的响应,会多出几个头信息字段。

   Access-Control-Allow-Origin: http://example.org
   Access-Control-Allow-Credentials: true
   Access-Control-Expose-Headers: FooBar
   Access-Control-Max-Age: 1728000
复制代码
  • Access-Control-Allow-Origin :该字段是必须的。它的值要么是请求时Origin字段的值,要么是一个*,表示接受任意域名的请求
  • Access-Control-Allow-Credentials: 该字段可选。它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。设为true,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为true,如果服务器不要浏览器发送Cookie,删除该字段即可。
  • Access-Control-Expose-Headers:该字段可选。CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定
  • Access-Control-Max-Age: 该字段可选,用来指定本次预检请求的有效期,单位为秒。在此期间,不用发出另一条预检请求。

withCredentials

CORS请求默认不发送Cookie和HTTP认证信息。如果要把Cookie发到服务器,一方面要服务器同意,指定Access-Control-Allow-Credentials字段。另一方面,开发者必须在AJAX请求中打开withCredentials属性。

如要发送 Cookie,Access-Control-Allow-Origin 字段就不能设为星号,必须指定明确的、与请求网页一致的域名,同时,Cookie 依然遵循同源政策,只有用服务器域名设置的 Cookie 才会上传。

WebSocket协议跨域

WebSocket protocol是HTML5一种新的协议。它实现了浏览器与服务器全双工通信,同时允许跨域通讯,是server push技术的一种很好的实现。

原生WebSocket API使用起来不太方便,推荐使用Socket.io,它很好地封装了webSocket接口,提供了更简单、灵活的接口,也对不支持webSocket的浏览器提供了向下兼容。

nginx代理跨域

同源策略是浏览器的安全策略,不是HTTP协议的一部分。服务器端调用HTTP接口只是使用HTTP协议,不会执行JS脚本,不需要同源策略,也就不存在跨越问题。

通过nginx配置一个代理服务器(域名与网站域名相同,端口不同)做跳板机,反向代理访问后台接口,并且可以顺便修改cookie中domain信息,方便当前域cookie写入,实现跨域登录。

假定当前页面在http://www.test.com下,跨域请求http://www.test1.com下的接口

# Nginx代理服务器
server {
  listen       80;
  server_name  http://www.test.com;

  location /api {
    # 反向代理地址
    proxy_pass  http://www.test1.com;  
    # 修改Cookie中域名
    proxy_cookie_domain http://www.test1.com http://www.test.com; 
    index  index.html index.htm;
		
    # 前端跨域携带了Cookie,所以Allow-Origin配置不可为*
    add_header Access-Control-Allow-Origin http://www.test.com;  
    add_header Access-Control-Allow-Credentials true;
  }
}
复制代码

node代理跨域

本质上是通过启一个代理服务器,实现数据的转发,也可以通过设置cookieDomainRewrite参数修改响应头中cookie中域名,实现当前域的cookie写入,方便接口登录认证。 这里使用 express + http-proxy-middleware 来搭建一个代理服务器, webpack-dev-server 里就是使用的它。

let express = require('express')
let proxy = require('http-proxy-middleware')
let app = express()

app.use('/api', proxy({
    // 代理跨域目标接口
    target: 'http://www.test1.com',
    changeOrigin: true,

    // 修改响应头信息,实现跨域并允许带cookie
    onProxyRes: function(proxyRes, req, res) {
        res.header('Access-Control-Allow-Origin', 'http://www.test.com')
        res.header('Access-Control-Allow-Credentials', 'true')
    },

    // 修改响应信息中的cookie域名,为false时,表示不修改
    cookieDomainRewrite: 'http://www.test.com'
}))

app.listen(3000)
复制代码

如果你收获了新知识,请点个赞告诉我~

本文收纳于: 从零开始的大前端筑基之旅(深入浅出,持续更新~)

推荐阅读:

参考文档

  1. 什么是跨域?跨域解决方法
  2. 详解跨域(最全的解决方案)
  3. 你真的了解跨域吗

以上内容来自于网络,如有侵权联系即删除
相关文章

上一篇: 前端必看的微前端

下一篇: Flutter Hooks 使用及原理

客服紫薇:15852074331
在线咨询
客户经理