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 })
})
此时再进行校验效果依旧一样
然后就可以添加接口来实现策略的增删改查,进行权限的灵活控制了。