最近做一个基于nodejs的权限管理,查阅了一两天,发现大致是这样的:
passportjs
node-oauth
rbac
node_acl
express_acl
connect-roles
需求
选取原则
最后选择了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 backend
acl = 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))
}
到上面为止,我们分析数据结构之后,我们可以获取很多接口并没有暴露的数据了。
回到我们最关心的问题,这个不支持通匹配,怎么办???
这里就先有限考虑自定义扩展,先静静的看看API,思路如下:
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/[email protected]@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)
}
node acl demo
node权限控制模块node_acl的应用