模型管理和权限管理

阅读本小节前,请确保你一定完成了快速开始的全部内容本小结使用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。

  1. bookApi.linDelete(
  2. "deleteBook", // 权限的唯一标示
  3. "/:id",
  4. {
  5. auth: "删除图书", // 权限名称
  6. module: "图书", // 权限模块
  7. mount: true // 是否挂载当前权限
  8. },
  9. groupRequired,
  10. async ctx => {
  11. const id = getSafeParamId(ctx);
  12. await bookDto.deleteBook(id);
  13. ctx.success({
  14. msg: "删除图书成功"
  15. });
  16. }
  17. );

在快速开始一节中已经在数据中创建了一个超级管理员的账号,为了更好的测试,我们还需要一个普通用户的账号,接下来我们把fake.js的内容更换为如下内容:

  1. require("./initial");
  2. const { db } = require("lin-mizar/lin/db");
  3. const { User, Group, Auth } = require("lin-mizar/lin");
  4. // eslint-disable-next-line no-unused-vars
  5. const { Book } = require("../../app/models/book");
  6. const run = async () => {
  7. const group = new Group();
  8. group.name = "普通分组";
  9. group.info = "就是一个分组而已";
  10. await group.save();
  11. const user = new User();
  12. user.nickname = "pedro";
  13. user.password = "123456";
  14. await user.save();
  15. await Auth.create({
  16. auth: "删除图书",
  17. module: "图书",
  18. group_id: group.id
  19. });
  20. db.close();
  21. };
  22. run();

如果你仔细理解了上面架构介绍,这段代码的作用便是创建一个权限(删除图书),一个用户(pedro),一个权限组(普通分组);且权限与权限组已经绑定了,不过新建的用户却未与权限组关联,这个新建权限就是上段代码中的删除图书的 API。

WARNING

这里,虽然新建了pedro这个用户,但是他却没有删除图书这个权限。

请在 postman 的 url 地址栏输入http://127.0.0.1:5000/cms/user/login,请求方法选择 post 方法,并在请求参数的 body 里面填入下面数据:

  1. {
  2. "nickname": "super",
  3. "password": "123456"
  4. }

如果顺利,你会得到如下返回结果:

  1. {
  2. "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NTI3NDQyMDAsImlkZW50aXR5IjoyLCJ0eXBlIjoiYWNjZXNzIiwiaWF0IjoxNTUyNzQwODEwfQ.IKJb96TZQPpwR01OaXzaLBorMD1ZWZZ7HKx4GIjHZYs",
  3. "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NjA1MTY2MDAsImlkZW50aXR5IjoyLCJ0eXBlIjoicmVmcmVzaCIsImlhdCI6MTU1Mjc0MDgxMH0.TqhAuWKxVReLZKhyRkoMsuY35_uTnQooJuL5G5rNQFE"
  4. }

模型管理和权限管理 - 图1

到此,我们拿到了访问 API 所必须的令牌,请记住这是超级管理员的令牌,它可以访问一切 API。接下来我们访问改变 postman 的 url 地址为

http://127.0.0.1:5000/cms/admin/authority,并且在请求头中加入键值对。

  1. # 此处的access_token是变量,为上面返回结果的access_token
  2. Authorization: Bearer ${access_token}

结果为:

  1. {
  2. // 图书模块
  3. "图书": {
  4. // 一个权限
  5. "删除图书": ["GET getTestInfo"] // 权限下的endpoint
  6. },
  7. 省略......
  8. }

请记住以/admin为前缀的 url 一般为超级管理员专有,需要以超级管理员账号申请令牌才可访问。

模型管理和权限管理 - 图2

如果你顺利得到了结果,你可能不明白这些数据究竟代表着什么。这没关系,我们会一一说明,不过在此之前,你的先了解一个概念,那就是 Lin 中的endpoint(端点)

无论你是否熟悉 koa,在这里,一个 endpoint 所能起到的作用那便是——唯一标识一个视图函数,或者说一个 url,一个 API(严格来说 API 不等同于一个视图函数,但是在大多数的开发中,一个视图函数确实与一个 API 对应)

在刚才的返回结果中,我们可以找到GET getTestInfo这个字段,这个字段就是一个endpoint。也就是说,这个端点标识了一个视图函数,而删除图书代表的是这个权限的名称。聪明的你会发现,一个权限可以拥有多个端点,换言之那就是一个权限可以对应多个视图函数。当然一般情况下,一个权限对应一个视图函数,我们也强烈推荐你这么做。

删除图书这个权限它还有一个重要的属性,那就是模块(module),也就是图书这个字段。

之所以需要这个属性,是因为权限一旦多了之后,你可能无法很好的梳理它们之间的关联,而有了模块这个概念之后,你可以很好的区分哪些权限属于哪一个模块,在前端操作界面,当管理员进行操作的时候,这也会为他提供诸多便利。

WARNING

开发者请注意,此处的删除图书权限和图书模块对应的是上述添加视图函数中的

  1. bookApi.linDelete(
  2. "deleteBook",
  3. "/:id",
  4. {
  5. auth: "删除图书", // 权限名
  6. module: "图书", // 权限模块
  7. mount: true // 是否挂载权限,为false时,该权限不生效
  8. },
  9. groupRequired,
  10. async ctx => {
  11. const id = getSafeParamId(ctx);
  12. await bookDto.deleteBook(id);
  13. ctx.success({
  14. msg: "删除图书成功"
  15. });
  16. }
  17. );

权限的命名和分配均是由开发者自己来斟酌,如果你们是团队协作,请与你们的前端、客户仔细交流再做决定。

上面,我们添加了一个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,你会得到如下结果:

模型管理和权限管理 - 图3

现在权限系统已经开始显现它的威能了。它告诉我们,pedro 这个用户未被分配权限组,并没有权限能够访问这个 API。既然没有权限,那我们便分配这个权限给 pedro(请注意,这里分配权限仅为了测试方便,一般的只允许超级管理员分配)。

打开fake.js文件,换成如下代码:

  1. require("./initial");
  2. const { db } = require("lin-mizar/lin/db");
  3. // eslint-disable-next-line no-unused-vars
  4. const { User, Group, Auth } = require("lin-mizar/lin");
  5. const run = async () => {
  6. const group = await Group.findOne({
  7. where: {
  8. name: "普通分组"
  9. }
  10. });
  11. const user = await User.findOne({
  12. where: {
  13. nickname: "pedro"
  14. }
  15. });
  16. user.group_id = group.id;
  17. await user.save();
  18. db.close();
  19. };
  20. run();

运行它后,pedro 用户便被分配到了 id 为 1(理论上为 1,可能为其它值) 的这个权限组。接下来,我们再次访问http://127.0.0.1:5000/v1/book/1,结果如下:

模型管理和权限管理 - 图4

如果你也是一样的结果,那么恭喜你,你已经完成了一个权限开发的全部流程,再你后续的开发过程中,都是类似的做法来完成全部的权限管理开发。

守卫函数

在上一节中,你一定注意到了一个名为groupRequired的中间件。我们把这它称之为守卫函数。请记住,守卫函数是权限系统中非常重要的一环,在基础库中我们提供了 3 个守卫函数,分别是:

name说明作用
loginRequired被 loginRequired 装饰的视图函数需登陆后才可访问**
groupRequired被 groupRequired 装饰的视图函数需登陆且被授予相应的权限后才可访问**
adminRequired被 adminRequired 装饰的视图函数只有超级管理员才可访问**

开发者请注意,这三个守卫函数是开发层面上权限管理。如果你的视图函数未加任何守卫函数修饰,那么它可以被任何人访问,这样的视图函数一般是登陆这些功能的视图函数。又如哪些视图函数需要用户登陆才能访问,如用户修改密码,那么它可以加上loginRequired这个守卫函数。如果有些视图函数的功能需要授予权限才能访问,请使用groupRequired。而有些视图函数非超级管理员不可操作,那么请加上adminRequired修饰。

模型管理

在权限管理的架构介绍时,我们就已经介绍了三个模型类userModelgroupModelauthModel。这是 Lin 里面最重要的三个模型,Lin 默认暴露的 API 和权限系统均直接依赖于这三个模型类。接下来请记住一个原则,如果你想使用这三个类,请通过ctx.manager来得到这三个类,而后再使用,如下:

  1. bookApi.get("/:id", async ctx => {
  2. // # 得到用户模型
  3. ctx.manager.userModel;
  4. // # 得到权限组模型
  5. ctx.manager.groupModel;
  6. // # 得到权限模型
  7. ctx.manager.authModel;
  8. });

你可能会疑惑,为什么我们不直接通过require导入模型来使用,而是间接通过 manager来访问,因为这三个核心模型默认集成在 Lin 中的,可是有时候我们需要对其中某个模型进行扩展。例如,userModel 可能还需要一个phone(电话)属性,那么我们就必须扩展该模型,因此你就需要改变这个 userModel,所以我们才把 userModel 挂载到 manager 中。

扩展模型

刚刚我们谈到扩展模型,接下来让我们来实操一下如何扩展 userModel。首先我们在app/models下新建user.js文件,并添加如下代码:

  1. 'use strict';
  2. const { modelExtend } = require('lin-mizar/lin/factory');
  3. const { UserInterface } = require('lin-mizar/lin/interface');
  4. const { User } = require('lin-mizar');
  5. const Sequelize = require('sequelize');
  6. modelExtend(UserInterface, {
  7. openid: {
  8. type: Sequelize.STRING(64),
  9. allowNull: true
  10. }
  11. });
  12. User.prototype.sayHello = function () {
  13. console.log('hello world!');
  14. };
  15. module.exports = { User };

接下来,我们修改app/app.js文件中的 createApp 函数:

  1. const { User } = require('./models/user');
  2. // .....省略代码
  3. async function createApp () {
  4. const app = new Koa();
  5. applyBodyParse(app);
  6. applyCors(app);
  7. config.initApp(app);
  8. const { log, error, Lin } = require("lin-mizar");
  9. app.use(log);
  10. app.on("error", error);
  11. const lin = new Lin();
  12. await lin.initApp(app, true, true, User, null, null);
  13. indexPage(app);
  14. return app;

由于 sequelize 的特性,当数据库中有 lin_user 这张表时,它并不会直接更新这张表。所以为了确保扩展成功,请你在数据库中先删除掉 lin_user 这张表,然后再次运行starter.js文件。如果,一切顺利你会在数据库中看到 lin_user 这张表多了一个openid字段。到这里,我们的模型扩展已经成功了,接下来你可以通过ctx.manager来访问新的 userModel。

InfoCrudMixin

Lin 默认集成了 sequelize 这个方便的 ORM 库,因此我们可以很好的根据模型(model) 来进行相关的数据库操作。某种意义上,sequelize 提供的 API 已经可以很好的操作数据库了。

sequelize 在模型定义时,提供了createdAtupdatedAt,deletedAt这三个选项,方便进行数据的软删除和观测。

为了保持与其他版本的一致性,InfoCrudMixin 在基础模型上改变了createdAtupdatedAt,deletedAt这三个字段名。其分别被改为create_timeupdate_timedelete_time这三个字段。

并且 InfoCrudMixin 提供了 create_time 的 get 方法,会把 Date 类型的 create_time改变为 unix 时间戳。

小节

在本节中,我们熟悉了一下权限管理的开发流程,并介绍 manager 的模型管理和扩展。

如果你对模型类的操作还不够了解,甚至有些疑惑,请你详细阅读sequelize模型管理和权限管理 - 图5的官方文档。