全解跨域请求处理办法

为何会有跨域问题

咱们试想一下如下几种状况:javascript

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

为了解决不一样域名相互访问数据致使的不安全问题,Netscape提出的一个著名的安全策略——同源策略,它是指同一个“源头”的数据能够自由访问,但不一样源的数据相互之间都不能访问。html

同源策略

很明显,上述第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个相同:java

协议相同
host相同
端口相同

简单理解一下:node

/*如下两个数据非同源,由于协议不一样*/
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 请求知足一下条件时称为简单请求,不然是非简单请求:webpack

  1. 请求方法是 HEAD,GET,POST 之一
  2. HTTP的头信息不超出如下几种字段:web

    • Accept
    • Accept-Language
    • Content-Language
    • Last-Event-ID
    • Content-Type
  3. Content-Type 取值仅限于 application/x-www-form-urlencoded, multipart/form-data, text/plain

非简单请求在发送以前会发送一次 OPTION 预请求,若是在跨域操做遇到返回 405(Method Not Allowed) 错误,须要服务端容许 OPTION 请求。shell

HTTP 跨域访问的处理办法及适用条件

JSOP

适用条件:请求的 GET 接口须要支持 jsonp 访问

这里须要强调的是,jsonp 不属于 Ajax 的部分,它只是把 url 放入 script 标签中实现的数据传输,不受同源策略限制。因为通常库也会把它和 Ajax 封装在一块儿,因为其和 Ajax 根部不是一回事,因此这里不讨论。下面是一个 jsonp 的例子:express

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)npm

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.comw1.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.comwww.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;  #反向代理
    }
}
相关文章
相关标签/搜索