JS系列之 前端跨域问题

406 阅读9分钟
为了记录自己所学知识的过程,并且决定通过亲自记录的方式积累下来。
如今网上的信息量如此之大,经常看过什么知识但之后回想,又常常想不清细节和出处。
那就像左耳进右耳出,无用功。笔记多为学习他人总结经验再自己整合,多有冒犯。

为什么会出现跨域问题?
这就要说到 浏览器的同源策略

同源策略

首先我们先了解下同源是什么

同源的定义

协议,域名,端口 三者一致就是同源

同源策略的目的

同源策略 是浏览器的安全策略,用于限制一个 origin 文档 或者它加载的脚本如何与 另一个源的资源 进行交互。它能帮助隔离潜在的恶意文档,减少可能被攻击的媒介,保护用户信息的安全,防止被恶意网站窃取数据。

同源策略是一种约定,它是浏览器最核心,也是最基本的安全功能。可以说 Web 是构建在同源策略基础之上的, 如果缺少了同源策略,则浏览器的正常功能可能都会受到影响。

同源策略 是浏览器的行为,是为了保护本地数据不被请求得到的数据污染,因此拦截的是浏览器请求得到的数据,即请求发送了,服务器响应了,但是无法被浏览器接受。

举个栗子:
当一个浏览器打开两个 tab 页,百度和谷歌。当浏览器的百度 tab 页执行一个脚本的时候会检查这个脚本是属于哪个页面的,即检查是否同源,只有和百度同源的脚本才会被执行。如果非同源,那么在请求数据时,浏览器会在控制台报出异常,提示拒绝访问。

同源策略的限制范围

共有三种行为会受到限制

  1. 无法读取非同源网页的 Cookie、LocalStorage 和 IndexedDB。
  2. 无法接触非同源网页的 DOM。
  3. 无法向非同源地址发送 AJAX 请求(可以发送,但浏览器会拒绝接受响应)。

另外,通过 JavaScript 脚本可以拿到其他窗口的 window 对象。
如果是非同源的网页,目前允许一个窗口可以接触其他网页的 window 对象有九个属性和四个方法。

  • 九个属性: window.closed | window.frames | window.length | window.location | window.opener | window.parent | window.self | window.top | window.window
  • 四个方法:window.blur() | window.close() | window.focus() | window.postMessage() 上面的九个属性之中,只有 window.location 是可读写的,其他八个全部都是只读。而且,即使是 location 对象,非同源的情况下,也只允许调用 location.replace() 方法和写入 location.href 属性。

没有同源策略的两大危险场景

浏览器是从两个方面去实现同源策略

  1. 针对接口的请求
    cookie 一般用来处理登录等场景,因为 http 连接是无状态的,cookie 可以让服务端知道是谁发出的请求。初次登录成功时,服务端验证通过会在响应头加入 Set-Cookie 字段,浏览器接收后保存,然后下次再 发送请求时,浏览器会自动将 cookie 附加在 HTTP 请求头字段 Cookie 中,这样服务端就知道这个用户已经登录过和其他信息。

假如你登录购物网站或者银行网站以后,又去浏览其他网站。如果没有同源策略限制,其他网站就可以通过 截取请求中的 cookie 获取你的隐私信息,相当于登录你的账户,就可以做坏事了嘿嘿嘿...

  1. 针对 DOM 的查询 非法网站可以模仿银行网站,如果你不小心输入,通过获取页面 DOM 元素例如用户名和密码的 input标签。 就可以窃取你的账户和密码

注意,同源策略可以规避一些危险,但也只是提高了攻击的成本,并不是绝对的安全
就像学校围起的一圈栅栏,一般人不会去翻,真想搞事也能翻过去

跨域

顾名思义,跨域指的就是非同源资源之间尝试进行交互通信,但是由于受到浏览器同源策略的限制,无法正常进行交互通信。

非同源请求和服务端设置 cors 限制会造成跨域。

一、跨域的解决方案

1. JSONP

jsonp 就是利用浏览器对 <script> 标签没有跨域限制的漏洞。通过 <script> 标签指向一个需要访问的地址并提供一个回调函数来接收数据。

jsonp 实现简单,没有兼容问题,但是只能发送 GET 请求。

面试官:问什么 JSONP 只能发生 GET 请求

首先介绍 JSONP 原理,浏览器对 script 标签没有跨域限制。所以可以通过创建一个 script 标签,将 src 设置为目标请求,插入到 dom 中,服务器接受该请求并返回数据,数据通常被包裹在回调钩子中。本质上 script 加载资源是 GET 请求,看过支持 post 请求的 script 么?

JSONP 实现如下:

  1. 网页添加一个 <script> 元素,向服务器请求一个脚本,这不受同源政策限制,可以跨域请求。 请求的脚本网址有一个callback参数(?callback=bar),用来告诉服务器,客户端的回调函数名称(bar)。
<script src="http://api.foo.com?callback=bar"></script>
  1. 服务器收到请求后,将 JSON 数据放在函数里面当参数,作为字符串返回(bar({...}))。
  2. 这样,只要客户端定义了bar()函数,就能在该函数体内,拿到服务器返回的 JSON 数据。

举个栗子:

// 网页动态插入<script>元素,像跨域网址发出请求
function addScriptTag(src) {
  var script = document.createElement('script');
  script.setAttribute('type', 'text/javascript');
  script.src = src;
  document.body.appendChild(script);
}

window.onload = function () {
  addScriptTag('http://example.com/ip?callback=foo');
}

function foo(data) {
  console.log('Your public IP address is: ' + data.ip);
};

上面代码通过动态添加 <script> 元素,向服务器 example.com 发出请求。callback 参数,用来指定回调函数的名字,这是必需的。
服务器收到这个请求以后,会将数据放在回调函数的参数位置返回。

foo({
  'ip': '8.8.8.8'
});

由于 <script> 元素请求的脚本,直接作为代码运行。这时,只要浏览器定义了 foo 函数,该函数就会立即调用。作为参数的 JSON 数据被视为 JavaScript 对象,而不是字符串,因此避免了使用 JSON.parse 的步骤。

2. CORS

CORS 是一个 W3C 标准,全称是“跨域资源共享”(Cross-origin resource sharing)。它允许浏览器向跨域的服务器,发出 XMLHttpRequest 请求,从而克服了 AJAX 只能同源使用的限制。AJAX 本身不支持跨域,现在可以配合 CORS 实现跨域了。 CORS 需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能。

浏览器会自动进行 CORS 通信,实现 CORS 通信的关键是后端。只要后端实现了 CORS,就实现了跨域。

简单请求 & 非简单请求

CORS 请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)。 同时满足以下两个条件就是简单请求,其余是非简单请求

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

简单请求就是简单的 HTTP 方法与简单的 HTTP 头信息的结合。
这样划分的原因是,表单在历史上一直可以跨域发出请求。简单请求就是表单请求,浏览器沿袭了传统的处理方式,不把行为复杂化,否则开发者可能转而使用表单,规避 CORS 的限制。对于非简单请求,浏览器会采用新的处理方式。

非简单请求比简单请求多了一步,预检请求。

浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些 HTTP 方法和头信息字段。只有得到肯定答复,浏览器才会发出正式的 XMLHttpRequest 请求,否则就报错。
这是为了防止这些新增的请求,对传统的没有 CORS 支持的服务器形成压力,给服务器一个提前拒绝的机会,这样可以防止服务器收到大量 DELETE 和 PUT 请求,这些传统的表单不可能跨域发出的请求。

总结: 用 CORS 实现跨域时,简单请求中,后端需要设置 Access-Control-Allow-Origin 指定允许跨域的地址。如果要携带 cookie , 则前后端都要设置 credentials。 前端初始化 axios 请求实例时,设置 withCredentials: true;后端设置 Access-Control-Allow-Credentials 为 true。
复杂请求时,前端需要设置 Access-Control-Request-Method 指定是哪种复杂请求和 Access-Control-Request-Headers 添加额外发送的头部字段;后端对应的就要设置 Access-Control-Allow-MethodAccess-Control-Allow-Headers 允许前端复杂请求。预检请求成功的话,服务器会返回 204,真正发出请求成功响应时,才是200。拒绝预检请求时,服务器返回一个正常的 HTTP 响应,带有拒绝信息,但是没有任何 CORS 字段,会触发请求错误回调。服务端还可以设置预检请求有效期 Access-Control-Max-Age

jsonp 和 cors 比较

  1. CORS与JSONP的使用目的相同,但是比JSONP更强大。
  2. JSONP只支持 GET 请求,CORS 支持所有类型的 HTTP 请求。JSONP 的优势在于支持老式浏览器,以及可以向不支持 CORS 的网站请求数据。

3. document.domain

该方式只能用于二级域名相同的情况下,比如 a.test.com 和 b.test.com 适用于该方式。
只需要给页面添加 document.domain = 'test.com' 表示二级域名都相同就可以实现跨域

A 网页通过脚本设置一个 Cookie。

document.cookie = "test1=hello";

B 网页就可以读到这个 Cookie。

var allCookie = document.cookie;

注意,这种方法只适用于 Cookie 和 iframe 窗口,LocalStorage 和 IndexedDB 无法通过这种方法,规避同源政策

3.其他

  • postMessage
  • iframe 和多窗口通信
  • nodejs 中间件代理跨域
  • WebSocket 协议跨域
  • link, script, img, background: url(), @font-face() 等均不受跨域限制

二、JSONP CORS 对比

  1. JSONP 最大优势是对浏览器的支持性好;虽然目前主流浏览器支持 CORS,但 IE10 以下不支持。

  2. JSONP 只能用于获取资源(即只读,类似于GET请求);CORS 支持所有类型的 HTTP 请求,功能完善。

  3. JSONP 没办法进行错误处理;而CORS可以通过onerror事件监听错误,并且浏览器控制台会看到报错信息,利于排查。

  4. JSONP 只会发一次请求;而对于复杂请求,CORS会发两次请求。

  5. JSONP 不是跨域规范,存在很明显的安全问题:callback参数注入和资源访问授权设置。CORS 是跨域规范,在资源访问授权方面进行了限制(Access-Control-Allow-Origin),而且标准浏览器都做了安全限制,比如拒绝手动设置 origin 字段,相对来说是安全了一点。。

参考文章

不要再问我跨域问题了
跨域方案实践
MDN
阮大神文章