最近做一个基于nodejs的权限管理,查阅了一两天,发现大致是这样的:
passportjs
node-oauth
rbac
node_acl
express_acl
connect-roles
需求
- 按照模块,页面,API等级别做权限控制,暂时不需要做到按钮级别
- 主要程序开发完毕,需要侵入少
- 存储主要考虑redis
- 自己开发管理页面,方便自定义和维护
选取原则
- 轻量级别
passportjs太强大,大到怕怕 - 文档清晰(示例|API)
node_acl的readme 我只能说,真的是很友好,API也不多但是都非常get到点 - 好上手
- 容易扩展
- 功能强大或者适用
- 代码侵入少
最讨厌到处修改代码 - 人气指数相对比较高
最后选择了node_acl,主要是
- 人气相对比较高,大约2000star,
- 其次功能本身很独立,提供内存,mongo和redis三种存储方式
- API简单好用
- 文档好读
- 源码不多,方便去自定义和扩展
- 我们主要程序开发完毕,需要侵入少,研究后发现node_acl应该可以
确认node_acl后,就开始研究一些小细节和设计,说这么多,就是自己写一点代码进行接口功能测试。
问题列表:
- 权限继承
addRoleParents满足需求,确实好用。比如guest, user ,admin三个Role, user集成guest, admin集成user。然后对不同的Role进行权限配置。还是不错的 - Resource 不支持同配
这是什么意思,比如消息有下面几个路径, /msg/delete, msg/add, /msg/list,你就必须一条一条的配置,是不是很狗血
Added the possibility to have a wildcard in resource name - 不提供所有的Role查询的API
我进入后,居然不知道有多少种Role - 删除角色后,数据有残存
- 需要引入模块来关联页面或者API
- 初始化需要有超级管理员
- 默认设置,全部允许?全部不允许访问?
- 是否引入目录继承关系
- 。。。。。。
这里扒拉扒拉写这么多,很多只要API能做到,剩下的就是设计问题。麻烦的问题来了
- Resource不支持通配
- 不提供所有的Role查询的API
这两个底层基本的功能不支持,还玩个蛋。
冷静,冷静,我们打开源码,会发现,插件一共就7个js(版本0.4.11)
- acl.js
核心之核心文件,暴露Role,Resource, Permission等等的API - backend.js
backend API定义,并没实际作用 - contract.js
参数验证js - memory-backend.js
内存中存储 - mongodb-backend.js
mongodb存储 - redis-backend.js
redis存储 - index.js
默认文件
三种backend都是存储数据的,那我们先导出数据来看一看:
关于怎么导出redis
- 安装redis-dump
- edis-dump -h 127.0.0.1 -d 0 --json > c:db.json
我们看一看导出的文件
{ "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_meta@roles,acl_meta@users,acl_meta@users
acl_meta@roles就表示存储的所有的Role, 其他的同理
翻到代码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; } }现在我们要获取当前所有的角色,怎么获取了,这个主要给超级管理员。
我们只要拼接处 acl_meta@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))}到上面为止,我们分析数据结构之后,我们可以获取很多接口并没有暴露的数据了。
回到我们最关心的问题,这个不支持通匹配,怎么办???
- Mongodb-backend
项目主要考虑redis,这个不得己不会考虑 - 已有插件
查询了一遍,node_acl的插件倒是有几个,好像都是支持更多存储的 - 自定义扩展
- 目录权限继承
这个倒是可以考虑 - 手动维护,配合 acl.middleware的第一个参数,限定目录
这个很尴尬 - Acl.middleware + 额外开发中间件()
修改比较多,感觉不好 - await next()之后,再返回前重新拦截
很无赖的想法
这里就先有限考虑自定义扩展,先静静的看看API,思路如下:
- userId => roles (userRoles)
- roles => resources | 依据实际条件缓存 (whatResources)
- 通过resources来匹配path,查找到满足条件的resources|resource
- 通过匹配的resource查询访问权限 (isAllowed)
1,2,4都是有现成的API,唯独3要自己实现,这里就要提到 path-to-regexp, 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)}版权声明
本文仅代表作者观点,不代表百度立场。
本文系作者授权百度百家发表,未经许可,不得转载。


