- Authentication (认证): 你是谁?
- Authorization (授权): 你能干什么?
比如你去酒店开房,你需要拿身份证办理Check in,这是一个认证过程,身份证和密码的功效是一样的证明了你是谁,前台给你的房卡表示授权你开302房间,你不能用身份证去开别人房间吧,这就是认证和授权的区别。
整个授权过程有2个重要的概念:
- role
- scope (permission) 两个词汇同一概念,我们暂时用scope来表示
从零开始
我们写了两个api, 分别都需要认证,
let users = [
{ id: 1, name: 'foo' },
{ id: 2, name: 'bar' }
];
/**
* List all users
* auth: required
*/
router.get('/users', auth(), async ctx => {
ctx.body = users;
});
/**
* Update user
* auth: required
*/
router.patch('/users/:id', auth(), async ctx => {
let target = null;
try {
target = _.find(users, x => x.id === Number.parseInt(ctx.params.id));
target.name = target.name.toUpperCase();
ctx.body = target;
} catch (error) {
ctx.throw(404, error);
}
});
这里mock了一个auth的middleware,完成用户foo的认证,
let auth = () => {
return async (ctx, next) => {
ctx.user = { name: 'foo' };
await next();
}
};
这个时候我们来了一个需求,普通用户可以查看所有用户,但只有管理员可以修改用户,给user加上role [user | admin], 然后在api中判断一下就搞定了,
router.patch('/users/:id', auth(), async ctx => {
if (ctx.user.role !== 'admin') {
ctx.throw(404);
}
}
这个时候我们就把角色(role)做了硬编码,假设我们还有一个更新产品的功能,我也一样会判断角色是否是admin,但问题是用户可能需要自己管理角色,或者说他说管理员A负责用户管理,管理员B负责产品管理,这个时候你就懵b了吧,所以我们新建一层抽象,这个时候才引出了scope,我们基于scope做授权管理,而role和scope关系开放给用户去设定。想明白底层逻辑,这个时候你去看RBAC(Role based Access Control)就会比较好的理解。
ok, 我们来实现一下,首先建立user, role和scope关系,
let users = [
{ id: 1, name: 'foo', roles: ['user'] },
{ id: 2, name: 'bar', roles: ['user', 'admin'] }
];
let roles = [
{ name: 'user', scopes: ['user:read'] },
{ name: 'admin', scopes: ['user:read', 'user:write'] },
];
然后在auth中根据user的roles得到对应的scopes合集,
let auth = () => {
return async (ctx, next) => {
ctx.scopes = [];
try {
// ?name=foo
// ?name=bar
ctx.user = _.find(users, x => x.name === ctx.query.name);
ctx.scopes = _
.chain(ctx.user.roles)
.map(roleName => _.find(roles, role => role.name === roleName).scopes)
.flatten()
.uniq()
.value();
debug(ctx.scopes);
} catch (error) {
ctx.throw(401);
}
await next();
}
};
最后就是应用,
/**
* Update user
* auth: required
* scope: user:write
*/
router.patch('/users/:id', auth(), async ctx => {
if (!ctx.scopes.includes('user:write')) {
ctx.throw(404);
}
let target = null;
try {
target = _.find(users, x => x.id === Number.parseInt(ctx.params.id));
target.name = target.name.toUpperCase();
ctx.body = target;
} catch (error) {
ctx.throw(404, error);
}
});
测试一下,
➜ curl -XPATCH 'http://localhost:3000/users/1?name=foo'
Not Found%
➜ curl -XPATCH 'http://localhost:3000/users/1?name=bar'
{
"id": 1,
"name": "FOO",
"roles": [
"user"
]
}
接着来考虑一个问题,普通用户随便不能随便改其用户信息,但他应该可以改自己的用户信息吧,那么普通用户是否需要user:write的scope呢?
这里假设修改本人信息的api为PATCH /user
或者PATCH /users/me
,那么在这个route需要scope吗?实际情况是,每个验证过身份的用户都可以修改自己的用户信息,所以就根本不需要scope,
/**
* Update the authenticated user
* auth: required
* scope: /
*/
router.patch('/user', auth(), async ctx => {
ctx.body = { name: ctx.user.name.toUpperCase() };
});
重构
我们通过auth这个middleware同时整合Authentication和Authorization两个过程。
改造后的调用更加简洁,
/**
* Update user
* auth: required
* scope: user:write
*/
router.patch('/users/:id', auth('user:write', 'user'), async ctx => {
try {
let target = _.clone(_.find(users, x => x.id === Number.parseInt(ctx.params.id)));
target.name = target.name.toUpperCase();
ctx.body = target;
} catch (error) {
ctx.throw(404, error);
}
});
对应的auth
let auth = (...scopes) => {
return async (ctx, next) => {
ctx.scopes = [];
try {
// ?name=foo
// ?name=bar
ctx.user = _.find(users, x => x.name === ctx.query.name);
if (!ctx.user) { ctx.throw(401); }
ctx.scopes = _
.chain(ctx.user.roles)
.map(roleName => _.find(roles, role => role.name === roleName).scopes)
.flatten()
.uniq()
.value();
// debug(ctx.scopes);
if (scopes.length > 0) {
// 检查授权
if (_.intersection(scopes, ctx.scopes).length > 0) {
await next();
}
} else {
await next();
}
} catch (error) {
ctx.throw(401);
}
}
};
补充话题: Decorator?
先来认识一下babel 6中的decorator, 也可以看阮一峰写的文章,
@testable
class App {
hi() {
debug('hello world');
}
}
function testable(target) {
target.isTestable = true;
}
debug(App.isTestable);
运行这段代码,需要做点配置,
yarn add babel-cli babel-plugin-transform-decorators-legacy -D
.babelrc
{ "plugins": [ "transform-decorators-legacy" ] }
run➜ npx babel decorator.js | node -
有了decorator的基础就可以了解一下gwuhaolin/koa-router-decorator: @route decorator for koa-router
@route('/monitor')
export default class MonitorCtrl{
@route('/alive', HttpMethod.GET)
async alive(ctx) {
ctx.body = {
data: true,
};
}
}
好不容易摆脱了Spring,没想到吧。