跨域的 N 种实现方式
跨域, 什么是跨域?跨域有什么好处?跨域有什么不好?怎么实现跨域?
什么是跨域
只要协议、域名、端口有任何一个不同,都被当作是不同的域,之间的请求就是跨域操作. 对于端口和协议的不同,只能通过后台来解决。
跨域会有什么好处?
防止CSRF攻击、同源策略:隔离潜在恶意文件的重要安全机制
补充知识:
什么是CSRF攻击?
CSRF(Cross-site request forgery 跨站请求伪造,也被称为“One Click Attack”或者Session Riding,通常缩写为 CSRF 或者 XSRF,是一种对网站的恶意利用。尽管听起来像跨站脚本(XSS),但它与 XSS 非常不同,并且攻击方式几乎相左。XSS 利用站点内的信任用户,而CSRF则通过伪装来自受信任用户的请求来利用受信任的网站。与 XSS 攻击相比,CSR F攻击往往不大流行(因此对其进行防范的资源也相当稀少)和难以防范,所以被认为比XSS更具危险性。 为什么会出现CSRF攻击?
举例说明
比如说有两个网站 A 和 B。你是 A 网站的管理员,你在 A 网站有一个权限是删除用户,比如说这个过程只需用你的身份登陆并且 POST 数据到 【a.com/delUser 】, 就可以实现删除操作。好现在说B网站,B网站被攻击了,别人种下了恶意代码,你点开的时候就会模拟跨域请求,如果是针对你,那么就可以模拟对 A 站的跨域请求,恰好这个时候你已经在 A 站登陆了。那么攻击者 在B 站内通过脚本,模拟一个用户删除操作是很简单的。面对这种问题,有从浏览器解决,但个人认为最好是从网站端解决,检测每次 POST 过来数据时的 Refer,添加AccessToken 等都是好方法。
防范 CSRF 攻击可以遵循以下几种规则:
- Get 请求不对数据进行修改
- 不让第三方网站访问到用户 Cookie
- 阻止第三方网站请求接口
- 请求时附带验证信息,比如验证码或者 Token
什么是 XSS 攻击
Cross-Site Scripting(跨站脚本攻击)简称 XSS,是一种代码注入攻击。XSS 可以分为多种类型,但是总体上我认为分为两类:持久型、非持久型和DOM-Based型 XSS. 持久型也就是攻击的代码被服务端写入进数据库中,这种攻击危害性很大,因为如果网站访问量很大的话,就会导致大量正常访问页面的用户都受到攻击。 一次性(非持久性) 通过用户点击链接引起
- 反射型 XSS,也可称为非持久型 XSS。
- 其攻击方式往往是通过诱导用户去点击一些带有恶意脚本参数的 URL 而发起的。
- 事实上由于反射型 XSS 因为 URL 特征导致很容易被防御。很多浏览器如 Chrome 都内置了 相应的 XSS 过滤器,来防范用户点击了反射型 XSS 的恶意链接
- 反射型 XSS 归根到底就是由于不可信的用户输入被服务器在没有安全防范处理,然后就直接使用到响应页面中,然后反射给用户而导致代码在浏览器执行的一种 XSS 漏洞。
XSS 主要做什么事:
- 窃取用户 Cookie
- 伪造请求
- XSS 钓鱼
防范措施: 针对 URL 编码, HTML编码, JS 编码。 URL只能使用英文字母(a-zA-Z)、数字(0-9)、-_.~4个特殊字符以及所有(; , /?:@&=+$#)保留字符。 例如:
// 使用了汉字
var url1 = 'http://www.帅.com';
然后由于 encodeURI 不转义 & 、 ? 和 = 。
使用encodeURLComponent
// "http://a.com?a=%3F%26"
encodeURI('http://a.com') + '?a=' + encodeURIComponent('?&');
相应的解码
decodeURl()
decodeURLComponent()
判断输入格式:
过滤特殊字符:<、 > 、&、
过滤危险字符: 去除<"script"> 、javascript、onclik
/**
* 转义 HTML 特殊字符
* @param {String} str
*/
function htmlEncode(str) {
return String(str)
.replace(/&/g, '&')
.replace(/"/g, '"')
.replace(/'/g, ''')
.replace(/</g, '<')
.replace(/>/g, '>');
}
// 正则获取危险标签
var REGEXP_TAG = /<(script|style|iframe)[^<>]*?>.*?<\/\1>/ig;
// 正则获取危险标签属性
var REGEXP_ATTR_NAME = /(onerror|onclick)=([\"\']?)([^\"\'>]*?)\2/ig;
/**
* 过滤函数
*/
function filter(str) {
return String(str)
.replace(REGEXP_TAG, '')
.replace(REGEXP_ATTR_NAME, '');
}
或者使用 CSP 建立白名单,开发者明确告诉浏览器哪些外部资源可以加载和执行。 两种方式开启 CSP。
- 设置 HTTP Header 中的 Content-Security-Policy
- 设置 meta 标签的方式
Content-Security-Policy: default-src ‘self’
Content-Security-Policy: img-src https://*
Content-Security-Policy: child-src 'none'
怎么实现跨域访问?
- JSONP
JSONP跨域GET请求是一个常用的解决方案,下面我们来看一下JSONP跨域是如何实现的,并且探讨下JSONP跨域的原理。 利用在页面中创建"script"节点的方法向不同域提交HTTP请求的方法称为JSONP,这项技术可以解决跨域提交Ajax请求的问题。
JSONP的工作原理如下所述:
假设在http://example1.com/index.php这个页面中向http://example2.com/getinfo.php提交GET请求,我们可以将下面的JavaScript代码放在http://example1.com/index.php这个页面中来实现: 代码如下:
//server.js
const url = require('url');
require('http').createServer((req, res) => {
// console.log('req: ', req);
const data = {
x: 10
};
console.log('url.parse(req.url, true).query: ', url.parse(req.url, true).query);
const callback = url.parse(req.url, true).query.callback;
res.writeHead(200);
res.end( `${callback}(${JSON.stringify(data)})` ); // jsonpCallback(data)
}).listen(3000, '127.0.0.1');
console.log('启动服务,监听 127.0.0.1:3000');
//html
<
script >
function jsonpCallback(data) {
alert('获得 X 数据:' + data.x);
} <
/script> <
script src = "http://127.0.0.1:3000?callback=jsonpCallback" > < /script>
当GET请求从http://xx1.html返回时,可以返回一段JavaScript代码,这段代码会自动执行,可以用来负责调用http://xxx2.html页面中的一个callback函数。
JSONP的优点是:它不像XMLHttpRequest对象实现的Ajax请求那样受到同源策略的限制;它的兼容性更好,在更加古老的浏览器中都可以运行,不需要XMLHttpRequest或ActiveX的支持;并且在请求完毕后可以通过调用 callback 的方式回传结果。
JSONP的缺点则是:它只支持GET请求而不支持POST等其它类型的HTTP请求;它只支持跨域HTTP请求这种情况,不能解决不同域的两个页面之间如何进行 JavaScript 调用的问题。 再来一个例子:
//server.js
const url = require('url');
require('http').createServer((req, res) => {
// console.log('req: ', req);
const data = {
"x": "10"
};
console.log('url.parse(req.url, true).query: ', url.parse(req.url, true).query);
if (url.parse(req.url, true).query.word === 'sjh') {
const callback = url.parse(req.url, true).query.callback;
res.writeHead(200);
res.end( `${callback}(${JSON.stringify(data)})` );
}
}).listen(3000, '127.0.0.1');
console.log('启动服务,监听 127.0.0.1:3000');
//html
var qsData = {
'word': 'shj'
};
$.ajax({
async: false,
url: "http://127.0.0.1:3000", //跨域的dns
type: "GET",
dataType: 'jsonp',
jsonp: 'callback',
data: qsData,
timeout: 5000,
success: function(json) {
console.log('json: ', json);
alert(json.x)
// let obj = JSON.parse(json);
// console.log('obj: ',obj );
},
error: function(xhr) {
console.log('xhr: ', xhr);
//请求出错处理
alert("请求出错)", xhr);
}
});
这种方式其实是上例.ajax api底层的参数就被封装而不可见了。 jquery .ajax方法就和ajax XmlHttpRequest没什么关系了,这种跨域方式其实与ajax XmlHttpRequest协议无关了。 取而代之的则是JSONP协议。JSONP是一个非官方的协议,它允许在服务器端集成Script tags返回至客户端,通过javascript callback的形式实现跨域访问。
Jsonp的执行过程如下: 首先在客户端注册一个callback (如:'jsoncallback'), 然后把callback的名字(如:jsonp1236827957501)传给服务器。注意:服务端得到callback的数值后,要用jsonp1236827957501(......)把将要输出的json内容包括起来,此时,服务器生成 json 数据才能被客户端正确接收。
然后以 javascript 语法的方式,生成一个function, function 名字就是传递上来的参数 'jsoncallback'的值 jsonp1236827957501 . 最后将 json 数据直接以入参的方式,放置到 function 中,这样就生成了一段 js 语法的文档,返回给客户端。
客户端浏览器,解析script标签,并执行返回的 javascript 文档,此时javascript文档数据,作为参数, 传入到了客户端预先定义好的 callback 函数(如上例中jquery $.ajax()方法封装的的success: function (json))里。 可以说jsonp的方式原理上和是一致的(qq空间就是大量采用这种方式来实现跨域数据交换的)。JSONP是一种脚本注入(Script Injection)行为,所以有一定的安全隐患。 那jquery为什么不支持post方式跨域呢?
-
跨域资源共享CORS
什么是CORS CORS(cross-Origin Resource Sharing) 跨域资源共享,管理跨源请求。而跨域资源共享是有益的。今天的我们创建的大多数网站加载资源从网络的各个不同的地方, 当我们发送 GET 请求时,大多数情况浏览器头会fan hui Access-Control-Allow-Origin: * 。意思是能够共享资源在任何域名下。否则就是只能在特定的情况下了。 当请求时以下时,将在原始请求前先进行标准预请求,使用 OPTIONS 头,
http: //www.example.com/foo-bar.html
浏览器访问外部资源时会出现下面这种情况
而我们想要的效果是这种情况.
我们可以在 HTTP 请求头加上 CORS 标准。
-
Access-Control-Allow-Origin
-
Access-Control-Allow-Credentials
-
Access-Control-Allow-Headers
-
Access-Control-Allow-Methods
-
Access-Control-Expose-Headers
-
Access-Control-Max-Age
-
Access-Control-Request-Headers
-
Access-Control-Request-Method
-
Origin
当我们发送 GET 请求时,大多数情况浏览器头会返回 Access-Control-Allow-Origin: * 。意思是能够共享资源在任何域名下。否则就是只能在特定的情况下了。
app.use(cors()); 当请求时以下时,将在原始请求前先进行标准预请求,使用 OPTIONS 头,
- PUT
- DELETE
- CONNECT
- OPTIONS
- TRACE
- PATCH
当我们发的预处理请求并指示原始请求是否安全,如果指定原始请求时安全的,则它将允许原始请求。否则拒接。
//npm install cors
response.setHeader('Content-Type', 'text/html');
var express = require('express');
var cors = require('cors');
//或者直接设置
// res.writeHead(200, {
//'Access-Control-Allow-Origin': 'http://localhost:8080'
// });
var app = express();
app.use(cors());
app.get('/hello/:id', function(req, res, next) {
res.json({
msg: 'CORS-enabled!'
});
});
app.listen(80, function() {
console.log('CORS-enabled web ');
});
-
Server Proxy:
服务器代理,当你需要有跨域请求额操作时发送给后端,让后端帮你带为请求,然后将最后的获取的结果发送给你。
//html
$.get('http://127.0.0.1:3000/topics', function(data) {
consoel.log(data)
})
//server.js
const url = require('url');
const http = require('http');
const server = http.createServer((req, res) => {
const path = url.parse(req.url).path.slice(1);
console.log('path: ', path);
if (path === 'topics') {
http.get('http://cnodejs.org/api/v1/topics', (resp) => {
const {
statusCode
} = resp;
const contentType = resp.headers['content-type'];
console.log('resp: ', statusCode, contentType);
let error;
if (statusCode !== 200) {
error = new Error('Request Failed.\n' +
`Status Code: ${statusCode}` );
} else if (!/^application\/json/.test(contentType)) {
error = new Error('Invalid content-type.\n' +
`Expected application/json but received ${contentType}` );
}
if (error) {
console.error(error.message);
// Consume response data to free up memory
resp.resume();
return;
}
res.setEncoding('utf8');
let data = "";
resp.on('data', chunk => {
data += chunk;
});
res.on('end', () => {
try {
const parsedData = JSON.parse(rawData);
console.log(parsedData);
} catch (e) {
console.error(e.message);
}
});
})
}
}).listen(3000, '127.0.0.1');
console.log('启动服务,监听 127.0.0.1:3000');
- document.domain + iframe
基于iframe实现的跨域要求两个域具有aa.xx.com, bb.xx.com这种特点,也就是两个页面必须属于一个基础域(例如都是xxx.com,或是xxx.com.cn),使用同一协议(例如都是 http)和同一端口(例如都是80),这样在两个页面中同时添加document.domain,就可以实现父页面调用子页面的函数,当然这种方法只能解决主域相同而二级域名不同的情况。 代码如下:
//页面一在head内添加js如下:
document.domain = “xx.com”;
function aa() {
alert(“p”);
}
//body添加iframe和js如下
<
iframe src = ”http: //localhost:8080/2.html“ id=”i”>
document.getElementById(‘i’).onload = function() {
var d = document.getElementById(‘i’).contentWindow;
d.a();
};
//页面二 head添加如下
document.domain = “xx.com”;
function a() {
alert(“c”);
}
//这时候父页面就可以调用子页面的a函数,实现js跨域访问
6.1、通过location.hash跨域
以下三种都是通过 iframe 方式来,我们根据 iframe 能够在浏览器下能够跨域的特点,进行通信。
在 url 中,www.a.com#la 的 "#la" 就是 location.hash,改变 hash 值不会导致页面刷新,所以可以利用 hash 值来进行数据的传递,当然数据量是有限的。
//locationNameA
<
script >
function startRequest() {
var ifr = document.createElement('iframe');
ifr.style.display = 'none';
ifr.src = 'http://127.0.0.1:5501/locationNameB.html#name';
document.body.appendChild(ifr);
}
function checkHash() {
try {
var data = location.hash ? location.hash.substring(1) : '';
if (console.log) {
console.log('Now the data is ' + data);
}
} catch (e) {};
}
setInterval(checkHash, 2000);
<
/script>
//locationNameB
<
script >
function callBack() {
try {
parent.location.hash = 'somedata';
} catch (e) {
// ie、chrome的安全机制无法修改parent.location.hash,
// 所以要利用一个中间的cnblogs域下的代理iframe
var ifrproxy = document.createElement('iframe');
ifrproxy.style.display = 'none';
ifrproxy.src = 'http://127.0.0.1:5502/locationNameC.html#sjh';
document.body.appendChild(ifrproxy);
}
}
<
/script>
//locationNameC
<
script >
parent.parent.location.hash = self.location.hash.substring(1);
console.log('parent.parent.location.hash: ', parent); <
/script>
6.2 通过window.name跨域
window对象有个 name 属性,该属性有个特征:即在一个窗口(window)的生命周期内, 窗口载入的所有的页面都是共享一个window.name 的,每个页面对 window.name 都有读写的权限,window.name 是持久存在一个窗口载入过的所有页面中的。window.name 属性的 name 值在不同的页面(甚至不同域名)加载后依旧存在(如果没修改则值不会变化),并且可以支持非常长的 name 值(2MB)。
window.name = data; //父窗口先打开一个子窗口,载入一个不同源的网页,该网页将信息写入。
window.location = 'http://a.com/index.html';//接着,子窗口跳回一个与主窗口同域的网址。
var data = document.getElementById('myFrame').contentWindow.name。//然后,主窗口就可以读取子窗口的window.name了。
如果是与iframe通信的场景就需要把iframe的src设置成当前域的一个页面地址。
//http://127.0.0.1:5502/window.nameA.html
let data = '';
const ifr = document.createElement('iframe');
ifr.src = "http://127.0.0.1:5501/window.nameB.html";
ifr.style.display = 'none';
document.body.appendChild(ifr);
ifr.onload = function() {
ifr.onload = function() {
console.log('ifr。contentWindow: ', ifr.contentWindow, ifr.contentWindow.name);
data = ifr.contentWindow.name;
console.log('收到数据:', data);
}
ifr.src = "http://127.0.0.1:5502/window.nameC.html";
} <
/script>
//http://127.0.0.1:5501/window.nameB.html
<
script >
window.name = "你想要的数据!"; <
/script>
//http://127.0.0.1:5502/window.nameC.html
emty
6.3 windows.postMessage
otherWindow.postMessage(message, targetOrigin, [transfer]);
接收数据:data、type:类型、source:对象、origin:源;
//postMessageChild.html
<
iframe src = "./postMessageChild.html"
id = "myFrame" > < /iframe> <
script >
iframe = document.getElementById('myFrame')
iframe.onload = function() {
iframe.contentWindow.postMessage('MessageFromIndex1', '*')
setTimeout(() => {
iframe.contentWindow.postMessage('MessageFromIndex2', '*')
})
}
function receiveMessageFromIframePage(event) {
console.log('receiveMessageFromIframePage', event)
}
window.addEventListener('messae', receiveMessageFromIframePage, false)
// postMessageParent.html
parent.postMessage({
msg: 'MessageFromIframePage'
}, "*");
function receiveMessageFromParent(event) {
consle.log('receiveMessageFromParent', event)
}
widnow.addEventListener('message', receiveMessageFromParent, false)
- websocket
WebSocket是一种通信协议,使用ws://(非加密)和wss://(加密)作为协议前缀。该协议不实行同源政策,只要服务器支持,就可以通过它进行跨源通信。
//html
<
ul > < /ul> <
input type = "text" >
$(function() {
var iosocket = io.connect('http://localhost:3000/');
var $ul = $("ul");
var $input = $("input");
iosocket.on('connect', function() { //接通处理
$ul.append($('<li>连上啦</li>'));
iosocket.on('message', function(message) { //收到信息处理
$ul.append($('<li></li>').text(message));
});
iosocket.on('disconnect', function() { //断开处理
$ul.append('<li>Disconnected</li>');
});
});
$input.keypress(function(event) {
if (event.which == 13) { //回车
event.preventDefault();
console.log("send : " + $input.val());
iosocket.send($input.val());
$input.val('');
}
});
});
//server.js
var io = require('socket.io')(3000);
io.sockets.on('connection', function(client) {
client.on('message', function(msg) { //监听到信息处理
console.log('Message Received: ', msg);
client.send('服务器收到了信息:' + msg);
});
client.on("disconnect", function() { //断开处理
console.log("client has disconnected");
});
});
console.log("listen 3000...");
后记
欢迎关注微信公众号!,进行更好的交流。