My Little World

Ajax和跨域

ajax

ajax全称Asynchronous JavaScript and XML(异步的JavaScript与XML),是网页无需刷新页面、使用js与服务器进行交互的一种技术。

ajax的基本流程可以概括为:页面上js脚本实例化一个XMLHttpRequest对象,设置好服务器端的url、必要的查询参数、回调函数之后,向服务器发出请求,
服务器在处理请求之后将处理结果返回给页面,触发事先绑定的回调函数。
这样,页面脚本如果想要改变一个区域的内容,只需要通过ajax向服务器获取与该区域有关的少量数据,在回调函数中将该区域的内容替换掉即可,不需要刷新整个页面。

XMLHttpRequest在发送请求的时候,有两种方式:同步与异步。
同步方式是请求发出后,一直到收到服务器返回的数据为止,浏览器进程被阻塞,页面上什么事也做不了。
而异步方式则不会阻塞浏览器进程,在服务端返回数据并触发回调函数之前,用户依然可以在该页面上进行其他操作。
ajax的核心是异步方式,而同步方式只有在极其特殊的情况下才会被用到。

XMLHttpRequest 对象是一个接口,用于创建一个http请求对象实例,打开一个URL,然后发送这个请求,
当传输完毕后,结果的HTTP状态以及返回的响应内容也可以从请求对象中获取
五种状态
0:未打开
1:未发送
2:以获取响应头
3:正在下载响应体
4:请求完成
XMLHttpRequest API

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
const xhr = new XMLHttpRequest()

xhr.onreadystatechange = function () {
switch (xhr.readyState) {
case 0:
// UNSENT (未打开)
debugger
break
case 1:
// OPENED (未发送)
debugger
break
case 2:
// HEADERS_RECEIVED (已获取响应头)
debugger
break
case 3:
// LOADING (正在下载响应体)
debugger
break
case 4:
// DONE (请求完成)
if (xhr.status === 200) {
console.log(xhr.responseType)
console.log(xhr.responseText)
console.log(xhr.response)
}
break
}
}

xhr.open('GET', 'http://y.com:7001/json', true)
xhr.send(null)

使用ajax 封装POST,GET请求

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
function Ajax(method,url,callback,data,async){
method = method.toUpperCase()
async = async || true
let xmlHttp = null;
if (XMLHttpRequest) {
xmlHttp = new XMLHttpRequest();//服务器请求对象
}
else {
xmlHttp = new ActiveXObject('Microsoft.XMLHTTP');//兼容微软请求对象
}
let params = [];
for (var key in data){
params.push(key + '=' + opt.data[key]);
}
params = params.join('&');
if (method === 'POST') {//请求方法为POST,则执行如下操作
xmlHttp.open(method, url, async);
xmlHttp.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded;charset=utf-8');
xmlHttp.send(params);
}
else if (method === 'GET') {//请求方法为GET,则执行如下操作
xmlHttp.open(opt.method, opt.url + '?' + params, async);
xmlHttp.send(null);
}
xmlHttp.onreadystatechange = function () {
if (xmlHttp.readyState == 4 && xmlHttp.status == 200) {//响应是否成功
var data = JSON.parse(xmlHttp.responseText);
callback(data);
}else{
throw xmlHttp.responseText
}
};
}

同源策略

为了保证用户信息的安全,防止恶意的网站窃取数据
解决办法:同源策略
同源策略是指三个相同:协议相同,域名相同,端口相同
以上三个不相同则是非同源,非同源之间相互访问即跨域访问

跨域和ip没有关系

跨域

浏览器在阻止跨域,阻止方式可能是在一开始就限制了发起跨站的请求,也可能是跨站请求可以正常发起,但返回结果被浏览器拦截了

为什么要防止跨域

跨域访问时会受到同源策略的三个限制
1、Cookie、LocalStorage 和 IndexDB 无法读取。
通过浏览器document.cookie我们可以获取用户登录态,如果cookie可以读取的话,
就会出现在A公司网站里可以去B公司网站获取登录信息的事情,这样就容易将用户信息泄露
2、DOM 无法获得
如果DOM可以获得,现在我是一个假网站,利用iframe套嵌一个目前线上运营的电商网站,那么
消费者在输入支付密码时,那我就可以获取input的值,从而获取用户支付密码
3、AJAX 请求不能发送
如果AJAx可以发送的话,那我们就能将内网东西下载下来发送到外网服务器,从而造成内网信息泄露

如何实现跨域

jsonp

原理是利用<script>标签可以在任何域下获取资源的原理,将要跨域获取的接口url放在<script>标签的src里面,
然后js将标签放到body里面,其中url包含一个callback参数,用于指向处理response的函数,这个函数我们挂载的window上,
即我们在js中定义的的函数

如果在浏览器直接访问接口’http://x.com:7001/json?callback=xxx'
页面会显示

1
/**/ typeof xxx === 'function' && xxx({"msg":"hello world"});

就是说,在请求这个接口时,会去window上找xxx这个对象,看它是不是函数,如果是函数,
就将接口定义的response({“msg”:”hello world”})作为参数传递给xxx函数,并执行xxx函数

现在在http://y.com/x.html页面中进行跨域

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
//json 接口  服务端

module.exports = app => {
class MsgController extends app.Controller {
* index(req) {
this.ctx.body = { msg: 'hello world' }
}
}
return MsgController
}

// js 客户端请求
//方法一:

//定义相应处理函数
window.xxx = function (value) {
console.log(value)
}
//添加script标签
var script = document.createElement('script')
script.src = 'http://x.com:7001/json?callback=xxx'
document.body.appendChild(script)

//方法二:
require(['http://x.com:7001/json?callback=define'], function (value) {
console.log(value)
})

现在访问http://y.com/x.html,在浏览器console就会打印{"msg":"hello world”}

CORS

XMLHttpRequest 2.0以后可以使用cors方法进行跨域
CORS需要浏览器和服务器同时支持
整个CORS通信过程,都是浏览器自动完成,不需要用户参与。
对于开发者来说,CORS通信与同源的AJAX通信没有差别,代码完全一样。
浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。
实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//cors接口 服务端
module.exports = app => {
class CrosController extends app.Controller {
* index(req) {
this.ctx.set('Access-Control-Allow-Origin', '*')//如果不添加则会禁止访问
// this.ctx.set('Access-Control-Allow-Origin', 'http://xx.com')
// 如果我们要 http://*.qq.com 都支持跨域怎么办?
this.ctx.body = { msg: 'hello world' }
}
}
return CrosController
}

//js应用 客户端
var xhr = new XMLHttpRequest()
xhr.onreadystatechange = function () {
if (xhr.readyState === 4 && xhr.status === 200) {
console.log(JSON.parse(xhr.responseText).msg)
}
}
xhr.withCredentials = true//在头部添加cookie带到y.stuq,如果不设置,则不带
xhr.open('GET', 'http://x.com:7001/cros')
xhr.send(null)

跨域资源共享 CORS 详解
HTTP访问控制(CORS)

Access-Control-Allow-Origin 的属性值只允许设置为单个确定域名字符串或者 (),设置的话,最不安全,允许所有域可以访问

在服务器端设置CORS跨域请求中的多域名白名单,可以实现Access-Control-Allow-Origin 允许对某一个或几个网站开放跨域请求权限

原理就是在服务器端判断请求的Header中Origin属性值(req.header.origin)是否在我们的域名白名单列表内。
如果在白名单列表内,那么我们就把 Access-Control-Allow-Origin 设置成当前的Origin值,这样就满足了Access-Control-Allow-Origin 的单一域名要求,也能确保当前请求通过访问;如果不在白名单列表内,则返回错误信息。

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
// 判断origin是否在域名白名单列表中
function isOriginAllowed(origin, allowedOrigin) {
if (_.isArray(allowedOrigin)) {
for(let i = 0; i < allowedOrigin.length; i++) {
if(isOriginAllowed(origin, allowedOrigin[i])) {
return true;
}
}
return false;
} else if (_.isString(allowedOrigin)) {
return origin === allowedOrigin;
} else if (allowedOrigin instanceof RegExp) {
return allowedOrigin.test(origin);
} else {
return !!allowedOrigin;
}
}


const ALLOW_ORIGIN = [ // 域名白名单
'*.233.666.com',
'hello.world.com',
'hello..*.com'
];

app.post('a/b', function (req, res, next) {
let reqOrigin = req.headers.origin; // request响应头的origin属性

// 判断请求是否在域名白名单内
if(isOriginAllowed(reqOrigin, ALLOW_ORIGIN)) {
// 设置CORS为请求的Origin值
res.header("Access-Control-Allow-Origin", reqOrigin);
res.header('Access-Control-Allow-Credentials', 'true');

// 业务代码逻辑代码 ...
// ...
} else {
res.send({ code: -2, msg: '非法请求' });
}
});

与JSONP的比较:
CORS与JSONP的使用目的相同,但是比JSONP更强大。
JSONP只支持GET请求,CORS支持所有类型的HTTP请求。JSONP的优势在于支持老式浏览器,以及可以向不支持CORS的网站请求数据。

iframe-Hash

Location 对象是 Window 对象的一个部分,可通过 window.location 属性来访问
hash是location对象的的一个属性,可以设置或返回从井号 (#) 开始的 URL(锚)

iframe是HTML标签,作用是文档中的文档,或者浮动的框架(FRAME)。iframe元素会创建包含另外一个文档的内联框架(即行内框架)。

原理是在原域页面包装 跨域src的iframe标签,在跨域src的文件里请求跨域的资源(此时二者同域),
跨域src文件是可以获取到iframe父类,即我们原域的window对象,
通过改变原域的hash值,引发原域onhashchange,从而将资源带回到原域

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
//原域js
//包装iframe
var iframe = document.createElement('iframe')
iframe.src = 'http://x.com:7001/public/hash.html'
document.body.appendChild(iframe)
//处理请求资源
window.onhashchange = function () {
// 小练习,做个工具方法,取出query的值
console.log(location.hash)
}

//跨域的hash.html文件

<!DOCTYPE html>
<html>
<head>
</head>
<body>
<script>
var xhr = new XMLHttpRequest()
xhr.onreadystatechange = function () {
if (xhr.readyState === 4 && xhr.status === 200) {
var res = JSON.parse(xhr.responseText)
parent.location.href = `http://y.stuq.com:7001/public/3.html#msg=${res.msg}` //引起原域onhashchange,同时将response带回
}
}
xhr.open('GET', 'http://x.com:7001/json', true) //请求同域资源
xhr.send(null)
</script>
</body>
</html>

iframe-window.name

原理是利用iframe的window.name,name 值在不同的页面(甚至不同域名)加载后依旧存在(如果没修改则值不会变化),并且可以支持非常长的 name 值(2MB)

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
//原域js
var iframe = document.createElement('iframe')
iframe.src = 'http://x.com:7001/public/name.html'
document.body.appendChild(iframe)

var times = 0
iframe.onload = function () {
if (++times === 2) {//第一次打开跨域页面name,第二次加载通知原域iframe改变值
console.log(JSON.parse(iframe.contentWindow.name))
}
}

//name.html
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<script>
var xhr = new XMLHttpRequest()
xhr.onreadystatechange = function () {
if (xhr.readyState === 4 && xhr.status === 200) {
window.name = xhr.responseText //这里是原域iframe的window,iframe不能跨域读取对象,所以
location.href = 'http://y.com:7001/public/index.html' //再次加载iframe,通知原域parent,iframe的contentWindow.name需要做更改
//href的值,依然是跨域的也可以,这里是加载回原域的一个文件
}
}
xhr.open('GET', 'http://x.com:7001/json', true)
xhr.send(null)
</script>
</body>
</html>

iframe-postMessage

利用HTML5的postMessage方法

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
//原域js
var iframe = document.createElement('iframe')
iframe.src = 'http://x.stuq.com:7001/public/post.html'
document.body.appendChild(iframe)

window.addEventListener('message', function(e) {
console.log(JSON.parse(e.data))
}, false);

//post.html
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<script>
var xhr = new XMLHttpRequest()
xhr.onreadystatechange = function () {
if (xhr.readyState === 4 && xhr.status === 200) {
parent.postMessage(xhr.responseText, '*') //拿到父级页面parent,执行postmessage的一个操作,从而引发父级页面的message事件
//*代表targetOrigin可以是任何域
}
}
xhr.open('GET', 'http://x.stuq.com:7001/json', true)
xhr.send(null)
</script>
</body>
</html>

Window.postMessage() API

小结

跨域方法很多
选择如何使用可以考虑以下几方面
1.场景,选择简单的
2.安全,解决问题是否足够安全
3.数据来源,如果跨域接口可以传资源给原域,则可以使用iframe代理
4.承接第三种情景,如果接口不允许传资源,则只能寄希望于后台,使用反向代理的方法获取