乐趣区

全解跨域请求处理办法

为什么会有跨域问题
我们试想一下以下几种情况:

我们打开了一个天猫并且登录了自己的账号,这时我们再打开一个天猫的商品,我们不需要再进行一次登录就可以直接购买商品,因为这两个网页是同源的,可以共享登录相关的 cookie 或 localStorage 数据;
如果你正在用支付宝或者网银,同时打开了一个不知名的网页,如果这个网页可以访问你支付宝或者网银页面的信息,就会产生严重的安全的问题。如果该未知网站是黑客的工具,那他就可以借此发起 CSRF 攻击了。显然浏览器不允许这样的事情发生;
想必你也有过同时登陆好几个 qq 账号的情况,如果同时打开各自的 qq 空间浏览器会有一个小号模式,也就是另外再打开一个窗口专门用来打开第二个 qq 账号的空间。

为了解决不同域名相互访问数据导致的不安全问题,Netscape 提出的一个著名的安全策略——同源策略,它是指同一个“源头”的数据可以自由访问,但不同源的数据相互之间都不能访问。
同源策略
很明显,上述第 1 个和第 3 个例子中,不同的天猫商店和 qq 空间属于同源,可以共享登录信息。qq 为了区别不同的 qq 的登录信息,重新打开了一个窗口,因为浏览器的不同窗口是不能共享信息的。而第 2 个例子中的支付宝、网银、不知名网站之间是非同源的,所以彼此之间无法访问信息,如果你执意想请求数据,会提示异常:
No ‘Access-Control-Allow-Origin’ header is present on the requested resource. Origin ‘null’ is therefore not allowed access.
那么什么是同源的请求呢?同源请求要求被请求资源页面和发出请求页面满足 3 个相同:
协议相同 host 相同端口相同
简单理解一下:
/* 以下两个数据非同源,因为协议不同 */
http://www.abc123.com.cn/item/a.js
https://www.abc123.com.cn/item/a.js

/* 以下两个数据非同源,因为域名不同 */
http://www.abc123.com.cn/item/a.js
http://www.abc123.com/item/a.js

/* 以下两个数据非同源,因为主机名不同 */
http://www.abc123.com.cn/item/a.js
http://item.abc123.com.cn/item/a.js

/* 以下两个数据非同源,因为协议不同 */
http://www.abc123.com.cn/item/a.js
http://www.abc123.com.cn:8080/item/a.js

/* 以下两个数据非同源,域名和 ip 视为不同源
* 这里应注意,ip 和域名替换一样不是同源的
* 假设 www.abc123.com.cn 解析后的 ip 是 195.155.200.134
*/
http://www.abc123.com.cn/
http://195.155.200.134/

/* 以下两个数据同源 */ /* 这个是同源的 */
http://www.abc123.com.cn/source/a.html
http://www.abc123.com.cn/item/b.js
HTTP 简单请求和非简单请求
http 请求满足一下条件时称为简单请求,否则是非简单请求:

请求方法是 HEAD,GET,POST 之一

HTTP 的头信息不超出以下几种字段:

Accept
Accept-Language
Content-Language
Last-Event-ID
Content-Type

Content-Type 取值仅限于 application/x-www-form-urlencoded, multipart/form-data, text/plain

非简单请求在发送之前会发送一次 OPTION 预请求,如果在跨域操作遇到返回 405(Method Not Allowed) 错误,需要服务端允许 OPTION 请求。
HTTP 跨域访问的处理办法及适用条件
JSOP
适用条件:请求的 GET 接口需要支持 jsonp 访问
这里需要强调的是,jsonp 不属于 Ajax 的部分,它只是把 url 放入 script 标签中实现的数据传输,不受同源策略限制。由于一般库也会把它和 Ajax 封装在一起,由于其和 Ajax 根部不是一回事,所以这里不讨论。下面是一个 jsonp 的例子:
window.jsonpCallback = console.log;
var JSONP = document.createElement(“script”);
JSONP.src = “http://tcc.taobao.com/cc/json/mobile_tel_segment.htm?tel=13122222222&t=” + Math.random() + “&callback=jsonpCallback”;;
document.body.appendChild(JSONP);
后端支持 jsonp 方式(Nodejs)
var querystring = require(‘querystring’);
var http = require(‘http’);
var server = http.createServer();

server.on(‘request’, function(req, res) {
var params = qs.parse(req.url.split(‘?’)[1]);
var fn = params.callback;

// jsonp 返回设置
res.writeHead(200, { ‘Content-Type’: ‘text/javascript’});
res.write(fn + ‘(‘ + JSON.stringify(params) + ‘)’);

res.end();
});

server.listen(‘8080’);
console.log(‘Server is running at port 8080…’);
document.domain
适用条件:host 中仅服务器不同的情况,域名本身应该相同
www.dom.com 和 w1.dom.com 需要同源才能访问,可以将 document.domain 设置为 dom.com 解决该问题
document.domain = ‘dom.com’;
例如,我想开发一个浏览器插件,发现腾讯视频页有个 iframe 其本身的跨域的,无法获取其 iframe 的 DOM 对象。但域名部分相同,可以通过该方法解决.
注:如果你想设置它为完全不同的域名,那肯定会报同源错误的,注意使用范围!
嵌入 iframe
适用条件:host 中仅服务器不同的情况,域名本身应该相同
有了上面的例子就不难理解这个方法了,严格来说这不是一个新的方法,而是上一个方法的延伸。通过设置 document.domain, 使同一个域名下不同服务器名的页面可以访问数据,但值得注意的是:这个数据访问不是相互的,外部页面可以访问 iframe 内部的数据,但 iframe 无法不能访问外部的数据。
location.hash
适用条件:iframe 和其宿主页面通信
一个完成的 url 中 # 及后面的部分为 hash, 可以通过修改这个部分完成 iframe 的和宿主直接的数据传递,下面演示一下 iframe 页面 (B.html) 像宿主 (A.html) 传数据, 反之同理:
// A.html
data = [‘book’, ‘map’, ‘shelf’, ‘knife’];
setTimeout(() => {
location.hash = window.encodeURIComponent(data.join(‘/’));
}, 1000);

// B.html
window.parent.onhashchange = function (e) {
var data = window.decodeURIComponent(e.newURL.split(‘#’)[1]).split(‘/’);
console.log(data); // [“book”, “map”, “shelf”, “knife”]
}
* 注意反向传递数据时应该使用 window.parent.location.hash
window.name
适用条件:宿主页面和 iframe 之间通信
window 对象有个 name 属性,该属性有个特征:即在 window 的生命周期内,窗口载入的所有的页面 (iframe) 都是共享一个 window.name 的,每个页面对 window.name 都有读写的权限,window.name 是持久存在一个窗口载入过的所有页面中的,并不会因新页面的载入而进行重置。
这样在 window 中编辑 window.name 就可以在 iframe 中得到,但这个过程缺乏监听,宿主页面 (A.html) 和 iframe 页面 (B.html) 相互并不知道对方在什么时候修改该值:
// A.html
setTimeout(() => {
window.parent.name = “what!”;
}, 2000);

// B.html
setTimeout(() => {
console.log(window.name); // what!
}, 2500);
postMessage
适用条件:postMessage 是 H5 提出的一个消息互通的机制,解决 iframe 不能消息互通的问题,也可以跨 window 通信,语法如下:
// 在 www.siteA.com 中发出消息
// @message{any} 要发送的数据(注意:老版本浏览器只支持字符串类型)
// @targetOrigin{string} 规定接收数据的域,只有其指定的域才能收到消息,如果为 ”*” 则没用域的限制
// transfer{any} 与 message 一同发送并转移所有权
window.postMessage(message, targetOrigin, [transfer]);

// 在另一个页面接受参数
window.onmessage = console.log;
这里暂不谈论第三个参数,因为你可能一辈子也用不到它。而 targetOrigin 最好不要使用 “*”,除非你想让所有页面都收到你的消息。
一种你会用到的场景(iframe):
<!– www.siteA.com/index.html –>
<script>
window.addEventListener(‘message’, function(e){
console.log(‘Get message: “‘ + e.data.title + ‘” from ‘ + e.origin); // ‘Get message: “Saying hello to siteA!” from http://www.siteB.com’
});
</script>
<iframe src=”http://www.siteB.com”></iframe>

<!– www.siteB.com/index.html –>
<script>
function sendMessage(){
window.postMessage({title: ‘Saying hello to siteA!’}, ‘http://www.siteA.com’);
}
setTimeout(sendMessage, 2000);
</script>
这一种仅仅是没有了 iframe,当你在同一个浏览器窗口同时打开 www.siteA.com 和 www.siteB.com 两个标签时也可以这样用
<!– www.siteA.com/index.html –>
<script>
window.addEventListener(‘message’, function(e){
console.log(‘Get message: “‘ + e.data.title + ‘” from ‘ + e.origin); // ‘Get message: “Saying hello to siteA!” from http://www.siteB.com’
});
</script>

<!– www.siteB.com/index.html –>
<script>
function sendMessage(){
window.postMessage({title: ‘Saying hello to siteA!’}, ‘http://www.siteA.com’);
}
setTimeout(sendMessage, 2000);
</script>
反向代理服务器
页面需要访问一些跨域接口,由于代理的存在,在服务器看来请求是不跨域,所以使用各种请求。但需要注意 http 到 https 的兼容问题。
比如当我在一些在线平台开发网站后得到一个页面 www.site-A.com, 而这个页面需要请求我自己的数据服务器 data.site-B.com 上的数据, 这样同样会产生跨域问题,但是 www.site-A.com 这个页面是挂在第三方服务器上的,解决这个问题可以采用代理服务器的方法:
var express = require(‘express’);
var request = require(‘request’);
var app = express();

app.use(‘/api’, function(req, res) {
var url = ‘http://data.site-B.com/api2’ + req.url;
req.pipe(request(url)).pipe(res);
});
app.use(‘/’, function(req, res) {
var url = ‘http://data.site-C.com’;
req.pipe(request(url)).pipe(res);
});
当然还需要同时配置一个 host:
127.0.0.1 local.www.site-B.com
然后访问 local.www.site-B.com 就 OK 了。
CORS
适用条件:CORS 需要服务端支持,且存在一定的兼容性问题(如今你已经可以不考虑,但必要时不要忘了这个 ’bug’)。其通过添加 http 头关键字实现跨域可访问,包括如下头内容:
# www.siteA.com/api 返回相应需要具有如下 http 头字段

Access-Control-Allow-Origin: ‘http://www.siteB.com’ # 指定域可以请求,通配符 ’*’(必须)
Access-Control-Allow-Methods: ‘GET,PUT,POST,DELETE’ # 指定允许的跨域请求方式(必须)
Access-Control-Allow-Headers: ‘Content-Type’ # 请求中必须包含的 http 头字段
Access-Control-Allow-Credentials: true # 配合请求中的 withCredentials 头进行请求验证
通过 express 实现也很简单,在注册路由之前添加:
var cors = require(‘cors’); // 通过 npm 安装
app.use(cors());
当然你也可以自定义一个中间件:
// 自定义中间件
var cors = function (req, res, next) {
// 自定义设置跨域需要的响应头。
res.header(‘Access-Control-Allow-Origin’, ‘http://www.siteB.com’);
res.header(‘Access-Control-Allow-Methods’, ‘GET,PUT,POST,DELETE’);
next();
};

app.use(cors); // 运用跨域的中间件
WebSocket 协议跨域
ws 协议是 H5 中的 web 全双工通信解决方案,常规 http 属于请求相应的过程,在客户端没有请求的情况下,服务端无法给客户端主动推送数据,ws 协议解决了这个问题,但处于安全考虑,其同样有同源策略的限制。
* 这里不讨论通过长连接和服务端挂起请求等方法推送数据,本文只讨论跨域。
下面举个例子(依赖 socket.io.js):
// 前端部分
socket.on(‘connect’, function() {
// 监听服务端消息
socket.on(‘message’, function(msg) {
console.log(‘data from server: ‘ + msg);
});

// 监听服务端关闭
socket.on(‘disconnect’, function() {
console.log(‘Server socket has closed.’);
});
});

document.getElementById(‘input’).onkeyup = function(e) {
if(!e.shiftKey && !e.ctrlKey && !e.altKey && e.keyCode === 13)
socket.send(this.value);
};

// 后端部分(node.js)
var http = require(‘http’);
var socket = require(‘socket.io’);

// 启 http 服务
var server = http.createServer(function(req, res) {
res.writeHead(200, {
‘Content-type’: ‘text/html’
});
res.end();
});

server.listen(‘8080’);
console.log(‘Server is running at port 8080…’);

// 监听 socket 连接
socket.listen(server).on(‘connection’, function(client) {
// 监听客户端信息
client.on(‘message’, function(msg) {
client.send(‘hello:’ + msg);
console.log(‘data from client: ‘ + msg);
});

// 监听客户端断开
client.on(‘disconnect’, function() {
console.log(‘Client socket has closed.’);
});
});
HTML 标签中的 crossorigin 属性
HTML 中 <img>, <video> 和 <script> 具有 crossorigin 属性。添加属性会使相应添加 CORS 相关 http 头(需要服务器支持)。同时,其还有以下可能的取值:

user-credentials 该请求通过 cookie 交换 user-credentials,服务器相应需添加 Access-Control-Allow-Origin
anonymous 该请求不会通过 cookie 交换 user-credentials,服务器相应需添加 Access-Control-Allow-Credentials

当只写了 crossorigin 属性没有指定值时,其默认值为 “anonymous”。即以下两行代码等价:
<scirpt src=”a.com/vendor.js” corssorigin></script>
<scirpt src=”a.com/vendor.js” corssorigin=”anonymous”></script>
几种不同的跨域方法比较

方法
使用条件
使用条件是否与后端交互
优点
缺点

JSONP
服务端支持 jsonp 请求

兼容所有浏览器
只支持 GET 请求,只能和服务端通信

CORS
服务器相应需要相关投资端支持

方便的错误处理,支持所有 http 请求类型
存在浏览器兼容性问题(如今可以忽略了)

document.domain
仅需要跨子域发起请求

使用便捷,没有兼容问题
对于完全不同的域名无法使用

postMessage
浏览器不同 window 间通信、iframe 和其宿主通信

支持浏览器页面间或页面和 iframe 间同行
需要浏览器兼容 H5 接口

window.name
iframe 和其宿主通信

简单易操作
数据暴露在全局不安全

location.hash
iframe 和其宿主通信

简单易操作
数据在 url 中不安全并且有长度限制

反向代理


任何情况都可用
使用比较麻烦,需要自己建立服务

扩展:基于 webpack 的反向代理配置示例
添加 webpack 配置如下:
const config = {
// …
devServer: {
// …
proxy: {
‘/api’: {
target: ‘https://data.site-B.com/api2’,
changeOrigin: true, // 允许跨域
secure: false // 允许访问 https
},
‘/’: {
target: ‘https://data.site-C.com’,
changeOrigin: true,
secure: false
},
}
}
};
module.exports = config;
扩展:基于 Nginx 反向代理和 CORS 配置示例
CORS 配置
location / {
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Credentials true;
add_header Access-Control-Allow-Methods: GET,PUT,POST,DELETE;
}
反向代理配置
server {
listen 7001;
server_name www.domain1.com;

location / {
proxy_pass http://www.B.com:7001; #反向代理
}
}

退出移动版