本文共 8509 字,大约阅读时间需要 28 分钟。
最近做一个基于nodejs的权限管理,查阅了一两天,发现大致是这样的:
需求
选取原则
最后选择了node_acl,主要是
确认node_acl后,就开始研究一些小细节和设计,说这么多,就是自己写一点代码进行接口功能测试。
问题列表:
这里扒拉扒拉写这么多,很多只要API能做到,剩下的就是设计问题。麻烦的问题来了
这两个底层基本的功能不支持,还玩个蛋。
冷静,冷静,我们打开源码,会发现,插件一共就7个js(版本0.4.11)三种backend都是存储数据的,那我们先导出数据来看一看:
关于怎么导出redis我们看一看导出的文件
{ "acl_allows_/@guest": { "type": "set", "value": [ "*" ] }, "acl_allows_/about@guest": { "type": "set", "value": [ "*" ] }, "acl_allows_/index@guest": { "type": "set", "value": [ "*" ] }, "acl_meta@roles": { "type": "set", "value": [ "guest" ] }, "acl_meta@users": { "type": "set", "value": [ "1024" ] }, "acl_resources@guest": { "type": "set", "value": [ "/", "/about", "/index" ] }, "acl_roles@user": { "type": "set", "value": [ "1024" ] }, "acl_users@1024": { "type": "set", "value": [ "user" ] }}
acl是前缀,在初始化acl的时候可以设置
var acl = require('acl');// Using redis backendacl = new acl(new acl.redisBackend(redisClient, 'acl'));
,,
, 其他的同理 翻到代码acl.js 看看系统是怎么取某个用户的Roles的/** userRoles( userId, function(err, roles) ) Return all the roles from a given user. @param {String|Number} User id. @param {Function} Callback called when finished. @return {Promise} Promise resolved with an array of user roles*/Acl.prototype.userRoles = function(userId, cb){ return this.backend.getAsync(this.options.buckets.users, userId).nodeify(cb);};
this.options.buckets.users是个什么鬼,翻到顶部,
options = _.extend({ buckets: { meta: 'meta', parents: 'parents', permissions: 'permissions', resources: 'resources', roles: 'roles', users: 'users' } }, options);
this.options.buckets.users: 就是users文本,那么联想这几个参数
'acl','users' ,'1024', 再看看,你是不是很惊喜,很意外。"acl_users@1024": { "type": "set", "value": [ "user" ] }
其实很简单,redis-backend.js里面有个方法叫做 bucketKey,专门用户拼接存储的key,
所以,你想获得什么数据,思路就很简单了,bucketKey : function(bucket, keys){ var self = this; if(Array.isArray(keys)){ return keys.map(function(key){ return self.prefix+'_'+bucket+'@'+key; }); }else{ return self.prefix+'_'+bucket+'@'+keys; } }
现在我们要获取当前所有的角色,怎么获取了,这个主要给超级管理员。
我们只要拼接处 ,就可以获得所有的角色了。/** allRoles( userId) 获得所有的Role @param {String|Number}用户Id **/Acl.prototype.allRoles = function (userId) { contract(arguments) .params('string|number') .end() return userId ? this.userRoles(userId) : this.backend.getAsync(this.options.buckets.meta, this.options.buckets.roles) .then(roles => roles.filter(r => !!r))}
到上面为止,我们分析数据结构之后,我们可以获取很多接口并没有暴露的数据了。
回到我们最关心的问题,这个不支持通匹配,怎么办???这里就先有限考虑自定义扩展,先静静的看看API,思路如下:
1,2,4都是有现成的API,唯独3要自己实现,这里就要提到 , express和koa都是基于这个来显示路由匹配的,那么我就有了上面的想法。
/** getMappedRerouces(path,resources) 获得用户有关联的所有资源 @param {String|Number}当前要匹配的路径 @param {Array}当前用户可以访问的所有Resource*/function getMappedRerouces(path, resources) { return [].concat(resources.filter(r = dbRe => { //TODO:: 第二个参数option调研 let re = pathToRegexp(dbRe) return !!re.exec(path) }))}
这个就可以获取当前请求path匹配的所有Resource,
很可能是多条,那么怎么办,任何一条匹配就应该是可以。 那么我们上最后的代码const Acl = require('acl')const contract = require('../node_modules/_acl@0.4.11@acl/lib/contract')const pathToRegexp = require('path-to-regexp')const originalIsAllowed = Acl.prototype.isAllowed/** getMappedRerouces(path,resources) 获得用户有关联的所有资源 @param {String|Number}当前要匹配的路径 @param {Array}当前用户可以访问的所有Resource*/function getMappedRerouces(path, resources) { return [].concat(resources.filter(r = dbRe => { //TODO:: 第二个参数option调研 let re = pathToRegexp(dbRe) return !!re.exec(path) }))}/** getAllResources( userId) 获得用户有关联的所有资源 @param {String|Number}用户Id */Acl.prototype.allResources = function (userId) { contract(arguments) .params('string|number') .end() return userId ? this.userRoles(userId).then(roles => this.whatResources(roles)) : this._allResources()}Acl.prototype._allResources = function () { return this.allRoles() .then(roles => this.backend.unionAsync(this.options.buckets.resources, roles))}/** allRoles( userId) 获得所有的Role @param {String|Number}用户Id */Acl.prototype.allRoles = function (userId) { contract(arguments) .params('string|number') .end() return userId ? this.userRoles(userId) : this.backend.getAsync(this.options.buckets.meta, this.options.buckets.roles) .then(roles => roles.filter(r => !!r))}/** isAllowed( userId, resource, permissions, function(err, allowed) ) Checks if the given user is allowed to access the resource for the given permissions (note: it must fulfill all the permissions). @param {String|Number} User id. @param {String|Array} resource(s) to ask permissions for. @param {String|Array} asked permissions. @param {Function} Callback called wish the result.*/Acl.prototype.isAllowed = function (userId, resource, permissions, cb) { contract(arguments) .params('string|number', 'string', 'string|array', 'function') .params('string|number', 'string', 'string|array') .end(); let args = [...arguments] // 1.userId => roles // 2.roles => resources | 依据实际条件缓存 // 3.通过resources来匹配path,查找到满足条件的resources|resource // 4.通过匹配的resource查询访问权限 return this.allResources(userId) .then(dbRe => getMappedRerouces(resource, Object.keys(dbRe))) .then(resources => { // 多个resource匹配的情况 return Promise.all((resources || []).map(re => { return originalIsAllowed.apply(this, [args[0], re, ...args.slice(2)]) })) }).then(allows => { return allows.some(Boolean) })}module.exports = Acl
怎么使用,
权限设置
acl.allow([ { roles: 'user', allows: [ { resources: ['/msg', '/msg/:id', '/download', '/activities','/msg/(.*)'], permissions: '*' } ] } ])
中间件拦截
const acl = require('../acl')//const getAllRouter = require('./util/getAllRouter')const pathToRegexp = require('path-to-regexp')const loginPath = '/login'module.exports = app => { async function aclmd(req, res, next) { var userId = 1024 if (userId) { const path = req.path if (path == loginPath) { await next() } else { //const aa = await anyMatch(path, userId, acl) const allowed = await acl.isAllowed(userId, path, '*') if (allowed) { next() } else { res.redirect(loginPath) res.end(); } } } else { res.redirect(loginPath) res.end(); } } app.use(aclmd)}
转载地址:http://zcvwx.baihongyu.com/