Casbin 是一个强大的、高效的开源访问控制库,提供了灵活的权限管理能力。Casbin 支持多种访问控制模型,并且可以很容易地集成到现有的应用中。适用于各种编程语言和框架环境,包括但不限于 Go、Python、JavaScript、Java、PHP、.NET 等。
Casbin 有两个核心概念,模型(Model)和策略(Policy),模型连接了请求和策略之间的映射规则,资源的可访问控制都写在策略中。
Casbin 的职责很明确,只负责资源的权限控制,不会进行其他的额外处理,包括身份认证、用户管理等。
可以理解为 Casbin 假定走到这一层的请求都是已经完成了前置处理的。
模型
模型相当于一个配置文件,告诉 Casbin 该如何对请求进行鉴权,一个模型文件最少具有四个部分,如下
[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
第一次看这个 model 文件确实会很迷惑,下面来逐步拆解 model 各部分的含义和作用。
request_definition
访问请求的定义,它定义了 e.Enforce(…) 函数中的参数。
[request_definition] r = sub, obj, act
sub, obj, act 是经典三元组: 访问实体 (Subject),访问资源 (Object) 和访问方法 (Action)。格式并不是固定的,可以根据需求随意定义
policy_definition
策略的定义,它定义了策略的格式,在进行权限校验时会根据定义的策略格式和 policy 集合进行比对。比如有下面两条策略
[policy_definition] p = sub, obj, act p2 = sub, act
p
是一种策略格式,它有实体、资源和方法三个部分,p2
是另一种策略格式,它只有实体和方法两个部分,在策略文件中我们可以通过p
和p2
两个标识来表明是那种策略。如下:
p, alice, data1, read p2, bob, write
policy_effect
对policy生效范围的定义, 当定义了多个policy_definition同时匹配访问请求request时, 该如何对多个决策结果进行集成以实现统一决策。
[policy_effect] e = some(where (p.eft == allow))
示例代码中的eft
是策略的一个默认参数,有allow
和 deny
两个可选值,默认是 allow,some是一个函数,功能和JS的Array.some一样,只要有一个allow,就表示允许访问(专业术语:allow-overrides)。
matchers
策略匹配器的定义,匹配器是一组表达式,确定了如何根据请求确定生效的策略规则。
[matchers] m = r.sub == p.sub && r.obj == p.obj && r.act == p.act
这是一个最简单的匹配器,它要求请求和策略的三元组必须完全一样。在匹配器中可以使用算术运算和逻辑运算来组合表达式。
另外 Casbin 也内置了一些常用的函数来方便使用
函数 | url | 模式 | 例子 |
---|---|---|---|
keyMatch | 像 /alice_data/resource1 这样的 URL路径 | 像 /alice_data/* 这样的URL路 径或 * 模式 | 地址 |
keyMatch2 | 像 /alice_data/resource1 这样的 URL路径 | 像 /alice_data/:resource 这 样的URL路径或 : 模式 | 地址 |
keyMatch3 | 像 /alice_data/resource1 这样的 URL路径 | 像 /alice_data/{resource} 这 样的URL路径或 {} 模式 | 地址 |
keyMatch4 | 像 /alice_data/123/book/123 这 样的URL路径 | 像 /alice_data/{id}/book/{id} 这样的URL路径或 {} 模式 | 地址 |
keyMatch5 | 像 /alice_data/123/?status=1 这 样的URL路径 | 像 /alice_data/{id}/* 这样的 URL路径, {} 或 * 模式 | 地址 |
regexMatch | 任何字符串 | 一个正则表达式模式 | 地址 |
ipMatch | 像 192.168.2.123 这样的IP地址 | 像 192.168.2.0/24 这样的IP地 址或CIDR | 地址 |
globalMatch | 像 /alice_data/resource1 这样的 路径样式路径 | 像 /alice_data/* 这样的glob模 式 | 地址 |
如果这些内置函数不满足需求,也可以自定义函数来进行处理(不同的语言包添加方式不一样,比如NodeJS是通过enforcer.addFunction(name, func) 的方式添加的)。 |
策略
Casbin 的策略支持多种存储方式,最基础的方式是 LocalPolicy,即数据保存在本地.csv
文件中,内容格式如下
p, alice, data1, read p, bob, data2, write
每一行都是一个条策略规则,每一行具体的内容要和模型中定义的 policy_definition
对应的上 。
现在我们有了模型和策略就可以开始代码实战了,首先安装依赖,为了方便测试我们用 koa 起一个服务
$ pnpm i casbin koa koa-router @ladjs/koa-views ejs koa-bodyparser koa-logger -S
依赖安装完成之后创建应用程序
import Koa from 'koa' import KoaLogger from 'koa-logger' import KoaBodyParser from 'koa-bodyparser' import views from '@ladjs/koa-views' import router from './src/router/index.ts' import { dirname } from "node:path"; import { fileURLToPath } from 'node:url' const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); import KoaRouter from 'koa-router' import { newEnforcer } from 'casbin' const router = new KoaRouter(); router.get('/', async (ctx) => { await ctx.render('form.ejs', { withResult: false }) }) router.post('/check_roles', async (ctx) => { const enforcer = await newEnforcer('basic_model.conf', 'basic_policy.csv') const { subject, resource, action } = ctx.request.body as Record<string, unknown> const res = await enforcer.enforce(subject, resource, action); console.log(ctx.request.body, ' => ', res) await ctx.render('form.ejs', { result: res ? 'success' : 'failed', withResult: true }) }) export default router const app = new Koa() app .use(KoaLogger()) .use(KoaBodyParser()) .use(views(__dirname + '/src/views', { map: { ejs: 'ejs' } })) .use(router.routes()) .use(router.allowedMethods()) app.listen(3000, () => { console.log('server started, listening on [ 3000 ]') })
模板文件就是一个基础的表单,用来提交数据
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous"> </head> <body> <div class="card" style="width: 400px;margin: 100px auto"> <div class="card-body"> <form action="/check_roles" method="post" > <div class="mb-3"> <label for="subject" class="form-label">用户标识</label> <input type="text" class="form-control" name="subject" /> </div> <div class="mb-3"> <label for="resource" class="form-label">访问资源</label> <input type="text" class="form-control" name="resource" /> </div> <div class="mb-3"> <label for="action" class="form-label">动作</label> <input type="text" class="form-control" name="action" /> </div> <button type="submit" class="btn btn-primary">检查</button> </form> </div> </div> <% if(withResult) { %> <h2 style="text-align: center;"> <%= result %> </h2> <% } %> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script> </body> </html>
此时输入策略文件中的策略进行测试可以看下哦效果如下
如果是可定义的权限控制,那么使用csv这种形式就不太方便了, Casbin 支持使用数据库进行持久化的数据加载和保存。只需要安装对应的数据库适配器即可,支持的适配器可以文档查询。下面我们就把策略文件替换为sqlite适配器,安装适配器和驱动
$ pnpm i sqlite3 casbin-basic-adapter -S
然后创建一个数据库文件,使用SQL语句进行建表,casbin适配器默认读取casbin_rule表,所以表名需要指定这个
CREATE TABLE casbin_rule ( id INTEGER PRIMARY KEY AUTOINCREMENT, -- 自动递增的主键 ptype TEXT NOT NULL, -- 字符串类型的字段,不允许为空 v0 TEXT, -- 字符串类型的字段 v1 TEXT, -- 字符串类型的字段 v2 TEXT, -- 字符串类型的字段 v3 TEXT, -- 字符串类型的字段 v4 TEXT, -- 字符串类型的字段 v5 TEXT, -- 字符串类型的字段 v6 TEXT -- 字符串类型的字段 ); -- 插入第一行数据: p, alice, data1, read INSERT INTO casbin_rule (ptype, v0, v1, v2) VALUES ('p', 'alice', 'data1', 'read'); -- 插入第二行数据: p, bob, data2, write INSERT INTO casbin_rule (ptype, v0, v1, v2) VALUES ('p', 'bob', 'data2', 'write');
添加一个数据库连接文件
import sqlite from 'sqlite3' import {fileURLToPath} from "node:url"; import {dirname, resolve} from "node:path"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const sqlite3 = sqlite.verbose() const db = new sqlite3.Database(resolve(__dirname, '../db/casbin.db')) export default db
将之前从csv中读取的代码改为通过适配器加载
router.post('/check_roles', async (ctx) => { const adapter = await BasicAdapter.newAdapter('sqlite3', db); const enforcer = await newEnforcer('basic_model.conf', adapter) const { subject, resource, action } = ctx.request.body as Record<string, unknown> const res = await enforcer.enforce(subject, resource, action); console.log(ctx.request.body, ' => ', res, ' By SQLite') await ctx.render('form.ejs', { result: res ? 'success' : 'failed', withResult: true }) })
此时再进行校验效果依旧一样
然后就可以添加接口来实现策略的增删改查,进行权限的灵活控制了。