浏览器同源策略及跨域

1,317 阅读8分钟

同源策略

一、概述

含义

最初的定义是指,A网页设置的cookie,B网页不能访问,前提是这两个网页必须是”同源“。

同源”指的是:

  • 协议必须相同
  • 域名必须相同
  • 端口必须相同

目的

同源策略的目的是为了保证用户信息的安全,不被恶意的网站窃取数据

限制范围

如果是非同源,共有三种行为受到限制:
  • ajax请求
  • dom无法获取
  • cookie、localStorage、indexDB无法读取
虽然这些限制是合理且必需的,但是有时候一些合理地用途也会受到限制

二、Cookie

cookie是浏览器写入的一小段信息片段,只有同源的网页才能共享。大小限制是4k。

两个页面的一级域名相同,二级域名不同时,可以通过设置相同的 document.domain,就可以共享cookie了。

document.domain = "example.com"

注意:这种方法只适合于 cookie 和 iframelocalStorage indexDB 并不适合用这种方式规避同源策略,需要使用postMessage api

服务器也可以在设置cookie的时候指定domain的字段为一级域名。

Set-Cookie: key=value; domain=.example.com; path=/
什么是一级域名和二级域名?

三、iframe

如果两个网页不同域名就无法获取DOM,典型的例子就是 iframe窗口 和window.open 打开的窗口。他们与父窗口无法通信。

比如:父窗口运行一下命令,如果iframe不是同源的,就会报错。

document.getElementById("myIFrame").contentWindow.document
// Uncaught DOMException: Blocked a frame from accessing a cross-origin frame.

contentWindow是返回当前iframe对象的window对象,可以通过window对象访问iframe内部的文档和dom

反之亦然,子窗口获取父窗口的dom也会报错。

解决办法:

如果一级域名相同,二级域名不同,可以通过设置 document.domain 属性,规避同源策略,拿到dom

如果是完全不同源的网页,有三种方式:
  • 片段标识符(fragment identifier)
  • window.name
  • 跨文档通信API

片段标识符(fragment identifier)

片段标识符,指的是链接urlhash部分

例如:"https://www.example.com/blog/2016/04/same-origin-policy.html#name=1" name=1

部分,只改变hash部分,页面不会刷新。

父窗口可以把信息写在片段标识符处,子窗口监听hash部分的变化,同时页面不会刷新。

let src = originUrl + '#' + data
document.getElementById("myIFrame").src = srcwindow.onhashchange = () => {
    console.log(location.hash)
}
注: url 的 hash 部分是带 # 号

同样的,子窗口也可以设置父窗口的hash部分。

parent.location.href = target + '#' + data

window.name

window.name 指的是当前窗口的name属性。一个窗口对应着一个name属性,也就是说当打开

一个新的窗口时,与上个窗口的name属性就无关了。

// 设置属性
window.name = data
// 获取属性
console.log(document.getElementById("myIFrame").contentWindow.name)

该属性的好处是,可以存放较长的字符串。缺点是必须监听窗口的name属性。

window.poseMessage

跨文档通信API(cross-document messaging

允许跨窗口通信,不论这两个窗口是否同源

var data = window.open('https://example2.com', 'title')
data.postMessage('hello', 'https://example2.com')

第一个参数是具体的信息内容,第二个参数是接收信息的窗口的源(origin),源指的

是:"https://www.example.com",即协议+ 域名+端口,也可以设置为 *


父窗口和子窗口都可以监听 message 事件,监听对方消息。

window.addEventListener('message', (e) => {
    console.log(e.data)
})
回调函数的参数e,有三个属性:
  • e.source:发送消息的窗口
  • e.origin:接收消息的窗口
  • e.data:消息内容


LocalStorage

通过window.postMessage,也可以读写其他窗口的LocalStorage


四、AJAX

同源策略下,只能向同源的网址发送请求。三种方式规避同源策略的影响:
  • JSONP
  • CORS
  • WebSocket

JSONP

最大的特点是:兼容老式浏览器,服务器改造小。只能处理get请求。

基本思路是:JSONP的原理就是利用<script>的标签没有跨域限制的漏洞。通过<script>标签指向一个需要访问的地址并提供一个回调函数来接收数据。或者<img>标签

关键点:

  • 服务端返回的数据不是JSON,而是JavaScript,也就是说contentTypetext/javascript,内容格式为callbackFunction(JSON)
  • callbackFunction需要注册到window对象上,因为script 加载后的执行作用域是window作用域
  • 需要考虑同时多个JSONP请求的情况,callbackFunction挂在 window上的属性名需要唯一
  • 请求结束需要移除本次请求产生的script标签和window上的回调函数

用法:

jsonp({
    url: '',
    data: {
        key: 'value'
    },
    callback: (data) => {
        console.log(data)
    }
})

实现方式:

function jsonp ({url, data, callback}) {
    // 获取head标签
    const container = document.getElementsByTagName('head')[0];

    // 动态生成回调函数名
    const cbName = `jsonp_${new Date().getTime()}`

    // 创建script标签
    const dom = document.createElement('script')

    // 设置script标签的src属性
    script.src = `${url}?${objectToQuery(data)}&callback=${cbName}`
    script.type = 'text/javascript'

    // 插入script标签
    container.appendChild(dom)
    
    // 在window上注册回调函数
    window[cnName] = function (res) {
        callback && callback(res)
        
        // 执行完清除
        container.removeChild(dom)
        delete window[cbName]
    }
    
    // 错误兼容处理
    dom.onerror = () => {
        window[cnName] = () => {
            callback && callback('some error')
            container.removeChild(dom)
            delete window[cbName]
        }
    }
}

// 拼接参数
function objectToQuery (obj) { 
    const arr = []; 
    for ( var i in o) { 
        arr.push(encodeURIComponent(i)+ '=' +encodeURIComponent(o[i])); 
    }
    return arr.join('&'); 
}

WebSocket

是一种通信协议,使用ws:// wss:// 作为协议前缀,因为此协议不实行同源策略,只要服务器支持,即可进行跨源通信。

浏览器发出 WebSocket 请求的头信息:

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
Origin: http://example.com
服务端根据 origin 字段判断是否允许此次通信。

CORS
  • Cross-Origin-ResourceSharing,跨域资源共享。跨域请求的终极解决方案。
  • 整个通信过程都是浏览器自动完成
  • 浏览器一旦发现AJAX请求跨域后,就会自动添加一些附加的头信息,有时还会多一次附加的请求。
  • 只要服务端支持CORS,就可以跨域

两种请求:

浏览器将CORS请求分为:简单请求非简单请求

同时满足以下两大条件,即为简单请求:
  • 请求方法为以下三种之一:
    • get
    • post
    • head
  • 头信息不超出以下几种字段:
    • Accept
    • Accept-Language
    • Content-Language
    • Last-Event-ID
    • Content-Type:只限于三个值:application/x-www-form-urlencoded、multipart/form-data、text/plain
由于表单一直可以跨域请求,所以兼容了form的content-type。

只要不同时满足以上的条件,即为非简单请求


简单请求
  1. 浏览器发现本次请求属于简单请求,自动在头部信息添加origin字段
  2. 服务端根据origin字段,判断是否同意本次请求
    1. 在许可范围内,响应头会新增几个头信息。
    2. Access-Control-Allow-Origin: http://api.bob.com
      Access-Control-Allow-Credentials: true
      Access-Control-Expose-Headers: FooBar
      Content-Type: text/html; charset=utf-8
    3. 不在许可范围内,返回一个正常的HTTP回应
    4. 浏览器判断响应头中有没有 Access-Control-Allow-Origin 字段,没有的话报错,XMLHttpRequest onerror 回调函数捕获。这种错误是无法通过状态码识别,因为HTTP回应的状态码可能是200
响应头字段:

Access-Control-Allow-Origin:该字段是必须的,要么是请求的origin的字段值,要么是 * 表示允许任意域名访问。

Access-Control-Allow-Credentials: 该字段是非必须的,表示是否允许发送cookie。默认情况下cookie不包含在cors的请求中。

withCredentials 属性

CORS请求默认不发送CookieHTTP认证信息。

如果要把cookie发送到服务器,一方面要服务器同意,指定Access-Control-Allow-Credentials字段。

Access-Control-Allow-Credentials:true

另一方面,需要在请求中设置 withCredentials属性

let xhr = new XMLHttpRequest();
xhr.withCredentials = true

注意:如果要发送cookie,Access-Control-Allow-Origin 就不能设置为 *,必须指定与请求源一致的域名。

非简单请求

常用情况:请求方法是PUT或者DELETE,或者 Content-Type的字段类型是application/json

  1. 非简单请求在发送cors请求之前会发送一次OPTIONS请求,称为“预检”请求。
  2. 浏览器先询问服务器,当前请求的域名是否在白名单中,以及可以使用哪些HTTP动词和头信息字段
  3. 得到肯定回复,浏览器才会发出正式的XMLHttpRequest请求

预检请求的请求头中会新增的字段:

origin:表示请求源

Access-Control-Request-Method:该字段是必须的,表示请求方法

Access-Control-Request-Headers:该字段表示自定义的请求头的信息

预检请求的回应
  1. 服务端收到预检请求后,检查以上三个字段,确认允许跨源请求,就可做出回应
  2. 如果否定了跨源请求,服务端返回一个正常的HTTP,但是没有任何相关CORS的响应头字段。浏览器抛出错误,XMLHttpRequest 的 onerror 回调函数捕获
  3. 如果同意了跨源请求,服务端会在请求头中添加一些字段

预检请求响应头中添加的字段:

Access-Control-Allow-Methods:GET,POST,PUT。该字段是必需的,表示服务端支持的所有的请求的方法

Access-Control-Allow-Headers:如果请求头中有Access-Control-Request-Headers字段,那响应头中也必需也有该字段,表示服务端支持的所有的头信息的字段

Access-Control-Allow-Credentials: 该字段是非必须的,表示是否允许发送cookie。默认情况下cookie不包含在cors的请求中

Access-Control-Max-Age:非必选,表示本次预检请求的有效时长,单位秒,在有效时长内,不发送另一条预检请求

一旦预检请求通过,以后每次的cors请求,就跟简单请求一样。

与JSONP的比较
  • JSONP 只支持get请求,但是兼容老式浏览器
  • CORS支持所有请求,但是不兼容ie10以下
参考链接:
跨域资源共享 CORS 详解(www.ruanyifeng.com/blog/2016/0…
面试中如何实现一个高质量的JSONP(juejin.cn/post/684490…
浏览器同源政策及其规避方法(www.ruanyifeng.com/blog/2016/0…