要考察对web安全是否有接触 ,关于安全方面的在面试时是经常被问到的。同一个意思可能问法也不同,比如说:“你对跨域了解吗?”“什么是跨域请求了?”其实都是同一个意思,回答思路可以先回答什么是跨域请求,有什么方式可以实现跨域请求基本上就可以了。

为什么要跨域?

跨域问题是浏览器同源策略限制,基于JS的安全,JS同源策略要求一个网站不能调用其它网站的js对象,本域的js不能操作其他域的页面对象(比如DOM),当前域名的js只能读取同域下的窗口(iframe)属性。

安全限制的同时也给注入iframe或是ajax应用带来了不少麻烦。所以我们要通过一些方法使得本域的js能够操作其他域的页面对象,或者其他域的js能操作本域的页面对象(iframe之间)。

这里需要明确的一点是:所谓的域跟js的存放服务器没有关系,比如baidu.com的页面加载了google.com的js,那么此js的所在域是baidu.com而不是google.com。也就是说,此时该js能操作baidu.com的页面对象,而不能操作google.com的页面对象。

一个网站的网址组成包括协议名,子域名,主域名,端口号。比如https://www.github.com/80,其中https是协议名,www.github.com是子域名,github.com是主域名,端口号是80,当在在页面中从一个url请求数据时,如果这个url的协议名、子域名、主域名、端口号任意一个有一个不同,就会产生跨域请求。

特别注意:

  • 如果是协议和端口造成的跨域问题“前台”是无能为力的,
  • 在跨域问题上,域仅仅是通过“URL的首部”来识别而不会根据域名对应的IP地址是否相同来判断。
    “URL的首部”可以理解为“协议, 域名 和 端口 必须匹配”。

    跨域方式小结

    使用JSONP跨域(跨全域)

    JSONP的请求过程:

请求阶段:浏览器创建一个 script 标签,并给其src 赋值(类似 http://example.com/api/?callback=jsonpCallback )。
发送请求:当给script的src赋值时,浏览器就会发起一个请求。
数据响应:服务端将要返回的数据作为参数和函数名称拼接在一起(格式类似jsonpCallback({name: 'abc'}))返回。当浏览器接收到了响应数据,由于发起请求的是 script,所以相当于直接调用 jsonpCallback方法,并且传入了一个参数。

  • 优点:它不像XMLHttpRequest对象实现 Ajax 请求那样受到同源策略的限制,兼容性好,在很古老的浏览器中也可以很好的运行,不需要XMLHttpRequestActiveX的支持,简单易用,支持浏览器与服务器双向通信。
    • 缺点:只支持GET请求,且只支持跨域HTTP请求这种情况(不支持HTTPS).不能很好的发现错误,并进行处理。与 Ajax 对比,由于不是通过 XmlHttpRequest 进行传输,所以不能注册 success、 error 等事件监听函数,不能解决不同域的两个页面或 iframe 之间进行数据通信的问题。

情景:网站http://localhost:63342/页面要请求http://localhost:3000/users/userlist页面,userlist页面返回json字符串{name: 'Mr.Cao', gender: 'male', career: 'IT Education'}

端口号为63342网站的一个页面index.html通过ajax请求url http://localhost:3000/users/userlist ,这个明显的出现了跨域请求,因为端口号不一样。请求时就会报错

解决方式,采用JSONP方式来请求index.html
使用 jQuery 集成的 $.ajax实现 JSONP 跨域调用

端口号为63342网站的页面

1
2
3
4
5
6
7
8
9
10
<script>
$.ajax({
url:"http://localhost:3000",
type:"get",
dataType:"jsonp",
success:function(e){
console.log(e);
}
});
</script>

Node.js 服务器代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//运行node server即可建立服务
const url = require('url');

require('http').createServer((req, res) => {

const data = {
name: 'Mr.Cao',
gender: 'male',
career: 'IT Education'
};
const callback = url.parse(req.url, true).query.callback;
res.writeHead(200);
res.end(`${callback}(${JSON.stringify(data)})`); //最关键的一步,拼接回调函数和作为函数参数的数据data

}).listen(3000, '127.0.0.1');

console.log('启动服务,监听 127.0.0.1:3000');
  • 这里一定要注意JSON 的格式转换,不能直接将 JSON 格式的数据直接传给回调函数,否则会发生编译错误: parsererror Error: jsonpCallback was not called

使用 script 标签原生实现 JSONP
Nodejs 服务器代码同上

端口号为63342网站的页面

1
2
3
4
5
6
7
script>
function jsonpCallback(data) {
alert('已获得数据:' + data);
console.log(data);
}
/script>
script src="http://127.0.0.1:3000?callback=jsonpCallback"></script>
  • 由于实现的原理不同,由 JSONP 实现的跨域调用不是通过 XmlHttpRequset 对象,而是通过 script 标签,所以在实现原理上,JSONP 和 Ajax 已经一点关系都没有了。看上去形式相似只是由于 jQuery 对 JSONP 做了封装和转换,实质上jQuery使用也还是script标签的实现方式。

后端使用 CORS 实现跨域调用(跨全域)

原理:CORS的思想,就是使用自定义的HTTP头部让浏览器与服务器进行沟通,从而决定请求或响应是应该成功,还是应该失败。

CORS 是一个 W3C 标准,全称是”跨域资源共享”(Cross-origin resource sharing)它允许浏览器向跨源服务器,发出 XMLHttpRequest 请求,从而克服了 ajax 只能同源使用的限制,是 JSONP 模式的现代版。与 JSONP 不同,CORS 除了 GET 要求方法以外也支持其他的 HTTP 请求。 CORS 一般用XMLHttpRequest,这种方式的错误处理比 JSONP 要来的好。另一方面,JSONP 可以在不支持 CORS 的老旧浏览器上运作。现代的浏览器都支持 CORS,目前IE浏览器的老版本还不支持

CORS 需要浏览器和服务器同时支持才可以生效,对于开发者来说,CORS 通信与同源的 ajax 通信没有差别,代码完全一样。浏览器一旦发现 ajax 请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。

因此,实现 CORS 通信的关键是服务器。只要服务器实现了 CORS 接口,就可以跨源通信。

具体的方法是在服务端设置Response Header响应头中的Access-Control-Allow-Origin为对应的域名,实现了CORS(跨域资源共享),这里出于在安全性方面的考虑就是尽量不要用 *,但对于一些不重要的数据则随意,例如图片

Node.js实现CORS的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
app.post('/cors', function(req, res) {
res.header("Access-Control-Allow-Origin", "*"); //设置请求来源不受限制
res.header("Access-Control-Allow-Headers", "X-Requested-With");
res.header("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS"); //请求方式
res.header("X-Powered-By", ' 3.2.1')
res.header("Content-Type", "application/json;charset=utf-8");
var data = {
name: req.body.name + ' - server 3001 cors process',
id: req.body.id + ' - server 3001 cors process'
}
console.log(data)
res.send(data)
res.end()
})

服务端代理 server proxy(跨全域)

服务器代理,顾名思义,当你需要有跨域的请求操作时发送请求给后端,让后端帮你代为请求,然后最后将获取的结果发送给你。在数据提供方没有提供对JSONP协议或者window.name协议的支持,也没有对其它域开放访问权限,我们可以通过server proxy的方式来抓取数据。

例如当baidu.com域下的页面需要请求google.com下的资源文件getUsers.php时,直接发送一个指向google.com/getUsers.php的Ajax请求肯定是会被浏览器阻止。这时,我们在Baidu.com下配一个代理,然后把Ajax请求绑定到这个代理路径下,例如baidu.com/proxy/,然后这个代理发送HTTP请求访问google.com下的getUsers.php,跨域的HTTP请求是在服务器端进行的(服务器端没有同源策略的限制),客户端并没有产生跨域的Ajax请求。

情景:你的页面需要获取Nodejs专业中文社区 论坛上一些数据,如通过https://cnodejs.org/api/v1/topics,当时因为不同域,所以你可以将请求后端,让其对该请求代为转发。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const url = require('url');
const http = require('http');
const https = require('https');

const server = http.createServer((req, res) => {
const path = url.parse(req.url).path.slice(1);
if(path === 'topics') {
https.get('https://cnodejs.org/api/v1/topics', (resp) => {
let data = "";
resp.on('data', chunk => {
data += chunk;
});
resp.on('end', () => {
res.writeHead(200, {
'Content-Type': 'application/json; charset=utf-8'
});
res.end(data);
});
})
}
}).listen(3000, '127.0.0.1');
console.log('启动服务,监听 127.0.0.1:3000');

通过代码你可以看出,当你访问http://127.0.0.1:3000的时候,服务器收到请求,会代你发送请求https://cnodejs.org/api/v1/topics最后将获取到的数据发送给浏览器。

这个跨域方式下不需要和目标资源签订协议,带有侵略性。

window name

window 对象的name属性是一个很特别的属性,当该window的location变化,然后重新加载,它的name属性可以依然保持不变。那么我们可以在页面 A中用iframe加载其他域的页面B,而页面B中用JavaScript把需要传递的数据赋值给window.name,iframe加载完成之后(iframe.onload),页面A修改iframe的地址,将其变成同域的一个地址,然后就可以读出iframe的window.name的值了(因为A中的window.name和iframe中的window.name互相独立的,所以不能直接在A中获取window.name,而要通过iframe获取其window.name)。
这个方式非常适合单向的数据请求,而且协议简单、安全。不会像JSONP那样不做限制地执行外部脚本。

flash URLLoader

flash有自己的一套安全策略,服务器可以通过crossdomain.xml文件来声明能被哪些域的SWF文件访问,SWF也可以通过API来确定自身能被哪些域的SWF加载。当跨域访问资源时,例如从域baidu.com请求域google.com上的数据,我们可以借助flash来发送HTTP请求。首先,修改域google.com上的crossdomain.xml(一般存放在根目录,如果没有需要手动创建) ,把baidu.com加入到白名单。其次,通过Flash URLLoader发送HTTP请求,最后,通过Flash API把响应结果传递给JavaScript。Flash URLLoader是一种很普遍的跨域解决方案,不过需要支持iOS的话,这个方案就不可行了。

Access Control

此跨域方法目前只在很少的浏览器中得以支持,这些浏览器可以发送一个跨域的HTTP请求(Firefox, Google Chrome等通过XMLHTTPRequest实现,IE8下通过XDomainRequest实现),请求的响应必须包含一个Access-Control-Allow-Origin的HTTP响应头,该响应头声明了请求域的可访问权限。例如baidu.comgoogle.com下的getUsers.php发送了一个跨域的HTTP请求(通过ajax),那么getUsers.php必须加入如下的响应头:

1
header("Access-Control-Allow-Origin: http://www.baidu.com");//表示允许baidu.com跨域请求本文件`

document domain

对于两个iframe之间 主域相同而子域不同的情况下,可以通过设置document.domain的办法来解决,具体做法是可以在http://www.example.com/a.htmlhttp://sub.example.com/b.html两个文件分别加上 document.domain = "a.com";然后通过a.html文件创建一个 iframe,去控制 iframe 的 window,从而进行交互,当然这种方法只能解决主域相同而二级域名不同的情况,如果你异想天开的把 script.example.com的 domain 设为qq.com显然是没用的.

通过修改document的domain属性,我们可以在域和子域或者不同的子域之间通信。同域策略认为域和子域隶属于不同的域,比如baidu.comyouxi.baidu.com是不同的域,这时,我们无法在baidu.com下的页面中调用youxi.baidu.com中定义的JavaScript方法。但是当我们把它们document的domain属性都修改为baidu.com,浏览器就会认为它们处于同一个域下,那么我们就可以互相获取对方数据或者操作对方DOM了。

问题:

1、安全性,当一个站点被攻击后,另一个站点会引起安全漏洞。

2、如果一个页面中引入多个iframe,要想能够操作所有iframe,必须都得设置相同domain。

location hash

适用于两个iframe之间,又称FIM,Fragment Identitier Messaging的简写
因为父窗口可以对iframe进行URL读写,iframe也可以读写父窗口的URL,URL有一部分被称为hash,就是#号及其后面的字符,它一般用于浏览器锚点定位,Server端并不关心这部分,应该说HTTP请求过程中不会携带hash,所以这部分的修改不会产生HTTP请求,但是会产生浏览器历史记录。此方法的原理就是改变URL的hash部分来进行双向通信。每个window通过改变其他 window的location来发送消息(由于两个页面不在同一个域下IE、Chrome不允许修改parent.location.hash的值,所以要借助于父窗口域名下的一个代理iframe),并通过监听自己的URL的变化来接收消息。这个方式的通信会造成一些不必要的浏览器历史记录,而且有些浏览器不支持onhashchange事件,需要轮询来获知URL的改变,最后,这样做也存在缺点,诸如数据直接暴露在了url中,数据容量和类型都有限等。下面举例说明:

假如父页面是baidu.com/a.html,iframe嵌入的页面为google.com/b.html(此处省略了域名等url属性),要实现此两个页面间的通信可以通过以下方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
1、a.html传送数据到b.html

(1) a.html下修改iframe的src为google.com/b.html#paco

(2) b.html监听到url发生变化,触发相应操作

2、b.html传送数据到a.html,由于两个页面不在同一个域下IE、Chrome不允许修改parent.location.hash的值,所以要借助于父窗口域名下的一个代理iframe

(1) b.html下创建一个隐藏的iframe,此iframe的src是baidu.com域下的,并挂上要传送的hash数据,如src="http://www.baidu.com/proxy.html#data"

(2) proxy.html监听到url发生变化,修改a.html的url(因为a.html和proxy.html同域,所以proxy.html可修改a.html的url hash)

(3) a.html监听到url发生变化,触发相应操作

b.html页面的关键代码如下

1
2
3
4
5
6
7
8
9
try {  
parent.location.hash = 'data';
} catch (e) {
// ie、chrome的安全机制无法修改parent.location.hash,
var ifrproxy = document.createElement('iframe');
ifrproxy.style.display = 'none';
ifrproxy.src = "http://www.baidu.com/proxy.html#data";
document.body.appendChild(ifrproxy);
}

因为parent.parent(即baidu.com/a.html)和baidu.com/proxy.html属于同一个域,所以可以改变其location.hash的值
parent.parent.location.hash = self.location.hash.substring(1);

使用HTML5的postMessage方法

不能和服务端交换数据,只能两个iframe之间或者两个页面之间
postMessage 是 HTML5 新增加的一项功能,跨文档消息传输(Cross Document Messaging),目前:Chrome 2.0+、Internet Explorer 8.0+, Firefox 3.0+, Opera 9.6+, 和 Safari 4.0+ 都支持这项功能,使用起来也特别简单。

安全性: postMessage 采用的是 双向安全机制 。发送方发送数据时,会确认接收方的源,而监听方监听到 message 事件后,也可以用event.origin判断是否来自于正确可靠的发送方

参考:
PostMessage_百度百科
Window.postMessage()

这个功能主要包括接受信息的”message”事件和发送消息的”postMessage”方法。比如baidu.com域的A页面通过iframe嵌入了一个google.com域的B页面,可以通过以下方法实现A和B的通信

A页面通过postMessage方法发送消息:

1
2
3
4
5
window.onload = function() {  
var ifr = document.getElementById('ifr');
var targetOrigin = "http://www.google.com";
ifr.contentWindow.postMessage('hello world!', targetOrigin);
};

postMessage的使用方法:

otherWindow.postMessage(message, targetOrigin);

otherWindow: 指目标窗口,也就是给哪个window发消息,是 window.frames属性的成员或者由window.open方法创建的窗口

message: 是要发送的消息,类型为 String、Object (IE8、9 不支持)

targetOrigin: 是限定消息接收范围,不限制请使用 *

B页面通过message事件监听并接受消息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var onmessage = function (event) {  
var data = event.data;//消息
var origin = event.origin;//消息来源地址
var source = event.source;//源Window对象
if(origin=="http://www.baidu.com"){
console.log(data);//hello world!
}
};
if (typeof window.addEventListener != 'undefined') {

window.addEventListener('message', onmessage, false);

} else if (typeof window.attachEvent != 'undefined') {
//ie兼容
window.attachEvent('onmessage', onmessage);
}

同理,也可以B页面发送消息,然后A页面监听并接受消息。

webSocket

websocket是一中全双工通信协议,该协议不实行同源政策,只要服务器支持,就可以通过它进行跨源通信
websocket的应用实例

总结及参考资料

跨域的方法很多,不同的应用场景我们都可以找到一个最合适的解决方案。比如单向的数据请求,我们应该优先选择JSONP或者window.name,双向通信优先采取location.hash,在未与数据提供方达成通信协议的情况下我们也可以用server proxy的方式来抓取数据。(其实讲道理,常用的也就是前三种方式)

关于跨域,你想知道的全在这里
深入理解前端跨域问题的解决方案——前端面试
几种跨域方式总结
你了解跨域请求吗?

最后更新: 2019年10月13日 22:36

原始链接: http://ldc5886.github.io/2019/06/01/JS%20%E8%B7%A8%E5%9F%9F/

× 请我吃糖~
打赏二维码