使用这个库实现接口鉴权,太简单了。(上)——从零开始搭建一个高颜值后台管理系统全栈框架(十二)
往期回顾
前言
前面我们实现了按钮权限控制,但是只在前端控制按钮显示和隐藏并没有多大用处,别人只要知道接口,可以通过一些请求工具直接调你的接口,所以后端在接受到请求的时候首先判断用户有没有权限,有权限则通过,无权限则拒绝访问。
接口鉴权这里我推荐使用casbin这个库,使用起来真的很简单,并且支持多个平台,node、java、go、php这些常用的后端语言都支持。我们公司的项目(java)接口鉴权这一块的功能是一个后端大佬自己从零开始开发的,我接触过casbin之后,向我们后端推荐了这个库,现在我们公司项目已经使用这个库了。
Casbin
概述
Casbin 是一个强大的、高效的开源访问控制框架,其权限管理机制支持多种访问控制模型。
支持很多种语言

Casbin能做什么
- 支持自定义请求的格式,默认的请求格式为{subject, object, action}。
 - 具有访问控制模型model和策略policy两个核心概念。
 - 支持RBAC中的多层角色继承,不止主体可以有角色,资源也可以具有角色。
 - 支持内置的超级用户 例如:root 或 administrator。超级用户可以执行任何操作而无需显式的权限声明。
 - 支持多种内置的操作符,如 keyMatch,方便对路径式的资源进行管理,如 /foo/bar 可以映射到 /foo*
 
Casbin不能做什么
- 身份认证 authentication(即验证用户的用户名和密码),Casbin 只负责访问控制。应该有其他专门的组件负责身份认证,然后由 Casbin 进行访问控制,二者是相互配合的关系。
 - 管理用户列表或角色列表。 Casbin 认为由项目自身来管理用户、角色列表更为合适, 用户通常有他们的密码,但是 Casbin 的设计思想并不是把它作为一个存储密码的容器。 而是存储RBAC方案中用户和角色之间的映射关系。
 
性能测试
上面是官网给出的性能测试数据,可以看出性能是没有问题的。
入门
前言
上面的介绍大家可能看的云里雾里,下面带着大家实战一下,让大家更深入的了解casbin的用法。
初始化一个midway项目
找一个合适的目录,执行下面命令创建midway项目。
npm init midway
安装casbin依赖
pnpm i casbin --save
创建casbin模型描述文件
在项目src目录下创建basic_model.conf文件,文件内容如下:
[request_definition]
r = sub, obj, act
[policy_definition]
p = sub, obj, act
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = r.sub == p.sub && r.obj == p.obj && r.act == p.act
后面讲解这些内容的含义
创建casbin策略文件
在项目src目录下创建basic_policy.csv文件,文件内容如下:
p, alice, data1, read
p, bob, data2, write
后面讲解这些内容的含义
在home.controler中使用casbin方法
// src/controller/home.controller.ts
import { App, Controller, Get } from '@midwayjs/core';
import { newEnforcer } from 'casbin';
import * as koa from '@midwayjs/koa';
import { join } from 'path';
@Controller('/')
export class HomeController {
  @App()
  app: koa.Application;
  @Get('/')
  async home(): Promise<boolean> {
    // this.app.getBaseDir() 获取当前项目基本目录,开发环境是src,打包过后是dist
    // 模型文件路径
    const casbinModelPath = join(this.app.getBaseDir(), '/basic_model.conf');
    // 策略文件路径
    const casbinPolicyPath = join(this.app.getBaseDir(), '/basic_policy.csv');
    // new一个casbin实例,有两个参数,一个是模型描述文件的路径,一个是策略文件的路径
    const e = await newEnforcer(casbinModelPath, casbinPolicyPath);
    // 这里判断bob这个人是否有data2的写权限
    // 从策略文件中可以看到bob是拥有data2的write权限的,所以这里应该返回为true
    // 策略文件里的内容
    // p, alice, data1, read
    // p, bob, data2, write
    const result = await e.enforce('bob', 'data2', 'write');
    console.log(true);
    return result;
  }
}
启动项目测试
在终端中使用npm run dev启动项目,项目启动成功后,访问http://127.0.0.1:7001/,可以看到和我们上面猜测的一样返回了true。

改一下代码,bob没有data2的read权限,这里应该返回fase。

和我们猜测一样

小结
这里我们简单的入了门,知道了如何在midway项目中使用casbin库。
model是什么
上面我们创建了一个basic_model.conf文件,可能大家对里面的内容有点迷惑,这里给大家解答一下。
model config至少包含四个部分,[request_definition], [policy_definition], [policy_effect], [matchers]。
request_definition
描述
[request_definition] 是访问请求的定义。 它定义了 e.Enforce(...) 函数中的参数。
[request_definition]
r = sub, obj, act
上面 sub, obj, act 表示经典三元组: 访问实体 (Subject),访问资源 (Object) 和访问方法 (Action)。 但是, 你可以自定义你自己的请求表单, 如果不需要指定特定资源,则可以这样定义 sub、act ,或者如果有两个访问实体, 则为 sub、sub2、obj、act。
小结
这里没啥好说的,文档已经很清楚了。
policy_definition
描述
[policy_definition] 是策略的定义。 它界定了该策略的含义。 例如,我们有以下模式:
[policy_definition]
p = sub, obj, act
p2 = sub, act
这些是我们对policy规则的具体描述
p, alice, data1, read
p2, bob, write-all-objects
policy部分的每一行称之为一个策略规则, 每条策略规则通常以形如p, p2的policy type开头。 如果存在多个policy定义,那么我们会根据前文提到的policy type与具体的某条定义匹配。 上面的policy的绑定关系将会在matcher中使用, 罗列如下:
(alice, data1, read) -> (p.sub, p.obj, p.act)
(bob, write-all-objects) -> (p2.sub, p2.act)
小结
看完上面描述,大家可能还有疑惑。这里我举个🌰。
我们刚才的例子中basic_model.conf文件里[policy_definition] 的配置是下面这样的:
[policy_definition]
p = sub, obj, act
然后我们的csv策略描述文件里的数据格式是这样的:
p, alice, data1, read
p, bob, data2, write
这个p和上面的p是对应的,bob相当于sub,data2相当于obj,write相当于act。
为啥要定义这个呢,因为有时候需要支持多种策略定义,后面说到RBAC模型的时候,再详细解释。
policy_effect
描述
[policy_effect] 部分是对policy生效范围的定义, 原语定义了当多个policy rule同时匹配访问请求request时,该如何对多个决策结果进行集成以实现统一决策。
小结
官方文档看完后,大家可能更迷惑,这里说一下我的理解。
[policy_effect]
e = some(where (p.eft == allow))
开始我一直不理解p.eft从哪来的,仔细看完文档后,才发现策略定义里面把这个省略了,最后一个参数就是eft,默认值都是allow。
策略定义中
[policy_definition]
p = sub, obj, act, eft
csv中
p, alice, data1, read, allow
p, bob, data2, write, allow
这样改造后大家应该理解了吧。
前面的some,表示如果匹配到了多个,只要有一个是allow就返回true。举个🌰:
csv中添加一条数据:
p, alice, data1, read, allow,
p, bob, data2, write, allow,
p, bob, data2, write, deny,
如果我们拿'bob', 'data2', 'write'去匹配,会匹配出两条数据,一个结果是allow,一个结果是deny,因为判断那里写了,只要有一个allow就返回true,所以匹配结果是true。
matchers
描述
[matchers] 是策略匹配器的定义。 匹配器是表达式。 它确定了如何根据请求评估策略规则。
[matchers]
m = r.sub == p.sub && r.obj == p.obj && r.act == p.act
上面的这个匹配器是最简单的,它表示请求的三元组:主题、对象、行为都应该匹配策略规则中的表达式。
在匹配器中,你可以使用算术运算符如 +, -, * , / ,也可以使用逻辑运算符如:&&,||,!。
内置匹配器函数
上面匹配表达式中除了使用简单的比较以外,还可以使用函数。casbin内置了一些常用的函数。

keyMatch2这个函数我们后面会用到,用来匹配动态参数接口。
自定义函数
如果上面函数不满足你的需求,还可以自定义函数。举个🌰

上面规则表示只要策略中的sub的其中一个值包含传过来的字符串,就返回true。
测试一下
const result = await e.enforce('b', 'data2', 'write');
因为bob包含b,所以肯定返回true
const result = await e.enforce('bb', 'data2', 'write');
因为上面策略中的sub的值没有包含bb的,所以肯定返回false。
小结
匹配器支持自定义函数,让这个库有更大的扩展空间。
RBAC模型实战
前言
下面我们用这个库实现接口鉴权功能,这个可以使用casbin内置的RBAC模型,这个模型实现了用户、角色、资源(接口)的权限控制。
model
可以从github上复制内置的RBAC模型配置,关于RBAC官方文档有讲解配置的含义。
[request_definition]
r = sub, obj, act
[policy_definition]
p = sub, obj, act
[role_definition]
g = _, _
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act
policy
可以从github上复制内置的RBAC策略示例数据,关于RBAC官方文档有讲解配置的含义。
p, alice, data1, read
p, bob, data2, write
p, data2_admin, data2, read
p, data2_admin, data2, write
g, alice, data2_admin
上面数据表示alice用户拥有data2_admin角色,data2_admin角色有data2资源的read权限和data2资源的write权限,所以alice用户有data2资源的read权限和data2资源的write权限,上面两行表示用户alice有date1资源的read权限,bob用户有data2的write权限。
测试

和我们猜测的一样,返回true

升级
如果把资源替换成接口呢,改造一个csv文件。
p, admin, /api/book, get
p, admin, /api/book, post
p, admin, /api/book, delete
p, user,  /api/book, get
g, 张三, user
g, 李四, admin
admin角色拥有/api/book这个接口的get,post,delete权限,user角色只有get权限。张三是user角色,李四是admin角色。
const result = await e.enforce('张三', '/api/book', 'get');

const result = await e.enforce('张三', '/api/book', 'post');

假设我们现在有个根据id获取单个book的接口,根据restful规范,接口应该设计成/api/book/:id,从前端拿到的请求url是/api/book/1这样的,那我们怎么匹配这种情况呢。
改造csv,给user角色添加一个/api/book/:id接口权限
p, admin, /api/book, get
p, admin, /api/book, post
p, admin, /api/book, delete
p, user,  /api/book, get
p, user,  /api/book/:id, get
g, 张三, user
g, 李四, admin
const result = await e.enforce('张三', '/api/book/1', 'get');
这样肯定返回false,因为model匹配那里写的是==,明显/api/book/1不等于/api/book/:id。这时候就需要用到内置函数keyMatch2了。
改造model文件


从数据库中加载策略
前言
上面我们都是从csv中加载的策略,有人会说,谁的管理系统会把用户、角色、接口这些信息存到csv中,一般都是存到数据库中。casbin支持从数据库中加载策略数据,并且已经有人写好了库,可以直接使用。
实战
安装依赖
pnpm i typeorm-adapter --save
pnpm i mysql2 --save
创建数据库
通过typeorm创建casbin实例
在项目启动的时候,创建一个单例service,全局每个地方都可以使用。
import { Singleton, Autoload, Init, App } from '@midwayjs/core';
import * as koa from '@midwayjs/koa';
import { Enforcer, newEnforcer } from 'casbin';
import { join } from 'path';
import TypeORMAdapter from 'typeorm-adapter';
@Autoload()
@Singleton()
export class CasbinService {
  @App()
  app: koa.Application;
  enforcer: Enforcer;
  @Init()
  async init() {
    const casbinModelPath = join(this.app.getBaseDir(), '/basic_model.conf');
    const adapter = await TypeORMAdapter.newAdapter({
      type: 'mysql',
      host: 'localhost',
      port: 3306,
      username: 'root',
      password: '12345678',
      database: 'casbin-demo',
    });
    // 这里创建casbin实例,第二个参数由以前的csv改成了从数据库中加载
    const e = await newEnforcer(casbinModelPath, adapter);
    // 从数据库中加载策略
    await e.loadPolicy();
    this.enforcer = e;
  }
}
启动项目后,发现数据库中自动创建了一个表。

把csv中的数据存迁移到数据库中

改造home.controler代码
import { App, Controller, Get, Inject } from '@midwayjs/core';
import * as koa from '@midwayjs/koa';
import { CasbinService } from '../casbin';
@Controller('/')
export class HomeController {
  @App()
  app: koa.Application;
  @Inject()
  casbinService: CasbinService;
  @Get('/')
  async home(): Promise<boolean> {
    const result = await this.casbinService.enforcer.enforce(
      '张三',
      '/api/book/1',
      'get'
    );
    return result;
  }
}
测试一下

总结
上面带着大家简单的入了一下门,至于怎么把系统中的用户、角色、接口信息转换成策略表的数据格式存到数据库中,我会在下一篇文章中以实战的方式分享给大家。
项目体验地址:fluxyadmin.cn/user/login
前端仓库地址:github.com/dbfu/fluxy-…
后端仓库地址:github.com/dbfu/fluxy-…
转载自:https://juejin.cn/post/7264920710786498572