Casbin 是一个强大的、高效的开源访问控制库,提供了灵活的权限管理能力。Casbin 支持多种访问控制模型,并且可以很容易地集成到现有的应用中。适用于各种编程语言和框架环境,包括但不限于 Go、Python、JavaScript、Java、PHP、.NET 等。
Casbin 有两个核心概念,模型(Model)和策略(Policy),模型连接了请求和策略之间的映射规则,资源的可访问控制都写在策略中。
casbin-flow.png

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 是另一种策略格式,它只有实体和方法两个部分,在策略文件中我们可以通过pp2两个标识来表明是那种策略。如下:

p, alice, data1, read
p2, bob, write

policy_effect

对policy生效范围的定义, 当定义了多个policy_definition同时匹配访问请求request时, 该如何对多个决策结果进行集成以实现统一决策。

[policy_effect]
e = some(where (p.eft == allow))

示例代码中的eft是策略的一个默认参数,有allowdeny两个可选值,默认是 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>

image.png
此时输入策略文件中的策略进行测试可以看下哦效果如下
image.png

如果是可定义的权限控制,那么使用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 })  
})

此时再进行校验效果依旧一样
image.png

然后就可以添加接口来实现策略的增删改查,进行权限的灵活控制了。

资源


前端小白