koa-unless增加method限制

2019年03月24日Web前端Web后端0

没事自己撸了个web项目,后端用的是koa2,并使用了koa-jwt进行鉴权,在使用的时候遇到了些小问题。

一、问题

用koa-jwt鉴权的时候,可以通过设置unless路径,使得某些api不用经过鉴权,比如登录接口:

app.use(jwtKoa({secret}).unless({
    path: [/^/api\/login/]
}));

当然还有些比如获取文章内容接口,我需要在未登录时显示文章,修改和删除是需要鉴权,但由于使用了restful api设计,获取文章和删除文章的api路径是一样的,使用前面的这种方式无法区分get与delete请求,导致鉴权没有任何意义了。

看了下koa-jwt文档上,并没有我需要的,只能看下代码咯,毕竟,比文档更全面的就只有代码了。

二、处理问题

查看koa-jwt代码,发现unless匹配与验证这块是使用的koa-unless,在package.json中也可以看到:

既然这样,我们就去查看koa-unless的文档,只能找到单一的url匹配和method匹配,并没有两者结合的判断方式。没办法,只能去看koa-unless的源码了。

koa-unless

koa-unless目录比较简单,源码就一个index.js,细看代码,

if (matchesCustom(ctx, opts) || matchesPath(requestedUrl, opts) ||
    matchesExtension(requestedUrl, opts) ||matchesMethod(ctx.method, opts)) {
    return next();
}

焦点到这个if判断上,此处就是在处理http请求的数据与unless函数是否匹配的地方,当能够匹配上时,则可以继续执行后面的函数。

function matchesPath(requestedUrl, opts) {
    // ...
}

matchesPath,该方法用于判断当前请求的url是否符合unless中的路径。

function matchesMethod(method, opts) {
    // ...
}

该方法用于判断当前请求方式是否在unless的允许范围内。

我们发现,这几个match匹配函数之间都是或运算符,因此只要任何一者满足,if就可以通过,所以,url与method之间并没有关系,因此,并没有我想要的功能。

小改以下

既然用||没有关联,改成&&是不是就可以了呢? 依旧不行,因为解析规则并没有改变,我没法给每个url增加独立的method。所以得大改一下了。

三、修改组件

我想要的功能是这样的,

app.use(jwtKoa({secret}).unless({
    method: ['POST'],
    path: [/^\/api\/login/,
        {url: /^\/api\/publicKey/, method: ['GET']}]
}));

path中可以像原先一样直接些路径的正则或者字符串,如果这么写了,他的method就由外侧与path同级的method控制(不写这个method,默认所有方法)。当然也可以在path中设置对象的方式规定url和method,这种方式中的method优先级更高。

考虑到路由多的情况下,在初始化的时候,将unless中的url与method解析到一个空对象之中,并以method为key值,而允许的路由则放在key对应的value数组中。这样,当请求的时候就不用整个unless遍历了。

// 请求方式
var methods = ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS', 'HEAD', 'TRACE', 'CONNECT'];
// 存储 method: [url1, url2 ...]
var map = {};

在初始化的时候,解析unless配置:

/**
 * 将用户写的unless配置转到map数据中
 * @param {Object} map 需要存储到的空对象
 * @param {Object} opts 填写的unless配置
 */
function filterUnless(map, opts) {
    // 处理不写外层method时,默认支持所有请求方式
    var mes = opts.method ? opts.method : methods;
    if (Array.isArray(opts.path)) {
        opts.path.forEach((item) => {
            var method = [],
                url='';
            if (Object.prototype.toString.call(item) === '[object Object]') {
                // path中的是对象的,则查找他的path和method
                url = item.url;
                method = item.method || mes;
            } else {
                // 单个字符串或正则
                url = item;
                method = mes;
            }
            // 记录
            record(map, method, url);
        });
    } else if (Array.isArray(opts.method)) {
        // 没有path,检测下是否有method
        opts.method.forEach((met) => {
            // 当方法后的value为空数组时,表示所有url均会符合
            map[ met.toLowerCase() ] = [];
        });
    }
}
// 将 key: ulr1记录到map中
function record(map, method, url) {
    method.forEach((met) => {
        if (!map[ met.toLowerCase() ]) {
            // 无值时,需要先创建空数组
            map[ met.toLowerCase() ] = [];
        }
        map[ met.toLowerCase() ].push(url);
    });
}

既然想要url和method能够相互关联,那么彼此之间肯定要有制约,那么将这两者的判断放到同个方法中。删掉if判断中的matchesPath()与matchesMethod()方法,增加matchesPathAndMethod()方法,参数是当前请求的url和请求方式method。

因为前面已经用map缓存了数据,所以后面的处理就会简单多了。

/**
 * 处理当前请求url和method是否符合unless中的配置
 * @param {Object} requestedUrl 请求url相关信息
 * @param {String} method 请求方式
 */
function matchesPathAndMethod(requestedUrl, method) {
    var path = requestedUrl.pathname,
        mets = map[ method.toLowerCase() ];
    if (!mets) {
        // 没这个方法
        return false;
    }
    if (!mets.length) {
        // 长度是0,证明所有请求都可以
        return true;
    }
    return mets.some(function (p) {
        return (typeof p === 'string' && p === path) ||
            (p instanceof RegExp && !!p.exec(path));
    });
}

对应method中没有url,则直接false,当对应method下是空数组,则是所有url都ok。其他情况下,则需要依次遍历method下的url是否匹配。

文件下载地址:koa-unless