titbit-toolkit
Version:
titbit框架的工具集,包括跨域、静态资源处理,权限过滤,请求计时,cookie,session,jwt等大量中间件
335 lines (259 loc) • 10.5 kB
JavaScript
'use strict';
/**
* 跨域请求中,在以下情况下,origin不会出现:
* https页面请求http接口。
* 但是在目前的策略中,在https页面中,引入http资源会引入不安全因素。
* 浏览器会报错:已阻止载入混合活动内容。
*
* 跨域请求中,origin字段是必须的。
*
* 在https跨域测试中,若要使用自签名的证书,必须先通过浏览器访问后端API,并把证书添加信任。
*
* 这之后,fetch才会成功请求。
*
* 这里讨论的跨域和referer问题只有在浏览器环境才会有效,通过命令请求完全不会理会这些处理过程。
*
* 严格的权限控制要通过token以及其他数据检测手段。
*
* 所以为了保证服务既可以适用于跨域也可以同源,必须要针对referer进行检测。
*
* 你可以设置在允许的referer内也返回消息头,因为有些应用根本不给你发送这个origin消息头,比如小程序。
*
* Access-Control-Allow-Credentials需要单独考虑,这个消息头需要用在浏览器环境下的fetch、Request使用了credentials选项为true的情况下。
* 这个时候,access-control-allow-origin不能是*而是具体的host。
* 这个通信过程是前后端需要协商好的,否则会出现cors报错。
*
*/
class Cors {
constructor(options = {}) {
//某些情况下,因为状态码是204导致连接提前关闭。
this.statusCode = 200;
this.allow = '*';
this.allowHeaders = 'authorization,content-type';
this.allowHeaderTable = {};
this.requestHeaders = '*';
this.credentials = false;
//Access-Control-Expose-Headers 指定哪些消息头可以暴露给请求端。
this.exposeHeaders = '';
this.allowEmptyReferer = true;
this.emptyRefererGroup = null;
this.referer = '';
this.methods = [
'GET', 'POST', 'DELETE', 'PUT', 'OPTIONS', 'PATCH', 'HEAD'
];
if (typeof options !== 'object') {
options = {};
}
this.optionsCache = null;
for (let k in options) {
switch (k) {
case 'allow':
if (options[k] === '*' || Array.isArray(options[k])) {
this.allow = options[k];
}
break;
case 'credentials':
this.credentials = !!options[k];
break;
case 'allowEmptyReferer':
this.allowEmptyReferer = !!options[k];
break;
//允许提交空referer的路由分组
case 'emptyRefererGroup':
if (typeof options[k] === 'string') options[k] = [ options[k] ];
if (Array.isArray(options[k])) this.emptyRefererGroup = options[k];
break;
case 'referer':
if (options[k] === '*') {
this[k] = '*';
} else {
if (typeof options[k] === 'string') options[k] = [ options[k] ];
if (Array.isArray(options[k])) this[k] = options[k];
}
break;
case 'requestHeaders':
this.requestHeaders = options[k];
break;
case 'methods':
if ((options[k] instanceof Array) || typeof options[k] === 'string') {
this.methods = options[k];
}
break;
case 'optionsCache':
case 'maxAge':
if (!isNaN(options[k]))
this.optionsCache = options[k];
break;
case 'allowHeaders':
if (Array.isArray(options[k])) {
if (options[k].length > 0)
this.allowHeaders = options[k].join(',');
} else if (typeof options[k] === 'string') {
this.allowHeaders = options[k].trim() || '*';
}
/* if (this.allowHeaders !== '*') {
this.allowHeaders.split(',')
.filter(p => p.length > 0)
.map(x => x.trim())
.forEach(x => {
this.allowHeaderTable[x] = x;
});
} */
break;
case 'exposeHeaders':
this.exposeHeaders = options[k];
break;
}
}
if (this.methods instanceof Array) {
this.methodString = this.methods.join(',');
} else {
this.methodString = this.methods;
}
this.allowTable = {};
//记录是否用于referer检测。
this.refererTable = {};
this.refererList = [];
if (Array.isArray(this.allow)) {
let lastSlash = 0;
let midIndex, midChar, host, useForReferer;
for (let aw of this.allow) {
useForReferer = true;
if (!aw) continue;
if (typeof aw === 'string') host = aw.trim();
else if (typeof aw === 'object') {
host = aw.url || '';
useForReferer = !!aw.referer;
}
if (!host) continue;
lastSlash = host.length - 1;
while (host[lastSlash] === '/' && lastSlash > 0) lastSlash--;
//不允许 / 结尾。
if (lastSlash < host.length - 1) host = host.substring(0, lastSlash+1);
if (!host.trim()) continue;
midIndex = parseInt(host.length / 2);
midChar = host[midIndex];
this.allowTable[host] = {
url: host,
length: host.length,
lastIndex: host.length - 1,
last: host[host.length - 1],
midIndex: midIndex,
midChar: midChar,
slashIndex: host.indexOf('/', 8),
referer: useForReferer
};
if (useForReferer) {
this.refererTable[host] = this.allowTable[host];
this.refererList.push(this.allowTable[host]);
}
}
this.allow = Object.keys(this.allowTable);
}
}
checkOrigin(url) {
return this.allowTable[url] ? true : false;
}
checkReferer(url) {
if (this.allow === '*') return true;
let aobj;
let ulen = url.length;
let refererTotal = this.refererList.length;
for (let i = 0; i < refererTotal; i++) {
aobj = this.refererList[i];
if (aobj.length > ulen || url[aobj.lastIndex] !== aobj.last) continue;
if (url[aobj.midIndex] !== aobj.midChar) continue;
if (aobj.slashIndex > 0 && url[aobj.slashIndex] !== '/') continue;
if (url.indexOf(aobj.url) === 0) return true;
}
/* for (let u in this.refererTable) {
aobj = this.refererTable[u];
//允许的referer长度比真实的值要短,所以超过的必然不是,如果最后一个字符不匹配可以直接跳过。
if (aobj.length > ulen || url[aobj.length - 1] !== aobj.last) continue;
if (url[aobj.midIndex] !== aobj.midChar) continue;
if (aobj.slashIndex > 0 && url[aobj.slashIndex] !== '/') continue;
//substring 之后 判等 比 indexOf 要慢。
if (url.indexOf(u) === 0) return true;
} */
return false;
}
/**
* 要区分两种状态:跨域请求和同源请求。
* 在同源请求:ctx.headers.referer必然是包含页面路径。
* 若直接请求此资源则不会返回数据。
*
*/
mid() {
let self = this;
return async (ctx, next) => {
//使用ctx.box.corsAllow控制,给中间件处理留出扩展空间。
//跨域请求,必须存在origin。
if (ctx.headers.origin) {
if (!(self.allow === '*'
|| self.allowTable[ctx.headers.origin]
|| ctx.box.corsAllow) )
{
return
}
} else {
/**
* 在浏览器里,如果是跨域,则必然会遵循跨域原则,所以origin和referer的规则都会有效。
* 若不是浏览器,仅凭CORS规范是无法约束非法请求的。
*/
//有一种情况,直接返回的页面并不具备referer,所以前端页面的请求不能有跨域扩展。
//直接通过file方式进行,也不会有referer。
//如果referer前缀就是host说明是本网站访问
//非跨域请求,或仅仅是没有携带origin
let referer = ctx.headers.referer || ''
//处理同源请求。允许提交空的referer或者允许某些路由分组可以提交空referer(针对前端页面)
//或者是检测到ctx.box.corsAllow,host和referer都是客户端的控制,检测必须要依赖服务端对host的配置。
if (!(
(!referer
&& (self.allowEmptyReferer || (self.emptyRefererGroup && self.emptyRefererGroup.indexOf(ctx.group) >= 0) )
)
|| ctx.box.corsAllow || (referer && self.checkReferer(referer))
)
) {
return
}
}
let req_headers = ctx.headers['access-control-request-headers']
if (req_headers && req_headers.indexOf('x-credentials') >= 0) {
//如果前端使用了credentials为include选项,同时使用x-credentials消息头通知后台,此时要做特殊处理。
ctx.headers['x-credentials'] = 'include'
//ctx.setHeader('access-control-request-headers', req_headers);
}
//服务端也要包含此消息头。
ctx.setHeader('access-control-request-headers', self.requestHeaders)
if (self.credentials || ctx.headers['x-credentials'] === 'include') {
let host = '*'
if (ctx.headers.origin) {
host = ctx.headers.origin
} else if (ctx.headers.referer) {
// https:// 不必搜索。
let ind = ctx.headers.referer.indexOf('/', 8)
if (ind < 0) {
host = ctx.headers.referer
} else {
host = ctx.headers.referer.substring(0, ind)
}
}
ctx.setHeader('access-control-allow-credentials', 'true')
ctx.setHeader('access-control-allow-origin', host)
} else {
ctx.setHeader('access-control-allow-origin', '*')
}
ctx.setHeader('access-control-allow-methods', self.methodString)
ctx.setHeader('access-control-allow-headers', self.allowHeaders)
if (self.exposeHeaders)
ctx.setHeader('access-control-expose-headers', self.exposeHeaders)
if (ctx.method === 'OPTIONS') {
self.optionsCache && ctx.setHeader('access-control-max-age', self.optionsCache)
ctx.status(self.statusCode)
} else {
return await next()
}
}
}
}
module.exports = Cors