需要用nodejs实现用户访问控制,有一个node_acl的包可以提供ACL功能,但是没找到什么资料。github上有一个node_acl的demo,翻译一下造福群众。原文地址在这里https://github.com/OptimalBits/node_acl/issues/38
举一个图书的例子,页面路由如下:
/books
/books/:bookId
/books/:bookId/pages
/books/:bookId/pages/:pageId
接下来要考虑每个路径都有哪些行为(action),action有以下四种,都是标准的HTTP查改增删的行为:
get
post
put
delete
这些也是你的权限(permisson)。
角色(role)定义如下:
admin - usually has unlimited access to controlled resources
user - has limited access to controlled resources
public - has very limited access to controlled resources
disabled - has no access to controlled resources
最后是使用者(user),关键是要将user和role对应。当需要检查权限的时候,你只要检查这个user所属的role是否对资源(resource)有权限就行。
(译者注:这里的resource可以理解为对某条路径的访问权限,比如角色admin可以访问/admin路径和/index路径,而角色guest只能访问/index路径)
所以数据结构可以这样设计,一方面要配对roles->resources->permissions,另一方面要配对users->roles
var publicRole = {
name: 'public',
resources: [
],
permissions: []
};
var adminRole = {
name: 'admin',
resources: [
'/books',
'/books/:param1',
'/books/:param1/pages',
'/books/:param1/pages/:pageId'
],
permissions: '*'
};
var userRole = {
name: 'user',
resources: [
'/books',
],
permissions: ['get', 'post']
};
var allRoles = [
publicRole,
adminRole,
userRole
];
以及
var users = [
{
username: 'public',
roles: ['public'],
password: 'public'
},
{
username: 'admin',
roles: ['admin'],
password: 'admin_password'
},
{
username: 'foobar',
roles: ['user'],
password: 'barfoo'
}
];
我要承认我跳过了一些步骤,所以让人困惑。在resources的定义中,我用了”:”符号,这是一个占位符,但实际上node_acl没有这种用法,它仅仅支持字符串的匹配。为了匹配带参数的路径,我们需要generalize paramaterized paths(词穷了,不知道怎么翻),稍后会讲到这一点。
定义了ACL数组和用户数组之后,需要将它们添加到ACL列表中,大概语法像下面那样:
for each role in allRoles
for each resource in role.resources
node_acl.allow(role.name, resource, role.permissions)
for each user in users
create a new User(user.username, user.password) as new user
on new user created
node_acl.addUserRoles(new user.id, user.roles)
(译者注,这里create a new User作者在另一个示例中是用mongodb的entity实现的,另一个示例的链接在本文底部)
这只是一个基础的用法,更高级的用法允许你对资源有更细化的权限。
最后我们来实现access control logic。我们假设用express()框架,用middleware()方法生成的中间件可以很容易将node_acl整合进来
app.get('/books/:bookId', node_acl.middleware(), booksCtrl.getBook)
app.put('/books/:bookId', node_acl.middleware(), booksCtrl.editBook)
app.delete('/books/:bookId', node_acl.middleware(), booksCtrl.deleteBook)
app.get('/books/:bookId/pages', node_acl.middleware(), booksCtrl.listPages)
app.post('/books/:bookId/pages', node_acl.middleware(), booksCtrl.addPage)
app.get('/books/:bookId/pages/:pageId', node_acl.middleware(), booksCtrl.getPage)
app.put('/books/:bookId/pages/:pageId', node_acl.middleware(), booksCtrl.editPage)
app.delete('/books/:bookId/pages/:pageId', node_acl.middleware(), booksCtrl.deletePage)
这样就可以自动生成类似如下的代码
func (reqest, response, next) ->
node_acl.isAllowed(request.userId, url, httpMethod, func(error, isAllowed) ->
if (isAllowed) ->
next()
else ->
response.notAllowed()
)
但是我有两点疑问:一个是我原先的应用没有req.userId这个属性,而是req.user.id;还有一个就是我的路径有一些没有显式表明的参数。(笔者注:比如路径中有bookId,你不可能把所有不同bookId的路径都添加到resources里面吧)
所以我不用它自动生成的中间件而是自己写了一个,大约如下:
function myMiddleware(req, res, next) ->
if (req.user is undefined) ->
req.user = { id: 'public' }
id = req.user.id
// here i need to normalize the route in order to ignore routes with parameters
routeParts = req.path.split('/')
for each part in routeParths
if (part matches an id format) ->
replace it with (':param' + counter)
routeParts.join('/')
// this will convert a route /books/abc123 to /books/:param1
// now actually do the acl check
node_acl.isAllowed(id, routeParts, request.method, func(err, isAllowed) ->
if(isAllowed) ->
next();
else
response.notAllowed
译者注:这个例子还是不够完整,作者又给了一个完整的例子 https://github.com/icompuiz/express-mongoose-acl/commit/e27ef6c2bd61623f44849fd676d38bb289bc07eb