跨域了解多少

跨域了解多少

同源策略

浏览器最核心最基本的安全策略,若缺少了同源策略,浏览器很容易受到XSS、CSFR等攻击

  1. 同源:协议+ 域名+ 端口,相同
  2. 同源策略限制:
  • cookie、localStorage、IndexDB
  • DOM、JS对象无法获取
  • ajax请求不能发送

跨域不一定是浏览器限制了发起跨站请求,也可能是跨站请求可以正常发起,但是返回结果被浏览器拦截了。最好的例子是 CSRF 跨站攻击原理,请求是发送到了后端服务器无论是否跨域!注意:有些浏览器不允许从 HTTPS 的域跨域访问 HTTP,比如 Chrome 和 Firefox,这些浏览器在请求还未发出的时候就会拦截请求,这是一个特例。

实现跨域

  1. jsonp
  2. cors(跨域资源共享)
  3. postMessage + iframe
  4. window.name + iframe
  5. location.hash + iframe
  6. document.domain + iframe
  7. websocket协议
  8. http-proxy(nodejs中间件)
  9. nginx

    利用iframe的onload事件

jsonp

利用src引入外域资源不受限制,通过创建动态script,使src指向一个跨域url,设定数据和回调函数。
回调函数是前后台约定的查询参数,后台返回一个可执行的JS文件即回调函数,这样浏览器就会调用 callback 函数,并传递解析后 json 对象作为参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
function jsonp(options) {
// 整理参数
options = options || {};
options.data = options.data || {};
options.timer = options.timer || 0;

// 随机回调函数的名字
var callbackName = 'jsonp' + Math.random();
callbackName = callbackName.replace('.', ' '); //函数名不可含有.
options.data[options.callback] = callbackName;

// 拼接url
var arr = [];
for(var key in options.data) {
arr.push(key + '=' + encodeURIComponent(options.data[key]));
}
options.url = options.url + '?' + arr.join('&');

// 添加script标签
var script = document.createElement("script");
script.type = "text/javascript";
script.src = options.url;
document.body.appendChild(script);

// 设定超时
if(options.timeout) {
var timer = setTimeout(function() {
document.body.removeChild(scipt);
window[options.data[options.callback]] = function() {
options.err && options.error();
}
}, options.timeout);
}

// 定义回调函数
window[options.data[options.callback]] = function(json) {
clearTimeout(timer);
options.success && options.success(json);
document.body.removeChild(script); //清除用过的script标签
window[options.data[options.callback]] = null; //释放window头上用过的属性
}
}
  1. 优点:兼容性强、简单易用、能之间访问文本,支持浏览器与服务器之间双向通信
  2. 缺点:
  • 只能get请求,这是由于 script 标签自身的限制决定的。
  • 需要保证请求的目的地的绝对安全
  • jsonp 由于不是通过 XMLHttpRequest 进行传输,所以不能注册 success 和 error 等事件监听函数。无法判断请求是否失败,没有错误处理

    CORS 跨域资源共享

    跨域资源共享标准新增了一组 HTTP 首部字段,允许服务器声明哪些源站有权限访问哪些资源。另外,规范要求,对那些可能对服务器数据产生副作用的 HTTP 请求方法(特别是 GET 以外的 HTTP 请求,或者搭配某些 MIME 类型的 POST 请求),浏览器必须首先使用 OPTIONS 方法发起一个预检请求(preflight request),从而获知服务端是否允许该跨域请求。服务器确认允许之后,才发起实际的 HTTP 请求。在预检请求的返回中,服务器端也可以通知客户端,是否需要携带身份凭证(包括 Cookies 和 HTTP 认证相关数据)。

允许浏览器向跨域服务器发出XMLHttpReuest请求,克服ajax只能同源的限制(关键在于服务器)

请求类型

  • 请求方式是head、get、post这3种
  • http的头信息不能超出一下几种字段
1
2
3
accept、accept-languge、content-languge、lase-event-id

content-Type:只限application/x-www-form-urlencoded、text/pain、multipart/form-data

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

服务端设置 Access-Control-Allow-Origin 就可以开启 CORS。 该属性表示哪些域名可以访问资源,如果设置通配符则表示所有网站都可以访问资源

值得注意的是 CORS 请求中必定包含 Origin 头部,但是包含此头部不一定意味着这个请求就是 CORS 请求。

所有CORS相关的头部均以Access-Control-开头

  1. Access-Control-Allow-Origin(required):此头部必须添加到响应报文中,不然缺省值会导致 CORS 请求失败。你可以设置 * 值让所有站点都可以访问你的数据,但最好还是控制一下
  2. Access-Control-Allow-Credentials(optional):设置此头部的值为 true,如果你想要请求附带 cookies。与上文提到的 withCredentials 属性协作。若此头部值为 true 而 withCredentials 属性为 false,会导致请求失败,反之亦然
  3. Access-Control-Allow-Expose-Headers(optional):XMLHttpRequest2 对象存在 getResponseHeader 方法,允许访问一些简单的响应头部如:Content-Type,Cache-Control 等等。如果想暴露一些特殊的头部,可以在此头部的值设置以逗号分隔的头部名称
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 服务器
let whitList= [''] //白名单
app.use((req,res,next)=>{
let origin= req.headers.origin
if(whitList.includes(origin)){
// 设置哪个源可以访问
res.setHeader('Access-Control-Allow-Origin',origin)
// 允许携带哪个头个头访问
Access-Control-Allow-Methods, '逗号隔开'
// 允许携带cookie
Access-Control-Allow-credentials, true
// 预检存活的时间
Access-Control-Max-Age, 6
// 允许前端获取哪个头
Access-Control-Expose-Headers, 'name'
}
// 不做任何处理
if(req.methods === 'options'){
res.end()
}
next()
})

// 前端
document.cookie= 'xxx'
xhr.withCredentials= true
xhr.open('xxx')
xhr.setRequestHeader('name', '信息')

预检请求

“需预检的请求”要求必须首先使用 OPTIONS 方法发起一个预检请求到服务器,以获知服务器是否允许该实际请求。”预检请求“的使用,可以避免跨域请求对服务器的用户数据产生未预期的影响。

postMessage + iframe(2个页面)

postMesaage是HTML5新定义的通信机制,该api定义在window对象

  • 用于解决:
  1. 页面和其打开新窗口的数据传递
  2. 多窗口之间消息传递
  3. 页面与嵌套的iframe消息传递

这种方式通常用于获取嵌入页面中的第三方页面数据。一个页面发送消息,另一个页面判断来源并接收消息

原理:使用postMessage方法必须有其他窗口的引用otherWindow发送方:

1
2
3
otherWindow.postMessage(data, targetOrigin, [transfer])

// 发送的数据 指定哪个窗口接收信息('*' 任意窗口, '/' 当前域下的窗口 )

接收方通过监听message事件获取数据

1
2
3
4
5
6
7
window.addEventListner('message',(e)=>{
// data type source origin
// 发送来的消息对象 类型 发送消息的winodw origin

// 给发送方发送消息
e.source.postMssage
}, false)

实例

1
2
3
4
5
6
7
8
9
10
11
12
// a页面
<iframe src='b的地址' id='iframe' onload='load()'>
</iframe>
function load(){
const iframe= 获取iframe标签
iframe.contentWindow.postMessage('数据', 'b的地址')
}

// b页面
window.onmessage= (e)=>{
e.source.postMessage('数据2', e.origin)
}

window.name + iframe(3个页面)

name值在不同页面(设置不同域名)记载后依旧存在,且支持非常长的值(2MB)

  • 原理:通过iframe的跨域能力,首先将src指向请求的地址,在请求的页面设置window.name,若不同源的话,则在加载好请求后将src由外域指向本地域(同源的html文件/ ‘about:blank;’),然后就可获取window.name

    实例

  • a、b同域,a获取c的数据
  • a先引用c,c设置window.name,加载好后将a的引用地址改为同源的b
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// a页面
<iframe src='c的地址' onload='load()'>
</irame>
let frist= true
function load(){
if(frist){
const iframe= 获取iframe标签
iframe.src= 'b的地址'
frist= false
}else{
// 获取数据
iframe.contentWindow.name
}
}

// c页面设置window.name
window.name= 'name数据'

location.hash + iframe(3个页面)

url路径后面的hash值可以用来通信,不同域的利用iframe的location.hash传值,相同域之间直接JS访问来通信(window.parent)

  • a、c不同域,a、b同域,当a访问c的时候
  • a给c传一个hash值,c收到hash后把hash直接传递给b
  • b再将hash放到a的hash中,最后a通过监听hashchange事件获取数据

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// a页面
<iframe src='c的地址#数据'>
</iframe>
window.onhashchange= ()=>{
// location.hash
}

// c页面
const iframe= document.createElement('iframe')
iframe.src= 'b的地址#返回的数据'
document.body.appendChild(iframe)

// b页面
window.parent.parent.location.hash= location.hash

document.domain

  • 仅限主域相同,子域不同的跨域
  • 两个页面都通过JS设置document.domain为主域实现同域,子窗口通过window.parent.xx获取父窗口的变量
1
2
3
4
5
6
7
8
9
10
11
// a嵌套iframe src为b

// a页面
document.domain= '同一主域'
function load(){
// frame.contentWindow.b的变量

}

// b页面
document.domian= '同一主域'

websocket

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 前端
const socket= new Websocket('后台地址')
socket.onopen= ()=>{
socket.send('数据')
}
socket.onmessage= (e)=>{
e.data
}

// 后台
const wbSocket= require('ws')
const wss= new webSocket.server({
port: 3000
})
wss.on('connection',(ws)=>{
ws.on('message', (data)=>{
// data
})
})

nginx

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

  • 通过nginx配置一个代理服务器(域名与domain1相同,端口不同)
  • 反向代理访问domain2接口

jsonp的安全性问题

1. jsonp劫持(属于csrf攻击)

当某个网站通过jsonp的方式跨域传递用户认证后的敏感信息时,攻击者可以构造恶意的JSONP调用页面诱导用户访问,已达到截取用户敏感信息的目的。解决:限制refer、部署token

2. callback中植入脚本的漏洞

解决:在响应头加上content-type:application/json:charset=utf-8