同源策略
一、概述
含义
“同源”指的是:
- 协议必须相同
- 域名必须相同
- 端口必须相同
目的
同源策略的目的是为了保证用户信息的安全,不被恶意的网站窃取数据
限制范围
- ajax请求
- dom无法获取
- cookie、localStorage、indexDB无法读取
二、Cookie
cookie
是浏览器写入的一小段信息片段,只有同源的网页才能共享。大小限制是4k。两个页面的一级域名相同,二级域名不同时,可以通过设置相同的 document.domain
,就可以共享cookie
了。
document.domain = "example.com"复制代码
cookie 和 iframe
,localStorage
和 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
片段标识符,指的是链接url
的hash
部分
例如:"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)
}复制代码
同样的,子窗口也可以设置父窗口的hash
部分。
parent.location.href = target + '#' + data复制代码
window.name
指的是当前窗口的name
属性。一个窗口对应着一个name
属性,也就是说当打开
一个新的窗口时,与上个窗口的name
属性就无关了。
// 设置属性
window.name = data
// 获取属性
console.log(document.getElementById("myIFrame").contentWindow.name)复制代码
跨文档通信API(cross-document messaging
)
var data = window.open('https://example2.com', 'title')
data.postMessage('hello', 'https://example2.com')复制代码
是:"https://www.example.com"
,即协议+ 域名+端口,也可以设置为 *
message
事件,监听对方消息。window.addEventListener('message', (e) => {
console.log(e.data)
})
复制代码
e
,有三个属性:- e.source:发送消息的窗口
- e.origin:接收消息的窗口
- e.data:消息内容
通过window.postMessage
,也可以读写其他窗口的LocalStorage
。
四、AJAX
- JSONP
- CORS
- WebSocket
最大的特点是:兼容老式浏览器,服务器改造小。只能处理get
请求。
JSONP
的原理就是利用<script>
的标签没有跨域限制的漏洞。通过<script>
标签指向一个需要访问的地址并提供一个回调函数来接收数据。或者<img>
标签关键点:
- 服务端返回的数据不是
JSON
,而是JavaScript
,也就是说contentType
是text/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('&');
}复制代码
是一种通信协议,使用ws://
和 wss://
作为协议前缀,因为此协议不实行同源策略,只要服务器支持,即可进行跨源通信。
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
复制代码
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
只要不同时满足以上的条件,即为非简单请求。
- 浏览器发现本次请求属于简单请求,自动在头部信息添加
origin
字段 - 服务端根据
origin
字段,判断是否同意本次请求 - 在许可范围内,响应头会新增几个头信息。
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复制代码
- 不在许可范围内,返回一个正常的
HTTP
回应 - 浏览器判断响应头中有没有
Access-Control-Allow-Origin
字段,没有的话报错,被XMLHttpRequest
的onerror
回调函数捕获。这种错误是无法通过状态码识别,因为HTTP
回应的状态码可能是200
Access-Control-Allow-Origin:该字段是必须的,要么是请求的origin的字段值,要么是 * 表示允许任意域名访问。
CORS
请求默认不发送Cookie
和HTTP
认证信息。
Access-Control-Allow-Credentials
字段。Access-Control-Allow-Credentials:true复制代码
withCredentials
属性let xhr = new XMLHttpRequest();
xhr.withCredentials = true复制代码
Access-Control-Allow-Origin
就不能设置为 *,必须指定与请求源一致的域名。常用情况:请求方法是PUT
或者DELETE
,或者 Content-Type
的字段类型是application/json
。
- 非简单请求在发送
cors
请求之前会发送一次OPTIONS
请求,称为“预检”请求。 - 浏览器先询问服务器,当前请求的域名是否在白名单中,以及可以使用哪些
HTTP
动词和头信息字段 - 得到肯定回复,浏览器才会发出正式的
XMLHttpReques
t请求
origin:表示请求源
Access-Control-Request-Method:该字段是必须的,表示请求方法
- 服务端收到预检请求后,检查以上三个字段,确认允许跨源请求,就可做出回应
- 如果否定了跨源请求,服务端返回一个正常的
HTTP
,但是没有任何相关CORS
的响应头字段。浏览器抛出错误,被XMLHttpRequest 的 onerror
回调函数捕获 - 如果同意了跨源请求,服务端会在请求头中添加一些字段
Access-Control-Allow-Methods:GET,POST,PUT。
该字段是必需的,表示服务端支持的所有的请求的方法
Access-Control-Request-Headers
字段,那响应头中也必需也有该字段,表示服务端支持的所有的头信息的字段Access-Control-Allow-Credentials: 该字段是非必须的,表示是否允许发送cookie。默认情况下cookie不包含在cors的请求中
JSONP
只支持get
请求,但是兼容老式浏览器CORS
支持所有请求,但是不兼容ie10
以下
跨域资源共享 CORS 详解(www.ruanyifeng.com/blog/2016/0…)
面试中如何实现一个高质量的JSONP(juejin.im/post/5ebdfe…)
浏览器同源政策及其规避方法(www.ruanyifeng.com/blog/2016/0…)