同源策略

同源策略是一个重要的安全策略,它用于限制一个origin的文档或者它加载的脚本如何能与另一个源的资源进行交互。它能帮助阻隔恶意文档,减少可能被攻击的媒介。

同源:两个 URL 的协议、域名、端口都相同的话,就是同源。

同源策略是一个保障我们信息的安全策略,目的是为了保证用户信息的安全,防止恶意的网站窃取数据。

同源策略的限制

1995年,同源政策由 Netscape 公司引入浏览器。目前,所有浏览器都实行这个政策。
最初,它的含义是指,A 网页设置的 Cookie,B 网页不能打开,除非这两个网页“同源”。

目前,同源策略共有三种行为受到限制:

  1. 无法读取非同源网页的 Cookie、LocalStorage 和 IndexedDB
  2. 无法接触非同源网页的 DOM
  3. 无法向非同源地址发送 Ajax 请求(可以发送,但浏览器会拦截响应不会给你数据)

跨域解决方案

JSONP

浏览器能够发送 HTTP请求的方式:

  1. 直接在浏览器的地址栏输入地址
  2. 利用 location.href 直接改变 URL 进行跳转
  3. 带有 srchref 等属性的元素
  4. 表单的提交
  5. Ajax

除了 Ajax,其他方式都不会受同源策略的影响,但是只有第三种方式能够获取数据不进行跳转。

原理

利用 script 标签向后端发送请求,后端将数据放到 JavaScript 段中返回给我们。

实现思路:

  1. 全局生成一个函数(例如:cb
  2. 在页面中动态插入一个 script 标签,src 为请求的目标地址
  3. 服务器返回的数据为 cb(data) (后端可以通过 query 得知函数名,也可以固定)
  4. 浏览器会立即执行请求回来的 js 脚本,通过全局函数获取到后端返回的数据
1
2
3
4
5
6
7
8
9
10
function jsonp(url) {
const script = document.createElement('script');
script.setAttribute("type","text/javascript");
script.src = url;
document.body.appendChild(script);
}

function cb(data) {
console.log(data);
}

特点

  • 使用简便,不存在兼容性问题
  • 只能完成GET请求,传递数据只能通过 query
  • 存在安全性问题,如果其他域不安全,很可能会在响应中夹带一些恶意代码
  • 需要后端进行配合
  • 会打乱服务器的消息格式,跨域时响应一段 js 代码,非跨域时又需要响应 json 格式

CORS

跨源资源共享 (Cross-origin resource sharing, CORS)是一个 W3C 标准,其思想是使用自定义的HTTP头部让浏览器与服务器进行沟通,从而决定请求或响应是应该成功,还是应该失败。

整个CORS通信过程,都是浏览器自动完成,对于前端来说,CORS通信与同源的Ajax通信没有差别,代码完全一样。

实现 CORS 通信的关键是服务器。简单来说:只要服务器允许,浏览器就可以访问该资源。

CORS 将请求分为简单请求和非简单请求,针对不同的请求,CORS规定了三种不同的场景:

  • 简单请求
  • 需要预检的请求
  • 附带身份凭证的请求

简单请求

定义

当请求同时满足以下条件时,浏览器会认为它是一个简单请求:

  1. 请求方法是以下三种方法之一
    • GET
    • POST
    • HEAD
  2. 请求头仅包含安全的字段,常见的安全字段如下
    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type
    • DPR
    • Downlink
    • Save-Data
    • Viewport-Width
    • Width
  3. 请求头如果包含 Content-Type 仅限下面的值之一
  • text/plain
  • multipart/form-data
  • application/x-www-form-urlencoded

基本流程

当浏览器判定某个Ajax 跨域请求是简单请求时,会发生以下的事情:

  1. 在请求头中自动添加 Origin 字段

    1
    2
    3
    4
    5
    GET /resources/public-data/ HTTP/1.1
    User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.1b3pre) Gecko/20081130 Minefield/3.1b3pre
    ....
    Connection: keep-alive
    Origin: http://foo.example

    Origin 字段的目的是告诉服务器是哪个源地址在跨域请求

  2. 服务器的响应中添加 Access-Control-Allow-Origin字段

    当服务器收到请求后,如果允许该请求跨域访问,需要在响应头中添加 Access-Control-Allow-Origin 字段

    该字段的值可以是:

    • *:任何源都允许
    • http://my.com:具体的源

    实际上,这两个值对于客户端而言都一样,因为客户端才不会管其他源服务器允不允许,只关心自己是否被允许

    1
    2
    3
    4
    5
    6
    HTTP/1.1 200 OK
    Date: Tue, 21 Apr 2020 08:03:35 GMT
    ...
    Access-Control-Allow-Origin: *

    ...
  3. 浏览器根据 Access-Control-Allow-Origin 字段的值决定是否将数据交给 js

CROS

需要预检请求

简单的请求对服务器的威胁不大,所以允许使用上述的简单交互即可完成。

但是对于非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求。

预检请求

预检请求的目的是:浏览器询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的请求,否则就报错。

预检请求的特征

  • 请求方法为 OPTIONS

  • 没有请求体

  • 请求头中包含

    • Origin:请求的源,和简单请求的含义一致
    • Access-Control-Request-Method:后续的真实请求将使用的请求方法
    • Access-Control-Request-Headers:后续的真实请求会改动的请求头

这个请求对于我们来说是不可见的,我们可以自己写一个服务器测试,就可以发现来了先来了一个 OPTIONS 的预检请求

服务器应该如何处理这个预检请求呢

服务器收到预检请求后,可以检查预检请求中包含的信息,响应的消息体中应该包含下面这些头部:

  • Access-Control-Allow-Origin:和简单请求一样,表示允许的源
  • Access-Control-Allow-Methods:表示允许的后续真实的请求方法
  • Access-Control-Allow-Headers:表示允许改动的请求头(非简单请求之外的)
  • Access-Control-Max-Age:告诉浏览器,多少秒内,对于同样的请求源、方法、头,都不需要再发送预检请求了

请求步骤

经历过预检请求并且服务器是允许访问,则后面的步骤和简单请求一样。

所以对于非简单请求,流程就是:

  1. 浏览器发送预检请求,询问服务器是否允许
  2. 服务器允许
  3. 浏览器发送真实请求
  4. 服务器响应真实请求

预检请求的CROS

附带身份凭证

默认情况下,Ajax 的跨域请求并不会附带cookie,这样一来,某些需要权限的操作就无法进行。

不过可以通过简单的配置就可以实现附带 cookie:

1
2
3
4
5
6
7
8
// xhr
xhr.withCredentials = true;

// fetch
fetch(url, { credentials: "include" });

// axios
axios.withCredentials = true;

这样一来,该跨域的 Ajax 请求就是一个附带身份凭证的请求

当一个请求需要附带cookie时,无论它是发送请求的阶段,还是预检请求的阶段,都会在请求头中添加 cookie 字段

服务器需要在响应头中添加:Access-Control-Allow-Credentials: true / false ,若服务器没有添加该字段,浏览器视为跨域被拒绝。

注意

对于附带身份凭证的请求,服务器不得设置 Access-Control-Allow-Origin: *

所以不推荐 Access-Control-Allow-Origin: *

请求头的获取

在跨域访问时,js 只能拿到一些最基本的响应头。

如:Cache-ControlContent-LanguageContent-TypeExpiresLast-ModifiedPragma

如果要访问其他头,则需要服务器通过 Access-Control-Expose-Headers 把允许 js 访问的头放入白名单,例如:

1
Access-Control-Expose-Headers: authorization, a, b

特点

  • 官方标准
  • CORS 通信与同源的 Ajax 通信没有差别,代码完全一样,容易维护
  • 支持所有类型的 HTTP 请求
  • 存在兼容性问题,特别是 IE10 以下的浏览器
  • 需要服务器配合

综上所述:CROS 相比于 JSONP 是一个更好的跨域解决方案。

服务器代理

跨域问题是浏览器端为了安全而存在的,对于服务器来说不存在跨域问题。

可以写一个代理服务器来实现跨域请求。

domain

通过修改 document.domain 的值来实现 iframe 跨域,但是仅适用于主域名相同,而子域名不同的情况。

不同的子域名将 document.domain 设置为相同的主域名,即可实现获取 iframe 中的数据(包括 DOM),iframe 也可以通过 parent 获取数据

postMessage

HTML5 新引进的 window.postMessage,可以使用它来向其它的 window 对象发送消息,无论这个 window 对象是否同源。

一个页面上的脚本不能直接访问另外一个页面上的方法或者变量,但是他们可以安全的通过消息传递技术交流。

调用 postMessage 方法的 window 是指要接收消息的那一个 window 对象,该方法的第一个参数 message 为要发送的消息(字符串),第二个参数 targetOrigin 用来限定接收消息的那个 window 对象所在的域,如果不限定域,可以使用通配符 *