跨域, 相信大家无论是在工作中还是在面试中经常遇到这个问题, 常常在网上看到别人所整理的一些方法, 看似知道是怎么回事, 但如果没有动手实践过, 总觉得自己没有真正的掌握, 在这里, 通过自己认真思考整理一些常用的方法.
跨域的产生
不用多讲, 作为一名前端开发人员, 相信大家都知道跨域是因为浏览器的同源策略所导致的. 所谓同源是指 "协议 + 域名 + 端口" 三者相同, 即便两个不同的域名指向同一个 ip 地址, 也非同源. 浏览器引入同源策略主要是为了防止 XSS,CSRF 攻击.
CSRF(Cross-site request forgery), 跨站请求伪造, 也被称为: one click attack/session riding, 缩写为: CSRF/XSRF.
在同源策略影响下, 域名 A 向域名 B 发送 Ajax 请求, 或操作 Cookie,LocalStorage,indexDB 等数据, 或操作 dom,JS 就会受到限制, 但请求 CSS,JS 等静态资源不受限制
跨域的解决方案
1 通过 JSONP 跨域
首先说一下 JSONP 的原理, 例如我们平时写 html 的时候常常会使用
<script src="www.b.com/js/jquery.js"></script > 这种方式去取放在另外服务器上的静态资源, 这个是不受同源策略所限制的, 所以我们利用这一点可以解决跨域的问题.
主要代码如下:
1.1 原生实现
在 www.a.com 域名写下如下代码, 去请求 www.b.com 域名的数据
- var script = document.creatElement('script');
- script.type = 'text/javascript';
- script.src = 'http://www.b.com/getdata?callback=demo';
- function demo(res){
- console.log(res);
- }
这里, 我们利用动态脚本的 src 属性, 变相地发送了一个 http://www.b.com/getdata?call... . 这时候, b.com 页面接受到这个请求时, 如果没有 JSONP, 会正常返回 JSON 的数据结果, 像这样:{ msg: 'helloworld' }, 而利用 JSONP, 服务端会接受这个 callback 参数, 然后用这个参数值包装要返回的数据: demo({msg: 'helloworld'});
这时候, 如果 a.com 的页面上正好有一个 demo 的函数:
- function demo(res){
- console.log(res);
- }
当远程数据一返回的时候, 随着动态脚本的执行, 这个 demo 函数就会被执行.
1.2 jQuery Ajax 请求实现
- $.Ajax({
- url:'http://www.b.com/getdata',
- type:'get',
- dataType: 'jsonp', // 请求方式为 JSONP
- jsonpCallback: 'demo', // 自定义回调函数名
- data: {}
- });
服务端代码实现:
以 Node.JS 为例
- var http = require(http);
- // 引入 url 模块解析 url 字符串
- var url = require('url);
- // 引入 querystring 模块处理 query 字符串
- var querystring = require('querystring');
- var server = http.createServer();
- server.on('request',function(req,res){
- var urlurlPath = url.parse(req.url).pathname;
- var param = querystring .parse(req.url.split('?')[1]);
- if(urlPath === '/getData' && param.callback) {
- res.writeHead(200,{'Content-Type','application/json;charset=utf-8'});
- var data = { msg: 'helloworld' };
- data = JSON.stringify(data );
- var callback = param .callback+'('+data+');';
- res.write(callback);
- res.end();
- } else {
- res.writeHead(200, {'Content-Type':'text/html;charset=utf-8'});
- res.write('Hell World\n');
- res.end();
- }
- })
JSONP 缺点: 只能使用 get 请求, 不推荐使用
2 CORS 跨域资源共享
跨域资源共享(CORS) 是一种机制, 它使用额外的 HTTP 头来告诉浏览器 让运行在一个 origin (domain) 上的 web 应用被准许访问来自不同源服务器上的指定的资源. 当一个资源从与该资源本身所在的服务器不同的域或端口请求一个资源时, 资源会发起一个跨域 HTTP 请求.
Cross-Origin Resource Sharing 跨域资源共享, 应该算是现在比较推荐的跨域处理方案. 不仅适用于各种 Method, 而且更加方便和简单
目前, 所有浏览器都支持该功能, IE 浏览器不能低于 IE10.
2.1 简单请求和非简单请求
浏览器将 CORS 请求分成两类: 简单请求 (simple request) 和非简单请求(not-so-simple request).
简单请求同时满足以下条件, 只要不满足以下条件的则为非简单请求
非简单请求会发出一次预检测请求, 返回码是 204, 预检测通过才会真正发出请求, 这才返回 200. 这里通过前端发请求的时候增加一个额外的 headers 来触发非简单请求.
2.2 进行带有身份凭证的 CORS 请求
默认情况下的跨域请求都是不会把 cookie 发送给服务器的, 在需要发送的情况下, 如果是 xhr, 那么需要设置 xhr.withCredentials=true,
如果是采用 fetch 获取的话, 那么需要在 request 里面设置 credentials:'include',
但是如果服务器在预请求的时候没返回 Access-Control-Allow-Crenditials:true 的话, 那么在实际请求的时候, cookie 是不会被发送给服务器端的, 要特别注意对于简单的 get 请求, 不会有预请求的过程,
那么在实际请求的时候, 如果服务器没有返回 Access-Control-Allow-Crenditials:true 的话那么响应结果浏览器也不会交给请求者
对于附带身份凭证的请求, 服务器不得设置 Access-Control-Allow-Origin 的值为 "*".
这是因为请求的首部中携带了 Cookie 信息, 如果 Access-Control-Allow-Origin 的值为 "*", 请求将会失败. 而将 Access-Control-Allow-Origin 的值设置为 http://www.a.com, 则请求将成功执行.
2.3 HTTP 响应首部字段
Access-Control-Allow-Origin: <origin> | *
Access-Control-Expose-Headers 头让服务器把允许浏览器访问的头放入白名单
Access-Control-Max-Age 头指定了 preflight 请求的结果能够被缓存多久
Access-Control-Allow-Credentials
头指定了当浏览器的 credentials 设置为 true 时是否允许浏览器读取 response 的内容.
Access-Control-Allow-Methods 首部字段用于预检请求的响应. 其指明了实际请求所允许使用的 HTTP 方法.
Access-Control-Allow-Headers 首部字段用于预检请求的响应. 其指明了实际请求中允许携带的首部字段.
2.4 以 Node.JS express 为例, 说明如何使用 cors 解决跨域
- var express=require('express');
- var url=require('url');
- var App=express();
- var allowCrossDomain = function(req, res, next) {
- res.header('Access-Control-Allow-Origin', 'http://localhost:63342');
- res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE');
- res.header('Access-Control-Allow-Headers', 'Content-Type');
- res.header('Access-Control-Allow-Credentials','true');
- next();
- };
- App.use(allowCrossDomain);
- App.get('/getData',function (req,res,next) {
- var queryValue=url.parse(req.url).query;
- if(queryValue==='fortunewheel@sina.com'){
- res.send(true);
- }else {
- res.send(false);
- }
- });
- App.listen(3001);
实际开发过程中, 为了安全, 会和 token 一起使用
3 Windows.postMessage
postMessage 是 HTML5 XMLHttpRequest Level 2 中的 API, 且是为数不多可以跨域操作的 Windows 属性之一, 它可用于解决以下方面的问题:
iframe 嵌套页面跨域通信
页面和其打开的新窗口的通信
多窗口之间消息传递
用法:
postMessage(data,origin)方法接受两个参数,
data: 需要传递的数据, html5 规范支持任意基本类型或可复制的对象, 但部分浏览器只支持字符串, 所以传参时最好用 JSON.stringify()序列化.
origin: 协议 + 主机 + 端口号, 也可以设置为 "*", 表示可以传递给任意窗口, 如果要指定和当前窗口同源的话设置为 "/".
代码示例:
- http://www.a.com/a.html
- var iframe = document.getElementById('iframe');
- iframe.onload = function() {
- var data = {
- name: 'jianjian'
- };
- // 向 http://www.b.com 传送跨域数据
- iframe.contentWindow.postMessage(JSON.stringify(data),'http://www.b.com');
- };
- // 接受 http://www.b.com 返回数据
- Windows.addEventListener('message', function(e) {
- alert('data from http://www.b.com--->' + e.data);
- }, false);
- http://www.b.com/b.html
- // 接收 http://www.a.com/a.html 的数据
- Windows.addEventListener('message', function(e) {
- alert('data from http://www.a.com/a.html--->' + e.data);
- var data = JSON.parse(e.data);
- if (data) {
- data.number = 16;
- // 处理后再发回 http://www.a.com/a.html
- Windows.parent.postMessage(JSON.stringify(data), 'http://www.a.com');
- }
- }, false);
- 4 document.domain
这种方式只适合主域名相同, 但子域名不同的 iframe 跨域.
实现原理: 两个页面都通过 JS 强制设置 document.domain 为基础主域, 就实现了同域.
使用方式:
- http://www.a.com/a.html
- document.domain = 'a.com';
- var a = 'hello world';
- "http://www.child.a.com/b.html
- document.domain = 'a.com';
- var b = Windows.parent.a;
- console.log(b);
- 5 Windows.name
Windows.name 传输技术的基本原理:
当在浏览器中打开一个页面, 或者在页面中添加一个 iframe 时即会创建一个对应的 Windows 对象, 当页面加载另一个新的页面时, Windows.name 的属性是不会变的. 这样就可以利用在页面动态添加一个 iframe 然后加载数据页面, 在数据页面将需要的数据赋值给 Windows.name. 然而此时承载的 iframe 的 parent 页面还是不能直接访问不在同一域下的 iframe 的那么属性, 这时, 只需要将 iframe 再加载一个与承载页面同域的空白页面, 即可对 Windows.name 进行数据读取.
通过 iframe 的 src 属性由外域转向本地域, 跨域数据即由 iframe 的 Windows.name 从外域传递到本地域. 这个就巧妙地绕过了浏览器的跨域访问限制, 但同时它又是安全操作.
具体实现:
http://www.a.com/a.html 主页面
http://www.b.com/b.html 数据页面
http://www.a.com/proxy.html 代理页面
http://www.a.com/a.html 代码:
- function crosDomainGetData(url,callback){
- var state = 0;
- var iframe = document.createElement('iframe);
- iframe.src = url;
- iframe.onload = function(){
- if(state === 1){
- // 代理页面成功过后, 读取 Windows.name
- var data = iframe.contentWindow.name;
- callback&&callback(data);
- // 销毁 iframe
- iframe.contentWindow.document.write('');
- iframe.contentWindow.close();
- document.body.removeChild(iframe);
- } else {
- // 第一次加载数据页面成功后, 切换代理页面
- state = 1;
- iframe.contentWindow.location = 'http://www.a.com/proxy.html';
- }
- }
- document.body.appendChild(iframe);
- }
- crosDomainGetData('http://www.b.com/b.html',function(data){
- alert(data);
- })
http://www.b.com/b.html 代码:
Windows.name = '123'
http://www.a.com/proxy.html 空白
6 nginx 代理跨域
- server{
- # 监听 8080 端口
- listen 8080;
- # 域名是 localhost
- server_name localhost;
- #凡是 localhost:8080/API 这个样子的, 都转发到真正的服务端地址 http://www.b.com:8080
- location ^~ /API {
- proxy_pass http://www.b.com:8080;
- }
- }
配置之后就不需要前端做什么修改了, 一般我们在前后端分离项目中开发阶段会采用这种方式, 但不是所有场景都能这样做, 例如后端接口是一个公共的 API, 比如一些公共服务获取天气什么的.
7 WebSocket 协议跨域
websoket 协议天然支持跨域, 你只需要学会如何使用它即可, 关于 websocket 协议请看我的另外一篇文章 WebSocket 网络通信协议
来源: http://developer.51cto.com/art/201812/588676.htm