往期回顾
前端框架搭建------从零开始搭建一个高颜值后台管理系统全栈框架(一)
后端框架搭建------从零开始搭建一个高颜值后台管理系统全栈框架(二)
实现登录功能jwt or token+redis?------从零开始搭建一个高颜值后台管理系统全栈框架(三)
封装axios,让请求变得丝滑------从零开始搭建一个高颜值后台管理系统全栈框架(四)
实现前后端全自动化部署,解放你的双手。------从零开始搭建一个高颜值后台管理系统全栈框架(五)
雪花算法,附件方案,邮箱验证,修改密码。------从零开始搭建一个高颜值后台管理系统全栈框架(六)
基于react-router v6实现动态菜单、动态路由。内含vue动态路由实现。------从零开始搭建一个高颜值后台管理系统全栈框架(七)
通过RBAC模型实现前后端动态菜单和动态路由------从零开始搭建一个高颜值后台管理系统全栈框架(八)
使用黑科技实现前端按钮权限控制,太优雅了。------从零开始搭建一个高颜值后台管理系统全栈框架(九)
前言
前面我们实现了按钮权限控制,但是只在前端控制按钮显示和隐藏并没有多大用处,别人只要知道接口,可以通过一些请求工具直接调你的接口,所以后端在接受到请求的时候首先判断用户有没有权限,有权限则通过,无权限则拒绝访问。
接口鉴权这里我推荐使用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项目。
sh
npm init midway
安装casbin依赖
sh
pnpm i casbin --save
创建casbin模型描述文件
在项目src目录下创建basic_model.conf
文件,文件内容如下:
ini
[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
文件,文件内容如下:
csv
p, alice, data1, read
p, bob, data2, write
后面讲解这些内容的含义
在home.controler中使用casbin方法
ts
// 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(...) 函数中的参数。
config
[request_definition]
r = sub, obj, act
上面 sub, obj, act 表示经典三元组: 访问实体 (Subject),访问资源 (Object) 和访问方法 (Action)。 但是, 你可以自定义你自己的请求表单, 如果不需要指定特定资源,则可以这样定义 sub、act ,或者如果有两个访问实体, 则为 sub、sub2、obj、act。
小结
这里没啥好说的,文档已经很清楚了。
policy_definition
描述
[policy_definition]
是策略的定义。 它界定了该策略的含义。 例如,我们有以下模式:
ini
[policy_definition]
p = sub, obj, act
p2 = sub, act
这些是我们对policy规则的具体描述
arduino
p, alice, data1, read
p2, bob, write-all-objects
policy部分的每一行称之为一个策略规则, 每条策略规则通常以形如p, p2的policy type开头。 如果存在多个policy定义,那么我们会根据前文提到的policy type与具体的某条定义匹配。 上面的policy的绑定关系将会在matcher中使用, 罗列如下:
css
(alice, data1, read) -> (p.sub, p.obj, p.act)
(bob, write-all-objects) -> (p2.sub, p2.act)
小结
看完上面描述,大家可能还有疑惑。这里我举个🌰。
我们刚才的例子中basic_model.conf
文件里[policy_definition]
的配置是下面这样的:
ini
[policy_definition]
p = sub, obj, act
然后我们的csv策略描述文件里的数据格式是这样的:
arduino
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时,该如何对多个决策结果进行集成以实现统一决策。
小结
官方文档看完后,大家可能更迷惑,这里说一下我的理解。
ini
[policy_effect]
e = some(where (p.eft == allow))
开始我一直不理解p.eft从哪来的,仔细看完文档后,才发现策略定义里面把这个省略了,最后一个参数就是eft,默认值都是allow。
策略定义中
ini
[policy_definition]
p = sub, obj, act, eft
csv中
arduino
p, alice, data1, read, allow
p, bob, data2, write, allow
这样改造后大家应该理解了吧。
前面的some,表示如果匹配到了多个,只要有一个是allow就返回true。举个🌰:
csv中添加一条数据:
arduino
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]
是策略匹配器的定义。 匹配器是表达式。 它确定了如何根据请求评估策略规则。
css
[matchers]
m = r.sub == p.sub && r.obj == p.obj && r.act == p.act
上面的这个匹配器是最简单的,它表示请求的三元组:主题、对象、行为都应该匹配策略规则中的表达式。
在匹配器中,你可以使用算术运算符如 +, -, * , / ,也可以使用逻辑运算符如:&&,||,!。
内置匹配器函数
上面匹配表达式中除了使用简单的比较以外,还可以使用函数。casbin内置了一些常用的函数。
keyMatch2这个函数我们后面会用到,用来匹配动态参数接口。
自定义函数
如果上面函数不满足你的需求,还可以自定义函数。举个🌰
上面规则表示只要策略中的sub的其中一个值包含传过来的字符串,就返回true。
测试一下
ts
const result = await e.enforce('b', 'data2', 'write');
因为bob包含b,所以肯定返回true
ts
const result = await e.enforce('bb', 'data2', 'write');
因为上面策略中的sub的值没有包含bb的,所以肯定返回false。
小结
匹配器支持自定义函数,让这个库有更大的扩展空间。
RBAC模型实战
前言
下面我们用这个库实现接口鉴权功能,这个可以使用casbin内置的RBAC模型,这个模型实现了用户、角色、资源(接口)的权限控制。
model
可以从github上复制内置的RBAC模型配置,关于RBAC官方文档有讲解配置的含义。
ini
[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官方文档有讲解配置的含义。
arduino
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文件。
javascript
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角色。
ts
const result = await e.enforce('张三', '/api/book', 'get');
ts
const result = await e.enforce('张三', '/api/book', 'post');
假设我们现在有个根据id获取单个book的接口,根据restful规范,接口应该设计成/api/book/:id
,从前端拿到的请求url是/api/book/1
这样的,那我们怎么匹配这种情况呢。
改造csv,给user角色添加一个/api/book/:id
接口权限
javascript
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
ts
const result = await e.enforce('张三', '/api/book/1', 'get');
这样肯定返回false,因为model匹配那里写的是==
,明显/api/book/1
不等于/api/book/:id
。这时候就需要用到内置函数keyMatch2了。
改造model文件
从数据库中加载策略
前言
上面我们都是从csv中加载的策略,有人会说,谁的管理系统会把用户、角色、接口这些信息存到csv中,一般都是存到数据库中。casbin支持从数据库中加载策略数据,并且已经有人写好了库,可以直接使用。
实战
安装依赖
sh
pnpm i typeorm-adapter --save
pnpm i mysql2 --save
创建数据库
这个不会自动创建数据库,需要我们自己建一个数据库,使用工具连接数据库创建casbin-demo
数据库。不用自己建表,typeorm-adapter
会自动帮我们建表。数据库方面的知识可以看下我这篇文章juejin.cn/post/723673...。
通过typeorm创建casbin实例
在项目启动的时候,创建一个单例service,全局每个地方都可以使用。
ts
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代码
ts
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-...