模型管理和权限管理
阅读本小节前,请确保你一定完成了快速开始的全部内容本小结使用
postman
作为 http 测试工具,请确保你有 postman 或类似的 http 测试工具,它是我们后续开发必不可少的工具。
权限管理
架构介绍
Lin 的定位是一整套的 CMS 解决方案。对于任何的 CMS 来说,权限这一块都是不可或缺的,因此 Lin 在基础框架中便已经集成了权限模块,它是开箱即用的。
不过 Lin 的权限模块的概念可能与其它的权限框架由些许不同,当然你完全不用担心,因为大部分权限系统的模式都大同小异。
在 Lin 的权限模块中,我们有三个模型类来组成这个这个权限模块。如下:
用户模型(userModel,数据表名称为 lin_user)用户是权限系统服务的基本单位,CMS 与一些网站的很大的区别在于,CMS 可能不存在不用登陆便可进入的页面(登陆页除外)。
权限组模型(groupModel,数据表名称为 lin_group)权限组是一个非常重要的概念,权限组是权限分配的基本单位,同时它也是容纳用户的容器,它是用户与权限之间的纽带。
一个用户只能属于一个权限组,超级管理员(admin)不属于任何权限组,但超级管理员拥有所有的权限,一个权限组可以拥有多个用户。
权限组也可拥有多个权限,也就是说,在某个权限组的用户拥有该权限组的所有权限。
- 权限模型(authModel,数据表名称为 lin_auth)你可以把一个权限理解成一把钥匙,然你拥有这把钥匙的时候你就可以打开某扇门,而当你没有这把钥匙的时候,你就会被锁在门的外面。
所以对于某个用户,比如说:你,当你拥有某个权限时,你就可以访问某个 API(或多个AP),而当你没有这个权限时,你访问 API 时会得到一个授权失败或禁止的信息。
基本使用
接下来,就让我们开始实战了。请先在app/v1/book.js
中添加如下一个删除图书功能的API。
bookApi.linDelete(
"deleteBook", // 权限的唯一标示
"/:id",
{
auth: "删除图书", // 权限名称
module: "图书", // 权限模块
mount: true // 是否挂载当前权限
},
groupRequired,
async ctx => {
const id = getSafeParamId(ctx);
await bookDto.deleteBook(id);
ctx.success({
msg: "删除图书成功"
});
}
);
在快速开始一节中已经在数据中创建了一个超级管理员的账号,为了更好的测试,我们还需要一个普通用户的账号,接下来我们把fake.js
的内容更换为如下内容:
require("./initial");
const { db } = require("lin-mizar/lin/db");
const { User, Group, Auth } = require("lin-mizar/lin");
// eslint-disable-next-line no-unused-vars
const { Book } = require("../../app/models/book");
const run = async () => {
const group = new Group();
group.name = "普通分组";
group.info = "就是一个分组而已";
await group.save();
const user = new User();
user.nickname = "pedro";
user.password = "123456";
await user.save();
await Auth.create({
auth: "删除图书",
module: "图书",
group_id: group.id
});
db.close();
};
run();
如果你仔细理解了上面架构介绍,这段代码的作用便是创建一个权限(删除图书),一个用户(pedro),一个权限组(普通分组);且权限与权限组已经绑定了,不过新建的用户却未与权限组关联,这个新建权限就是上段代码中的删除图书的 API。
WARNING
这里,虽然新建了pedro
这个用户,但是他却没有删除图书这个权限。
请在 postman 的 url 地址栏输入http://127.0.0.1:5000/cms/user/login
,请求方法选择 post 方法,并在请求参数的 body 里面填入下面数据:
{
"nickname": "super",
"password": "123456"
}
如果顺利,你会得到如下返回结果:
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NTI3NDQyMDAsImlkZW50aXR5IjoyLCJ0eXBlIjoiYWNjZXNzIiwiaWF0IjoxNTUyNzQwODEwfQ.IKJb96TZQPpwR01OaXzaLBorMD1ZWZZ7HKx4GIjHZYs",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NjA1MTY2MDAsImlkZW50aXR5IjoyLCJ0eXBlIjoicmVmcmVzaCIsImlhdCI6MTU1Mjc0MDgxMH0.TqhAuWKxVReLZKhyRkoMsuY35_uTnQooJuL5G5rNQFE"
}
到此,我们拿到了访问 API 所必须的令牌,请记住这是超级管理员的令牌,它可以访问一切 API。接下来我们访问改变 postman 的 url 地址为
http://127.0.0.1:5000/cms/admin/authority
,并且在请求头中加入键值对。
# 此处的access_token是变量,为上面返回结果的access_token
Authorization: Bearer ${access_token}
结果为:
{
// 图书模块
"图书": {
// 一个权限
"删除图书": ["GET getTestInfo"] // 权限下的endpoint
},
省略......
}
请记住以/admin
为前缀的 url 一般为超级管理员专有,需要以超级管理员账号申请令牌才可访问。
如果你顺利得到了结果,你可能不明白这些数据究竟代表着什么。这没关系,我们会一一说明,不过在此之前,你的先了解一个概念,那就是 Lin 中的endpoint(端点)。
无论你是否熟悉 koa,在这里,一个 endpoint 所能起到的作用那便是——唯一标识一个视图函数,或者说一个 url,一个 API(严格来说 API 不等同于一个视图函数,但是在大多数的开发中,一个视图函数确实与一个 API 对应)。
在刚才的返回结果中,我们可以找到GET getTestInfo
这个字段,这个字段就是一个endpoint。也就是说,这个端点标识了一个视图函数,而删除图书代表的是这个权限的名称。聪明的你会发现,一个权限可以拥有多个端点,换言之那就是一个权限可以对应多个视图函数。当然一般情况下,一个权限对应一个视图函数,我们也强烈推荐你这么做。
删除图书这个权限它还有一个重要的属性,那就是模块(module),也就是图书这个字段。
之所以需要这个属性,是因为权限一旦多了之后,你可能无法很好的梳理它们之间的关联,而有了模块这个概念之后,你可以很好的区分哪些权限属于哪一个模块,在前端操作界面,当管理员进行操作的时候,这也会为他提供诸多便利。
WARNING
开发者请注意,此处的删除图书权限和图书模块对应的是上述添加视图函数中的
bookApi.linDelete(
"deleteBook",
"/:id",
{
auth: "删除图书", // 权限名
module: "图书", // 权限模块
mount: true // 是否挂载权限,为false时,该权限不生效
},
groupRequired,
async ctx => {
const id = getSafeParamId(ctx);
await bookDto.deleteBook(id);
ctx.success({
msg: "删除图书成功"
});
}
);
权限的命名和分配均是由开发者自己来斟酌,如果你们是团队协作,请与你们的前端、客户仔细交流再做决定。
上面,我们添加了一个deleteBook
的 API,其对应的 url为http://127.0.0.1:5000/v1/book/1
,请求方法为delete
。如果此时,你以超级管理员的 token 进行操作,那么毫无疑问,这个 id 为 1 的图书会被删除。但是绝大多数情况下,我们不能让别人直接以超级管理员的身份来操作,这太危险了!!!
聪明的你又会发现,我们刚刚不是已经申请了一个名为pedro
的用户吗?而且他还未被分配到任何权限组(请注意,我们非常不推荐存在离群用户,即没有被分配到权限组的用户,超级管理员除外,此处我们仅仅是为了方便测试而未直接给 pedro 用户分配权限组),因此理论上说 pedro 并没有访问deleteBook
的权限,那么实际如何了。
我们通过 pedro 的账号名、密码登陆获取令牌,并将 header 中的 Authorization 字段换成相应的令牌字段。而后访问http://127.0.0.1:5000/v1/book/1
,你会得到如下结果:
现在权限系统已经开始显现它的威能了。它告诉我们,pedro 这个用户未被分配权限组,并没有权限能够访问这个 API。既然没有权限,那我们便分配这个权限给 pedro(请注意,这里分配权限仅为了测试方便,一般的只允许超级管理员分配)。
打开fake.js
文件,换成如下代码:
require("./initial");
const { db } = require("lin-mizar/lin/db");
// eslint-disable-next-line no-unused-vars
const { User, Group, Auth } = require("lin-mizar/lin");
const run = async () => {
const group = await Group.findOne({
where: {
name: "普通分组"
}
});
const user = await User.findOne({
where: {
nickname: "pedro"
}
});
user.group_id = group.id;
await user.save();
db.close();
};
run();
运行它后,pedro 用户便被分配到了 id 为 1(理论上为 1,可能为其它值) 的这个权限组。接下来,我们再次访问http://127.0.0.1:5000/v1/book/1
,结果如下:
如果你也是一样的结果,那么恭喜你,你已经完成了一个权限开发的全部流程,再你后续的开发过程中,都是类似的做法来完成全部的权限管理开发。
守卫函数
在上一节中,你一定注意到了一个名为groupRequired
的中间件。我们把这它称之为守卫函数。请记住,守卫函数是权限系统中非常重要的一环,在基础库中我们提供了 3 个守卫函数,分别是:
name | 说明 | 作用 |
---|---|---|
loginRequired | 被 loginRequired 装饰的视图函数需登陆后才可访问 | ** |
groupRequired | 被 groupRequired 装饰的视图函数需登陆且被授予相应的权限后才可访问 | ** |
adminRequired | 被 adminRequired 装饰的视图函数只有超级管理员才可访问 | ** |
开发者请注意,这三个守卫函数是开发层面上权限管理。如果你的视图函数未加任何守卫函数修饰,那么它可以被任何人访问,这样的视图函数一般是登陆这些功能的视图函数。又如哪些视图函数需要用户登陆才能访问,如用户修改密码,那么它可以加上loginRequired
这个守卫函数。如果有些视图函数的功能需要授予权限才能访问,请使用groupRequired
。而有些视图函数非超级管理员不可操作,那么请加上adminRequired
修饰。
模型管理
在权限管理的架构介绍时,我们就已经介绍了三个模型类userModel
、groupModel
和authModel
。这是 Lin 里面最重要的三个模型,Lin 默认暴露的 API 和权限系统均直接依赖于这三个模型类。接下来请记住一个原则,如果你想使用这三个类,请通过ctx.manager
来得到这三个类,而后再使用,如下:
bookApi.get("/:id", async ctx => {
// # 得到用户模型
ctx.manager.userModel;
// # 得到权限组模型
ctx.manager.groupModel;
// # 得到权限模型
ctx.manager.authModel;
});
你可能会疑惑,为什么我们不直接通过require
导入模型来使用,而是间接通过 manager来访问,因为这三个核心模型默认集成在 Lin 中的,可是有时候我们需要对其中某个模型进行扩展。例如,userModel 可能还需要一个phone(电话)
属性,那么我们就必须扩展该模型,因此你就需要改变这个 userModel,所以我们才把 userModel 挂载到 manager 中。
扩展模型
刚刚我们谈到扩展模型,接下来让我们来实操一下如何扩展 userModel。首先我们在app/models
下新建user.js
文件,并添加如下代码:
'use strict';
const { modelExtend } = require('lin-mizar/lin/factory');
const { UserInterface } = require('lin-mizar/lin/interface');
const { User } = require('lin-mizar');
const Sequelize = require('sequelize');
modelExtend(UserInterface, {
openid: {
type: Sequelize.STRING(64),
allowNull: true
}
});
User.prototype.sayHello = function () {
console.log('hello world!');
};
module.exports = { User };
接下来,我们修改app/app.js
文件中的 createApp 函数:
const { User } = require('./models/user');
// .....省略代码
async function createApp () {
const app = new Koa();
applyBodyParse(app);
applyCors(app);
config.initApp(app);
const { log, error, Lin } = require("lin-mizar");
app.use(log);
app.on("error", error);
const lin = new Lin();
await lin.initApp(app, true, true, User, null, null);
indexPage(app);
return app;
由于 sequelize 的特性,当数据库中有 lin_user 这张表时,它并不会直接更新这张表。所以为了确保扩展成功,请你在数据库中先删除掉 lin_user 这张表,然后再次运行starter.js
文件。如果,一切顺利你会在数据库中看到 lin_user 这张表多了一个openid
字段。到这里,我们的模型扩展已经成功了,接下来你可以通过ctx.manager
来访问新的 userModel。
InfoCrudMixin
Lin 默认集成了 sequelize 这个方便的 ORM 库,因此我们可以很好的根据模型(model) 来进行相关的数据库操作。某种意义上,sequelize 提供的 API 已经可以很好的操作数据库了。
sequelize 在模型定义时,提供了createdAt
,updatedAt
,deletedAt
这三个选项,方便进行数据的软删除和观测。
为了保持与其他版本的一致性,InfoCrudMixin 在基础模型上改变了createdAt
,updatedAt
,deletedAt
这三个字段名。其分别被改为create_time
,update_time
,delete_time
这三个字段。
并且 InfoCrudMixin 提供了 create_time 的 get 方法,会把 Date 类型的 create_time改变为 unix 时间戳。
小节
在本节中,我们熟悉了一下权限管理的开发流程,并介绍 manager 的模型管理和扩展。
如果你对模型类的操作还不够了解,甚至有些疑惑,请你详细阅读sequelize的官方文档。