Casbin学习教程

Casbin 学习教程(Go 版)

本文档面向正在学习 Golang 的开发者,从零开始系统讲解 Casbin 权限控制框架。

读完并完成文末练习后,你应该能够:理解 Casbin 的核心模型、编写 Model/Policy 文件、在 Go 项目中集成 Casbin,并实现 RBAC、ABAC 等常见权限方案。


目录

  1. [Casbin 是什么](#Casbin 是什么)
  2. 核心概念速览
  3. [PERM 元模型](#PERM 元模型)
  4. [Model 文件详解](#Model 文件详解)
  5. [Policy 策略文件详解](#Policy 策略文件详解)
  6. [Go 快速上手](#Go 快速上手)
  7. [RBAC 角色权限](#RBAC 角色权限)
  8. [RBAC with Domains(多租户)](#RBAC with Domains(多租户))
  9. [ABAC 属性权限](#ABAC 属性权限)
  10. [RESTful 路径匹配](#RESTful 路径匹配)
  11. [Adapter 持久化](#Adapter 持久化)
  12. 生产环境真实做法(重要)
  13. [Enforcer API 常用方法](#Enforcer API 常用方法)
  14. [在 Web 项目中集成](#在 Web 项目中集成)
  15. 最佳实践与常见坑
  16. 动手练习
  17. 参考资源

1. Casbin 是什么

Casbin 是一个开源的、支持多种访问控制模型的**授权(Authorization)**库。

需要区分两个概念:

概念 英文 职责 常见方案
认证 Authentication 确认「你是谁」 JWT、Session、OAuth
授权 Authorization 确认「你能做什么」 Casbin、OPA

Casbin 不负责登录,只负责在已知用户身份后,判断该用户是否允许执行某个操作。

为什么用 Casbin?

  • 模型与策略分离:权限规则写在配置文件里,改规则不必改业务代码
  • 表达力强:支持 ACL、RBAC、ABAC、RESTful 等
  • 跨语言:Go、Java、Python、Node.js 等均有官方 SDK
  • 可持久化:策略可存文件、MySQL、Redis 等

2. 核心概念速览

Casbin 的工作可以概括为四件事:

复制代码
请求(Request) + 策略(Policy) → 匹配器(Matcher) → 效果(Effect) → 允许/拒绝

四个核心组件:

组件 说明 存放位置
Model 定义权限模型结构(请求长什么样、怎么匹配) model.conf
Policy 具体的权限规则数据 policy.csv 或数据库
Enforcer 执行引擎,加载 Model + Policy 并做判断 Go 代码中创建
Adapter 策略持久化适配器(文件、DB 等) 可选

一次鉴权的完整流程

#mermaid-svg-O5X6lmriNG2SlJBu{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-O5X6lmriNG2SlJBu .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-O5X6lmriNG2SlJBu .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-O5X6lmriNG2SlJBu .error-icon{fill:#552222;}#mermaid-svg-O5X6lmriNG2SlJBu .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-O5X6lmriNG2SlJBu .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-O5X6lmriNG2SlJBu .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-O5X6lmriNG2SlJBu .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-O5X6lmriNG2SlJBu .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-O5X6lmriNG2SlJBu .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-O5X6lmriNG2SlJBu .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-O5X6lmriNG2SlJBu .marker{fill:#333333;stroke:#333333;}#mermaid-svg-O5X6lmriNG2SlJBu .marker.cross{stroke:#333333;}#mermaid-svg-O5X6lmriNG2SlJBu svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-O5X6lmriNG2SlJBu p{margin:0;}#mermaid-svg-O5X6lmriNG2SlJBu .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-O5X6lmriNG2SlJBu .cluster-label text{fill:#333;}#mermaid-svg-O5X6lmriNG2SlJBu .cluster-label span{color:#333;}#mermaid-svg-O5X6lmriNG2SlJBu .cluster-label span p{background-color:transparent;}#mermaid-svg-O5X6lmriNG2SlJBu .label text,#mermaid-svg-O5X6lmriNG2SlJBu span{fill:#333;color:#333;}#mermaid-svg-O5X6lmriNG2SlJBu .node rect,#mermaid-svg-O5X6lmriNG2SlJBu .node circle,#mermaid-svg-O5X6lmriNG2SlJBu .node ellipse,#mermaid-svg-O5X6lmriNG2SlJBu .node polygon,#mermaid-svg-O5X6lmriNG2SlJBu .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-O5X6lmriNG2SlJBu .rough-node .label text,#mermaid-svg-O5X6lmriNG2SlJBu .node .label text,#mermaid-svg-O5X6lmriNG2SlJBu .image-shape .label,#mermaid-svg-O5X6lmriNG2SlJBu .icon-shape .label{text-anchor:middle;}#mermaid-svg-O5X6lmriNG2SlJBu .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-O5X6lmriNG2SlJBu .rough-node .label,#mermaid-svg-O5X6lmriNG2SlJBu .node .label,#mermaid-svg-O5X6lmriNG2SlJBu .image-shape .label,#mermaid-svg-O5X6lmriNG2SlJBu .icon-shape .label{text-align:center;}#mermaid-svg-O5X6lmriNG2SlJBu .node.clickable{cursor:pointer;}#mermaid-svg-O5X6lmriNG2SlJBu .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-O5X6lmriNG2SlJBu .arrowheadPath{fill:#333333;}#mermaid-svg-O5X6lmriNG2SlJBu .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-O5X6lmriNG2SlJBu .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-O5X6lmriNG2SlJBu .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-O5X6lmriNG2SlJBu .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-O5X6lmriNG2SlJBu .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-O5X6lmriNG2SlJBu .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-O5X6lmriNG2SlJBu .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-O5X6lmriNG2SlJBu .cluster text{fill:#333;}#mermaid-svg-O5X6lmriNG2SlJBu .cluster span{color:#333;}#mermaid-svg-O5X6lmriNG2SlJBu div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-O5X6lmriNG2SlJBu .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-O5X6lmriNG2SlJBu rect.text{fill:none;stroke-width:0;}#mermaid-svg-O5X6lmriNG2SlJBu .icon-shape,#mermaid-svg-O5X6lmriNG2SlJBu .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-O5X6lmriNG2SlJBu .icon-shape p,#mermaid-svg-O5X6lmriNG2SlJBu .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-O5X6lmriNG2SlJBu .icon-shape .label rect,#mermaid-svg-O5X6lmriNG2SlJBu .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-O5X6lmriNG2SlJBu .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-O5X6lmriNG2SlJBu .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-O5X6lmriNG2SlJBu :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是

应用收到请求
构造 Casbin 请求参数
Enforcer.Enforce
Matcher 匹配 Policy?
Effect: allow
Effect: deny
放行
403 Forbidden


3. PERM 元模型

Casbin 基于 PERM 元模型(Policy、Effect、Request、Matcher):

复制代码
+--------+     +--------+     +----------+     +--------+
| Request| --> | Policy | --> | Matcher  | --> | Effect |
+--------+     +--------+     +----------+     +--------+
  谁要什么        规则库         匹配逻辑         最终决策
  • Request(r) :一次鉴权请求的参数,如 (用户, 资源, 动作)
  • Policy(p):存储的权限规则
  • Matcher(m):表达式,决定 Request 与 Policy 是否匹配
  • Effect(e) :多条规则命中时的汇总策略(some(where (p.eft == allow)) 表示有一条 allow 就通过)

4. Model 文件详解

Model 文件通常命名为 model.conf,由多个 [section] 组成。

4.1 最简单的 ACL 模型

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
逐段解释

[request_definition] --- 定义请求元组

复制代码
r = sub, obj, act
字段 含义 示例
sub Subject,主体(用户/角色) alice
obj Object,资源 data1
act Action,动作 read

[policy_definition] --- 定义策略元组,结构与 request 对应

[policy_effect] --- 最终效果

ini 复制代码
e = some(where (p.eft == allow))

含义:只要存在一条 allow 策略匹配,就允许访问。这是最常见的写法。

另一种写法(白名单 + 显式 deny):

ini 复制代码
e = some(where (p.eft == allow)) && !some(where (p.eft == deny))

[matchers] --- 匹配表达式

ini 复制代码
m = r.sub == p.sub && r.obj == p.obj && r.act == p.act

Casbin 内置了 keyMatchkeyMatch2regexMatch 等函数,后面会用到。

4.2 带角色的 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

[role_definition] 是 RBAC 的关键:

ini 复制代码
g = _, _

表示 g 是一个二元关系:g, 用户, 角色,即「用户拥有某角色」。

g(r.sub, p.sub) 的含义:请求中的用户 r.sub 是否拥有策略中定义的角色 p.sub


5. Policy 策略文件详解

重要说明policy.csv 主要用于学习、本地调试和单元测试

实际项目中几乎不会让人手工改 CSV,而是通过数据库 + 管理后台/API 动态维护策略。详见 [第 12 章](#第 12 章)。

Policy 文件通常命名为 policy.csv,每行一条规则。

5.1 ACL 策略示例

对应上面的 ACL 模型:

csv 复制代码
p, alice, data1, read
p, alice, data1, write
p, bob, data2, read

格式:p, 字段1, 字段2, 字段3p 表示 policy 行)

5.2 RBAC 策略示例

csv 复制代码
p, admin, data1, read
p, admin, data1, write
p, admin, data2, read
p, admin, data2, write
p, user, data1, read

g, alice, admin
g, bob, user
  • p 行:定义角色能做什么
  • g 行:定义用户 拥有什么角色

因此 alice 通过 g 继承 admin 的全部权限。

5.3 带 deny 的策略

Casbin 默认每条 policy 的 effect 为 allow。也可以显式写 deny:

csv 复制代码
p, alice, data1, read, allow
p, alice, data1, write, deny

需要在 policy_definition 中增加 eft 字段:

ini 复制代码
p = sub, obj, act, eft

6. Go 快速上手

6.1 环境准备

bash 复制代码
# 创建项目目录
mkdir casbin-demo && cd casbin-demo
go mod init casbin-demo

# 安装 Casbin
go get github.com/casbin/casbin/v2

6.2 项目结构

复制代码
casbin-demo/
├── go.mod
├── main.go
├── model.conf
└── policy.csv

6.3 创建 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

6.4 创建 policy.csv

csv 复制代码
p, alice, data1, read
p, bob, data2, write

6.5 编写 main.go

go 复制代码
package main

import (
	"fmt"
	"log"

	"github.com/casbin/casbin/v2"
)

func main() {
	// 创建 Enforcer:传入 model 路径和 policy 路径
	e, err := casbin.NewEnforcer("model.conf", "policy.csv")
	if err != nil {
		log.Fatalf("创建 Enforcer 失败: %v", err)
	}

	// Enforce(主体, 资源, 动作) → bool
	ok, _ := e.Enforce("alice", "data1", "read")
	fmt.Println("alice 读 data1:", ok) // true

	ok, _ = e.Enforce("alice", "data1", "write")
	fmt.Println("alice 写 data1:", ok) // false

	ok, _ = e.Enforce("bob", "data2", "write")
	fmt.Println("bob 写 data2:", ok) // true
}

6.6 运行

bash 复制代码
go run main.go

预期输出:

复制代码
alice 读 data1: true
alice 写 data1: false
bob 写 data2: true

6.7 代码讲解

  1. NewEnforcer(modelPath, policyPath)

    加载模型和策略,返回执行引擎。也可以只传 model,后续用 LoadPolicy() 加载。

  2. Enforce(...)

    参数顺序必须与 [request_definition] 中定义的字段一致。返回 (bool, error)

  3. 错误处理

    生产环境务必检查 error;教程为简洁有时用 _ 忽略。


7. RBAC 角色权限

RBAC(Role-Based Access Control)是 Casbin 最常用的模式。

7.1 模型文件 model_rbac.conf

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

7.2 策略文件 policy_rbac.csv

csv 复制代码
p, admin, /api/users, GET
p, admin, /api/users, POST
p, admin, /api/users, DELETE
p, editor, /api/articles, GET
p, editor, /api/articles, POST
p, viewer, /api/articles, GET

g, alice, admin
g, bob, editor
g, charlie, viewer

7.3 Go 示例

go 复制代码
package main

import (
	"fmt"
	"log"

	"github.com/casbin/casbin/v2"
)

func main() {
	e, err := casbin.NewEnforcer("model_rbac.conf", "policy_rbac.csv")
	if err != nil {
		log.Fatal(err)
	}

	tests := []struct {
		user, obj, act string
		want           bool
	}{
		{"alice", "/api/users", "DELETE", true},
		{"bob", "/api/articles", "POST", true},
		{"bob", "/api/users", "GET", false},
		{"charlie", "/api/articles", "GET", true},
		{"charlie", "/api/articles", "POST", false},
	}

	for _, t := range tests {
		ok, _ := e.Enforce(t.user, t.obj, t.act)
		status := "✓"
		if ok != t.want {
			status = "✗"
		}
		fmt.Printf("%s %s %s %s → %v (期望 %v)\n", status, t.user, t.act, t.obj, ok, t.want)
	}
}

7.4 角色继承

Casbin 支持角色的层级继承:

csv 复制代码
g, admin, superadmin
g, alice, admin

g 可以嵌套:alice → admin → superadmin。

在 matcher 中使用 g 的变体实现传递闭包:

ini 复制代码
[role_definition]
g = _, _

[matchers]
m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act

Casbin 的 g 函数默认支持多级继承(递归查找)。

7.5 动态管理角色

go 复制代码
// 给用户添加角色
_, err := e.AddRoleForUser("david", "editor")

// 删除用户的角色
_, err = e.DeleteRoleForUser("david", "editor")

// 获取用户的所有角色
roles, err := e.GetRolesForUser("alice")

// 获取拥有某角色的所有用户
users, err := e.GetUsersForRole("admin")

// 保存到 policy 文件(若使用文件 Adapter)
err = e.SavePolicy()

8. RBAC with Domains(多租户)

多租户 SaaS 场景中,同一用户在不同租户(domain)可能有不同角色。

8.1 模型

ini 复制代码
[request_definition]
r = sub, dom, obj, act

[policy_definition]
p = sub, dom, obj, act

[role_definition]
g = _, _, _

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

[matchers]
m = g(r.sub, p.sub, r.dom) && r.dom == p.dom && r.obj == p.obj && r.act == p.act

注意 g = _, _, _ 是三元组:用户, 角色, 域

8.2 策略

csv 复制代码
p, admin, tenant1, data1, read
p, admin, tenant1, data1, write
p, user, tenant1, data1, read
p, admin, tenant2, data2, read

g, alice, admin, tenant1
g, bob, user, tenant1
g, alice, user, tenant2

8.3 Go 调用

go 复制代码
// alice 在 tenant1 是 admin,可以写
ok, _ := e.Enforce("alice", "tenant1", "data1", "write") // true

// alice 在 tenant2 只是 user,tenant2 的 data2 没有 user 读权限
ok, _ = e.Enforce("alice", "tenant2", "data2", "read") // false

9. ABAC 属性权限

ABAC(Attribute-Based Access Control)根据属性做决策,比 RBAC 更灵活。

9.1 场景

规则:「年龄小于 18 的用户不能购买 alcohol 类商品」。

9.2 模型

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.Age >= 18 && r.obj.Category != "alcohol" || r.obj.Category == "alcohol" && r.sub.Age >= 18

更清晰的写法:

ini 复制代码
[matchers]
m = r.sub.Age >= 18 || r.obj.Category != "alcohol"

9.3 Go 中使用结构体

go 复制代码
package main

import (
	"fmt"
	"log"

	"github.com/casbin/casbin/v2"
)

type User struct {
	Name string
	Age  int
}

type Resource struct {
	Name     string
	Category string
}

func main() {
	e, err := casbin.NewEnforcer("model_abac.conf", "policy_abac.csv")
	if err != nil {
		log.Fatal(err)
	}

	alice := User{Name: "alice", Age: 25}
	bob := User{Name: "bob", Age: 16}

	book := Resource{Name: "Go语言编程", Category: "book"}
	wine := Resource{Name: "红酒", Category: "alcohol"}

	fmt.Println(e.Enforce(alice, book, "buy"))  // true
	fmt.Println(e.Enforce(bob, book, "buy"))    // true
	fmt.Println(e.Enforce(alice, wine, "buy"))  // true
	fmt.Println(e.Enforce(bob, wine, "buy"))    // false
}

ABAC 的 policy.csv 可以为空,规则全在 matcher 中;也可以结合 policy 做混合模型。

9.4 内置匹配函数

Casbin 在 matcher 中提供了丰富的函数:

函数 说明 示例
keyMatch(key1, key2) 路径前缀匹配 keyMatch("/foo/bar", "/foo/*") → true
keyMatch2(key1, key2) :id 的 RESTful 匹配 keyMatch2("/foo/1", "/foo/:id") → true
keyMatch3(key1, key2) 忽略 {} 内内容
regexMatch(key1, pattern) 正则匹配
ipMatch(ip1, ip2) IP 段匹配
globMatch(str, pattern) Glob 模式匹配

10. RESTful 路径匹配

Web API 权限常用路径 + HTTP Method 作为 (obj, act)

10.1 模型

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) && keyMatch2(r.obj, p.obj) && regexMatch(r.act, p.act)

10.2 策略

csv 复制代码
p, admin, /api/users, (GET|POST|PUT|DELETE)
p, user, /api/users/:id, GET
p, user, /api/profile, GET

g, alice, admin
g, bob, user

10.3 测试

go 复制代码
e.Enforce("bob", "/api/users/123", "GET")  // true(匹配 /api/users/:id)
e.Enforce("bob", "/api/users", "POST")     // false
e.Enforce("alice", "/api/users", "DELETE") // true

11. Adapter 持久化

默认 NewEnforcer 使用文件 Adapter,策略存在 CSV 中。生产环境通常用数据库。

11.1 文件 Adapter(默认)

go 复制代码
e, _ := casbin.NewEnforcer("model.conf", "policy.csv")

11.2 MySQL Adapter

bash 复制代码
go get github.com/casbin/gorm-adapter/v3
go 复制代码
import (
	"github.com/casbin/casbin/v2"
	gormadapter "github.com/casbin/gorm-adapter/v3"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

func main() {
	dsn := "user:password@tcp(127.0.0.1:3306)/casbin?charset=utf8mb4&parseTime=True"
	db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{})

	adapter, _ := gormadapter.NewAdapterByDB(db)
	e, _ := casbin.NewEnforcer("model.conf", adapter)

	// 从数据库加载策略
	_ = e.LoadPolicy()

	// 动态添加规则后保存
	_, _ = e.AddPolicy("alice", "data3", "read")
	_ = e.SavePolicy()
}

11.3 Redis Watcher(多实例同步)

多个服务实例共享策略时,一个实例修改策略后需通知其他实例 reload:

bash 复制代码
go get github.com/casbin/redis-watcher/v2
go 复制代码
w, _ := rediswatcher.NewWatcher("127.0.0.1:6379", rediswatcher.WatcherOptions{})
_ = e.SetWatcher(w)
_ = w.SetUpdateCallback(func(msg string) {
	_ = e.LoadPolicy()
})

12. 生产环境真实做法(重要)

你的直觉是对的:线上项目基本不会通过 CSV 文件维护权限。CSV 在前面的章节出现,是因为它能帮你快速理解 Casbin 的策略格式;真正上线时,做法完全不同。

12.1 开发 vs 生产:对比一览

维度 学习 / 本地开发 生产环境
Policy 存放 policy.csv 文件 MySQL / PostgreSQL 等数据库
Policy 修改方式 手工编辑文件 管理后台REST API 动态增删
Model 存放 model.conf 文件 仍是文件居多(改动少),或嵌入代码
多实例部署 单机,无感 需要 Watcher 通知其他节点 reload
谁维护权限 开发者 运营 / 管理员通过后台操作

12.2 生产环境的典型架构

#mermaid-svg-gKO8DfDKPOYq45kD{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-gKO8DfDKPOYq45kD .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-gKO8DfDKPOYq45kD .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-gKO8DfDKPOYq45kD .error-icon{fill:#552222;}#mermaid-svg-gKO8DfDKPOYq45kD .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-gKO8DfDKPOYq45kD .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-gKO8DfDKPOYq45kD .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-gKO8DfDKPOYq45kD .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-gKO8DfDKPOYq45kD .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-gKO8DfDKPOYq45kD .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-gKO8DfDKPOYq45kD .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-gKO8DfDKPOYq45kD .marker{fill:#333333;stroke:#333333;}#mermaid-svg-gKO8DfDKPOYq45kD .marker.cross{stroke:#333333;}#mermaid-svg-gKO8DfDKPOYq45kD svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-gKO8DfDKPOYq45kD p{margin:0;}#mermaid-svg-gKO8DfDKPOYq45kD .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-gKO8DfDKPOYq45kD .cluster-label text{fill:#333;}#mermaid-svg-gKO8DfDKPOYq45kD .cluster-label span{color:#333;}#mermaid-svg-gKO8DfDKPOYq45kD .cluster-label span p{background-color:transparent;}#mermaid-svg-gKO8DfDKPOYq45kD .label text,#mermaid-svg-gKO8DfDKPOYq45kD span{fill:#333;color:#333;}#mermaid-svg-gKO8DfDKPOYq45kD .node rect,#mermaid-svg-gKO8DfDKPOYq45kD .node circle,#mermaid-svg-gKO8DfDKPOYq45kD .node ellipse,#mermaid-svg-gKO8DfDKPOYq45kD .node polygon,#mermaid-svg-gKO8DfDKPOYq45kD .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-gKO8DfDKPOYq45kD .rough-node .label text,#mermaid-svg-gKO8DfDKPOYq45kD .node .label text,#mermaid-svg-gKO8DfDKPOYq45kD .image-shape .label,#mermaid-svg-gKO8DfDKPOYq45kD .icon-shape .label{text-anchor:middle;}#mermaid-svg-gKO8DfDKPOYq45kD .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-gKO8DfDKPOYq45kD .rough-node .label,#mermaid-svg-gKO8DfDKPOYq45kD .node .label,#mermaid-svg-gKO8DfDKPOYq45kD .image-shape .label,#mermaid-svg-gKO8DfDKPOYq45kD .icon-shape .label{text-align:center;}#mermaid-svg-gKO8DfDKPOYq45kD .node.clickable{cursor:pointer;}#mermaid-svg-gKO8DfDKPOYq45kD .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-gKO8DfDKPOYq45kD .arrowheadPath{fill:#333333;}#mermaid-svg-gKO8DfDKPOYq45kD .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-gKO8DfDKPOYq45kD .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-gKO8DfDKPOYq45kD .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-gKO8DfDKPOYq45kD .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-gKO8DfDKPOYq45kD .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-gKO8DfDKPOYq45kD .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-gKO8DfDKPOYq45kD .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-gKO8DfDKPOYq45kD .cluster text{fill:#333;}#mermaid-svg-gKO8DfDKPOYq45kD .cluster span{color:#333;}#mermaid-svg-gKO8DfDKPOYq45kD div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-gKO8DfDKPOYq45kD .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-gKO8DfDKPOYq45kD rect.text{fill:none;stroke-width:0;}#mermaid-svg-gKO8DfDKPOYq45kD .icon-shape,#mermaid-svg-gKO8DfDKPOYq45kD .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-gKO8DfDKPOYq45kD .icon-shape p,#mermaid-svg-gKO8DfDKPOYq45kD .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-gKO8DfDKPOYq45kD .icon-shape .label rect,#mermaid-svg-gKO8DfDKPOYq45kD .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-gKO8DfDKPOYq45kD .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-gKO8DfDKPOYq45kD .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-gKO8DfDKPOYq45kD :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 业务请求
后端服务
管理端
允许
拒绝
AddRoleForUser / AddPolicy
SavePolicy
Watcher 通知
权限管理后台
角色/菜单/接口配置
权限 API
Casbin Enforcer
casbin_rule 表
Enforce 鉴权
用户请求 API
JWT 认证
业务 Handler
403
其他服务实例 Reload

核心思路:

  1. Model(model.conf):定义「权限怎么判断」,上线后很少改,继续用配置文件或内嵌字符串即可。
  2. Policy(策略数据) :存在数据库的 casbin_rule 表里,通过代码 API 读写,不手工改文件。
  3. 管理后台:管理员勾选「角色能访问哪些菜单/接口」,后端调用 Casbin API 写入数据库。

12.3 数据库表长什么样

使用 gorm-adapter 后,Casbin 会自动建一张表(默认名 casbin_rule):

id ptype v0 v1 v2 v3 v4 v5
1 p admin /api/users GET
2 p admin /api/users POST
3 g alice admin
  • ptype = p:权限规则(对应 policy.csv 的 p, ...
  • ptype = g:角色绑定(对应 policy.csv 的 g, alice, admin
  • v0, v1, v2...:就是 CSV 里逗号后面的各字段

你在后台做的每一次「给用户分配角色」,底层就是往这张表插一条 g 记录。

12.4 生产级初始化代码

go 复制代码
package casbinx

import (
	"sync"

	"github.com/casbin/casbin/v2"
	gormadapter "github.com/casbin/gorm-adapter/v3"
	"gorm.io/gorm"
)

var (
	enforcer *casbin.Enforcer
	once     sync.Once
)

// Init 应用启动时调用一次,全局单例
func Init(db *gorm.DB, modelPath string) error {
	var err error
	once.Do(func() {
		adapter, e := gormadapter.NewAdapterByDB(db)
		if e != nil {
			err = e
			return
		}
		enforcer, e = casbin.NewEnforcer(modelPath, adapter)
		if e != nil {
			err = e
			return
		}
		_ = enforcer.LoadPolicy()
	})
	return err
}

func Enforcer() *casbin.Enforcer {
	return enforcer
}
go 复制代码
// main.go
func main() {
	db := initDB()
	if err := casbinx.Init(db, "configs/rbac_model.conf"); err != nil {
		log.Fatal(err)
	}
	// 启动 HTTP 服务...
}

12.5 权限管理 API(这才是日常开发写的)

管理员在后台操作,后端提供类似这样的接口:

go 复制代码
// POST /admin/roles/:roleId/permissions  给角色添加接口权限
func AddRolePermission(c *gin.Context) {
	role := c.Param("roleId")
	path := c.PostForm("path")   // /api/users
	method := c.PostForm("method") // GET

	e := casbinx.Enforcer()
	added, err := e.AddPolicy(role, path, method)
	if err != nil {
		c.JSON(500, gin.H{"error": err.Error()})
		return
	}
	if added {
		_ = e.SavePolicy() // 持久化到数据库
	}
	c.JSON(200, gin.H{"ok": true})
}

// POST /admin/users/:userId/roles  给用户分配角色
func AssignUserRole(c *gin.Context) {
	userID := c.Param("userId")
	role := c.PostForm("role")

	e := casbinx.Enforcer()
	_, err := e.AddRoleForUser(userID, role)
	if err != nil {
		c.JSON(500, gin.H{"error": err.Error()})
		return
	}
	_ = e.SavePolicy()
	c.JSON(200, gin.H{"ok": true})
}

// DELETE /admin/users/:userId/roles/:role  移除用户角色
func RemoveUserRole(c *gin.Context) {
	userID := c.Param("userId")
	role := c.Param("role")

	e := casbinx.Enforcer()
	_, _ = e.DeleteRoleForUser(userID, role)
	_ = e.SavePolicy()
	c.JSON(200, gin.H{"ok": true})
}

// GET /admin/users/:userId/permissions  查询用户拥有的所有权限
func GetUserPermissions(c *gin.Context) {
	userID := c.Param("userId")
	e := casbinx.Enforcer()
	perms, _ := e.GetImplicitPermissionsForUser(userID)
	c.JSON(200, gin.H{"permissions": perms})
}

日常开发流程:写这些管理 API + 鉴权中间件,而不是编辑 CSV。

12.6 首次部署:种子数据(Seed)

虽然不用 CSV,但项目第一次部署时,通常需要初始化默认角色和权限(如 superadmin)。做法是用代码写入,而不是维护一个 policy.csv:

go 复制代码
// 应用启动时执行一次,或通过 migration 脚本执行
func SeedDefaultPolicies(e *casbin.Enforcer) error {
	defaults := [][]string{
		{"p", "superadmin", "/api/*", "(GET|POST|PUT|DELETE)"},
		{"p", "admin", "/api/users", "(GET|POST)"},
		{"g", "admin", "superadmin"}, // 可选:角色继承
	}

	for _, rule := range defaults {
		ptype := rule[0]
		params := rule[1:]
		if ptype == "p" {
			exists, _ := e.HasPolicy(params)
			if !exists {
				_, _ = e.AddPolicy(params)
			}
		}
		if ptype == "g" {
			_, _ = e.AddGroupingPolicy(params)
		}
	}
	return e.SavePolicy()
}

12.7 和自建 RBAC 表的关系

很多项目本身已有 sys_usersys_rolesys_menu 等表。常见两种集成方式:

方式 A:Casbin 作为唯一策略源(推荐,简单)

复制代码
用户表 sys_user  ──→  user_id
角色表 sys_role  ──→  role_code  ──→  同步到 casbin_rule (g 规则)
菜单/接口        ──→  path+method ──→  同步到 casbin_rule (p 规则)

后台改角色权限时,同时更新 Casbin(AddPolicy + SavePolicy)。

方式 B:自建表为主,Casbin 只做鉴权引擎

业务表是主数据,每次变更后调用 LoadPolicy() 或自定义 Adapter 从业务表读取。适合已有成熟 RBAC 系统的迁移项目。

方式 C:不用 Casbin 存 Policy,只复用 Model + Matcher

少数团队把权限存在 Redis/业务表里,鉴权时手动构造参数调用 Enforce。这种用法较少,一般直接用 Adapter 更省心。

12.8 Model 文件在生产中放哪

做法 说明
配置文件 configs/rbac_model.conf 最常见,随代码仓库管理,变更走 Code Review
嵌入代码 //go:embed model.conf 容器化部署时少一个外部文件依赖
配置中心 超大团队、需热更新 Model 时(极少见)

Model 定义的是「匹配逻辑」,上线后通常几个月才改一次;真正频繁变动的是 Policy(谁有什么权限),所以 Policy 放数据库,Model 放文件是业界主流。

12.9 一次完整的权限变更链路

以「给运营小王开通 editor 角色」为例:

复制代码
1. 管理员在后台点击「分配角色」
2. 前端 POST /admin/users/1001/roles  { role: "editor" }
3. 后端 e.AddRoleForUser("1001", "editor")
4. 后端 e.SavePolicy()  → 写入 casbin_rule 表
5. (多实例)Watcher 广播 → 其他 Pod 执行 LoadPolicy()
6. 小王下次请求 → 中间件 e.Enforce("1001", "/api/articles", "POST") → true

12.10 小结:你需要记住的

复制代码
学习阶段:model.conf + policy.csv   ← 理解格式用
生产阶段:model.conf + 数据库 + 管理API ← 实际干活用
  • CSV 不是生产方案,只是 Casbin 策略的「文本表示形式」
  • 数据库里的 casbin_rule 表,本质上就是 CSV 的内容结构化存储
  • 你的业务代码重点是:Enforcer 单例 + 鉴权中间件 + 权限管理 CRUD API

13. Enforcer API 常用方法

13.1 鉴权

go 复制代码
ok, err := e.Enforce(sub, obj, act)
ok, err := e.Enforce(sub, dom, obj, act) // 多租户

13.2 策略 CRUD

go 复制代码
// 添加
e.AddPolicy("alice", "data1", "read")
e.AddPolicies([][]string{{"alice", "data2", "read"}, {"bob", "data2", "write"}})

// 删除
e.RemovePolicy("alice", "data1", "read")
e.RemoveFilteredPolicy(0, "alice") // 删除 alice 的所有策略

// 查询
e.GetPolicy()                              // 所有 p 规则
e.GetFilteredPolicy(0, "alice")          // 第一字段为 alice 的规则
e.HasPolicy("alice", "data1", "read")      // 是否存在

13.3 角色管理

go 复制代码
e.AddRoleForUser("alice", "admin")
e.DeleteRoleForUser("alice", "admin")
e.DeleteRolesForUser("alice")
e.GetRolesForUser("alice")
e.GetUsersForRole("admin")
e.HasRoleForUser("alice", "admin")

13.4 批量鉴权

go 复制代码
// 获取用户所有允许的权限(需配合 GetFilteredPolicy 或自定义)
roles, _ := e.GetImplicitRolesForUser("alice")
permissions, _ := e.GetPermissionsForUser("alice")

13.5 其他实用方法

go 复制代码
e.LoadPolicy()    // 重新加载策略
e.SavePolicy()    // 保存策略
e.ClearPolicy()   // 清空内存中的策略
e.EnableEnforce(true)  // 启用/禁用鉴权(禁用后 Enforce 全返回 true)
e.EnableLog(true)      // 开启调试日志

14. 在 Web 项目中集成

以下以 Gin 为例(Casbin 也提供官方 middleware)。

14.1 安装

bash 复制代码
go get github.com/gin-gonic/gin
go get github.com/casbin/casbin/v2

14.2 中间件示例

go 复制代码
package middleware

import (
	"net/http"

	"github.com/casbin/casbin/v2"
	"github.com/gin-gonic/gin"
)

func CasbinAuth(e *casbin.Enforcer) gin.HandlerFunc {
	return func(c *gin.Context) {
		// 从 JWT / Session 中获取用户(此处简化)
		user := c.GetString("username")
		if user == "" {
			c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "未登录"})
			return
		}

		obj := c.Request.URL.Path
		act := c.Request.Method

		ok, err := e.Enforce(user, obj, act)
		if err != nil {
			c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "鉴权失败"})
			return
		}
		if !ok {
			c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "无权限"})
			return
		}

		c.Next()
	}
}

14.3 路由注册

go 复制代码
package main

import (
	"github.com/casbin/casbin/v2"
	"github.com/gin-gonic/gin"

	"casbin-demo/middleware"
)

func main() {
	e, _ := casbin.NewEnforcer("model_rbac.conf", "policy_rbac.csv")

	r := gin.Default()

	// 模拟登录后设置 username
	r.Use(func(c *gin.Context) {
		c.Set("username", "alice") // 实际从 token 解析
		c.Next()
	})

	api := r.Group("/api", middleware.CasbinAuth(e))
	{
		api.GET("/users", listUsers)
		api.POST("/users", createUser)
		api.DELETE("/users/:id", deleteUser)
	}

	r.Run(":8080")
}

func listUsers(c *gin.Context)   { c.JSON(200, gin.H{"msg": "用户列表"}) }
func createUser(c *gin.Context)  { c.JSON(200, gin.H{"msg": "创建用户"}) }
func deleteUser(c *gin.Context) { c.JSON(200, gin.H{"msg": "删除用户"}) }

14.4 集成思路总结

复制代码
HTTP 请求
  ↓
认证中间件(JWT)→ 得到 user
  ↓
Casbin 中间件 → Enforce(user, path, method)
  ↓
业务 Handler

15. 最佳实践与常见坑

15.1 最佳实践

  1. Model 稳定,Policy 灵活

    Model 定义后尽量少改;权限变更只改 Policy 或数据库记录。

  2. 用角色而非直接给用户配权限

    用户量大时,g 关系比大量 p 规则更易维护。

  3. 路径匹配选对函数

    • 简单前缀:keyMatch
    • RESTful 带参数:keyMatch2
    • 复杂规则:regexMatch
  4. 生产环境用 DB Adapter

    文件 Adapter 适合开发;线上需要动态增删权限、多实例部署。

  5. Enforcer 单例

    Enforcer 创建有开销,应用启动时创建一次,全局复用。

  6. 修改策略后 SavePolicy

    动态修改后调用 SavePolicy() 持久化,否则重启丢失。

15.2 常见坑

问题 原因 解决
Enforce 始终 false matcher 写错或 policy 无匹配 e.EnableLog(true) 看日志
改 policy 不生效 内存未 reload e.LoadPolicy()
参数顺序错误 Enforce 参数与 request_definition 不一致 严格对照 model
角色继承不生效 role_definition 未定义或 matcher 未用 g() 检查 model
ABAC 结构体字段访问失败 字段需导出(大写开头) Age 而非 age
性能问题 每次请求 NewEnforcer 使用单例 + 缓存

15.3 调试技巧

go 复制代码
e.EnableLog(true) // 打印 matcher 匹配过程

// 查看当前加载的全部策略
policies, _ := e.GetPolicy()
for _, p := range policies {
	fmt.Println(p)
}

16. 动手练习

建议按顺序完成,巩固本章内容。

练习 1:基础 ACL

  1. 创建 ACL 模型和策略
  2. 实现:adminreport 有 read/write;guestreport 只有 read
  3. 用 Go 写测试用例验证

参考答案(点击展开)

policy.csv

csv 复制代码
p, admin, report, read
p, admin, report, write
p, guest, report, read

main.go 片段

go 复制代码
e, _ := casbin.NewEnforcer("model.conf", "policy.csv")
fmt.Println(e.Enforce("admin", "report", "write")) // true
fmt.Println(e.Enforce("guest", "report", "write")) // false

练习 2:RBAC 角色继承

  1. 定义角色:superadmin > admin > user
  2. superadmin 可 DELETE /api/*
  3. admin 可 POST /api/*
  4. user 可 GET /api/*
  5. 验证 alice 是 admin 时,不能 DELETE(未继承 superadmin)

参考答案(点击展开)

policy.csv

csv 复制代码
p, superadmin, /api/*, DELETE
p, admin, /api/*, POST
p, user, /api/*, GET

g, superadmin, admin
g, admin, user
g, alice, admin

model.conf matcher

ini 复制代码
m = g(r.sub, p.sub) && keyMatch(r.obj, p.obj) && r.act == p.act
go 复制代码
e.Enforce("alice", "/api/users", "POST")   // true
e.Enforce("alice", "/api/users", "DELETE") // false

练习 3:多租户

  1. 租户 companyAcompanyB
  2. 同一用户 tom 在 A 是 admin,在 B 是 viewer
  3. 实现 domain 模型并测试

提示

使用 [8. RBAC with Domains](#8. RBAC with Domains) 的模型,Enforce 四个参数:(user, domain, obj, act)


练习 4:Gin 集成

  1. 搭建 Gin 服务
  2. 接入 Casbin 中间件
  3. 用 curl 测试有权限和无权限的请求
bash 复制代码
# 有权限(alice + GET /api/users)
curl -i http://localhost:8080/api/users

# 可在中间件里临时把 user 改成 bob 测试 403

17. 参考资源

资源 链接
Casbin 官网 https://casbin.org/
Casbin Go 文档 https://casbin.org/docs/overview
Model 语法 https://casbin.org/docs/syntax-for-models
Policy 语法 https://casbin.org/docs/syntax-for-policy
在线编辑器 https://casbin.org/editor/
GitHub https://github.com/casbin/casbin

附录 A:完整可运行 RBAC 示例项目

将以下内容保存后可直接 go run main.go

目录结构

复制代码
casbin-rbac-demo/
├── go.mod
├── main.go
├── model.conf
└── policy.csv

go.mod

go 复制代码
module casbin-rbac-demo

go 1.21

require github.com/casbin/casbin/v2 v2.100.0

model.conf

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) && keyMatch2(r.obj, p.obj) && regexMatch(r.act, p.act)

policy.csv

csv 复制代码
p, admin, /api/users, (GET|POST|PUT|DELETE)
p, editor, /api/articles, (GET|POST)
p, viewer, /api/articles, GET

g, alice, admin
g, bob, editor
g, charlie, viewer

main.go

go 复制代码
package main

import (
	"fmt"
	"log"

	"github.com/casbin/casbin/v2"
)

func main() {
	e, err := casbin.NewEnforcer("model.conf", "policy.csv")
	if err != nil {
		log.Fatal(err)
	}

	type testCase struct {
		user, path, method string
		expect             bool
	}

	cases := []testCase{
		{"alice", "/api/users", "DELETE", true},
		{"alice", "/api/users/1", "GET", true},
		{"bob", "/api/articles", "POST", true},
		{"bob", "/api/users", "GET", false},
		{"charlie", "/api/articles", "GET", true},
		{"charlie", "/api/articles", "POST", false},
	}

	fmt.Println("=== Casbin RBAC 鉴权测试 ===")
	for _, c := range cases {
		ok, err := e.Enforce(c.user, c.path, c.method)
		if err != nil {
			log.Printf("鉴权出错: %v", err)
			continue
		}
		mark := "PASS"
		if ok != c.expect {
			mark = "FAIL"
		}
		fmt.Printf("[%s] %s %s %s => %v (期望 %v)\n", mark, c.user, c.method, c.path, ok, c.expect)
	}

	fmt.Println("\n=== 动态添加权限 ===")
	_, _ = e.AddRoleForUser("david", "viewer")
	ok, _ := e.Enforce("david", "/api/articles", "GET")
	fmt.Println("david GET /api/articles:", ok) // true

	_, _ = e.AddPolicy("viewer", "/api/profile", "GET")
	ok, _ = e.Enforce("charlie", "/api/profile", "GET")
	fmt.Println("charlie GET /api/profile:", ok) // true
}

相关推荐
techdashen1 小时前
Go 语言仓库 Top 100 贡献者分析报告
开发语言·后端·golang
何以解忧,唯有..1 小时前
Go 语言变量命名规范详解
开发语言·后端·golang
迷茫运维路2 小时前
【client-go源码学习记录一】调用链精读-从kubeconfig到ListPods
golang·client-go
何以解忧,唯有..3 小时前
Go 语言运算符详解:从基础到实战
开发语言·后端·golang
迷茫运维路4 小时前
Golang架构目录设计与设计模式教程
设计模式·golang
省四收割者4 小时前
从硬件中断到分布式协程:全景解构高并发机制与 C / Golang 的巅峰对决
c++·分布式·嵌入式硬件·golang
pixcarp14 小时前
知识库系统的内容资产闭环怎么设计
服务器·数据库·后端·golang
张忠琳17 小时前
【Go 1.26.4】Golang Select 深度解析
开发语言·后端·golang
提笔了无痕18 小时前
如何用Go实现整套RAG流程
开发语言·后端·golang