跳转至主要内容

在 Go 中实现 ABAC

基于角色的访问控制回答的是“编辑能做什么?”很多实际规则光有角色是不够的:作者可以编辑自己的草稿,编辑可以编辑本部门的文章,任何人都可以阅读已发布的文章。这些规则依赖用户和资源的属性,这正是基于属性的访问控制(ABAC)的用途。

本教程用 Go 标准库的 net/http 包搭建一个小型文章 API,并把所有这些规则放在一个 Casbin 策略文件中。同样的模型和策略可以原样用于本教程的 Node.js 和 Python 版本。如果你还在 RBAC 和 ABAC 之间犹豫,先读 选择访问控制模型。

1. 安装​

http.ServeMux 中的方法和通配符模式需要 Go 1.22 或更高版本。

go mod init example.com/abac
go get github.com/casbin/casbin/v3

2. 编写模型​

保存为 model.conf:

[request_definition]
r = sub, obj, act

[policy_definition]
p = sub_rule, obj_type, act

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

[matchers]
m = r.obj.Type == p.obj_type && r.act == p.act && eval(p.sub_rule)
  • 这里的 r.sub 和 r.obj 不是字符串,而是 Go 结构体。匹配器读取它们的字段,例如 r.obj.Type。
  • 每条策略规则都在 sub_rule 中带一个条件。 eval(p.sub_rule) 针对请求对这个条件求值,所以规则放在策略中,而不是模型中。

3. 编写策略​

保存为 policy.csv:

p, r.obj.Status == 'published', article, read
p, r.sub.Name == r.obj.Owner, article, read
p, r.sub.Name == r.obj.Owner && r.obj.Status == 'draft', article, update
p, r.sub.Role == 'editor' && r.sub.Department == r.obj.Department, article, update
p, r.sub.Role == 'editor' && r.sub.Department == r.obj.Department, article, publish

每一行可以读作“当条件为真时,允许对文章执行这个操作”:

  1. 任何人都可以阅读已发布的文章。
  2. 作者可以阅读自己的文章,包括草稿。
  3. 作者可以在文章还是草稿时编辑自己的文章。
  4. 编辑可以编辑本部门的任何文章。
  5. 编辑可以发布本部门的文章。
提示

条件中的字符串字面量用单引号,例如 'published'。字段中间出现双引号会破坏 CSV 解析。如果条件中需要逗号,用双引号把整个字段括起来。

4. 编写服务器​

ABAC 需要资源本身,而不只是它的 URL,所以检查在文章加载之后进行。 authorize 辅助函数负责这一步,答案为否时写出错误响应。

main.go
package main

import (
"encoding/json"
"log"
"net/http"
"sync"

"github.com/casbin/casbin/v3"
)

type User struct {
Name string
Role string
Department string
}

type Article struct {
ID string `json:"id"`
Type string `json:"-"`
Title string `json:"title"`
Owner string `json:"owner"`
Department string `json:"department"`
Status string `json:"status"`
}

// In a real application these come from your database.
var users = map[string]User{
"alice": {Name: "alice", Role: "author", Department: "engineering"},
"bob": {Name: "bob", Role: "editor", Department: "engineering"},
"carol": {Name: "carol", Role: "editor", Department: "sales"},
"dave": {Name: "dave", Role: "author", Department: "engineering"},
}

var (
mu sync.Mutex
articles = map[string]*Article{
"1": {ID: "1", Type: "article", Title: "Draft by alice", Owner: "alice", Department: "engineering", Status: "draft"},
"2": {ID: "2", Type: "article", Title: "Published by alice", Owner: "alice", Department: "engineering", Status: "published"},
}
)

type server struct {
e *casbin.Enforcer
}

// authorize loads the current user and asks Casbin whether they may perform act on the article.
func (s *server) authorize(w http.ResponseWriter, r *http.Request, a *Article, act string) bool {
// Replace this with the user from your session or JWT.
user, ok := users[r.Header.Get("X-User")]
if !ok {
http.Error(w, "unauthenticated", http.StatusUnauthorized)
return false
}
allowed, err := s.e.Enforce(user, *a, act)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return false
}
if !allowed {
http.Error(w, "forbidden", http.StatusForbidden)
return false
}
return true
}

func (s *server) article(w http.ResponseWriter, r *http.Request) *Article {
mu.Lock()
defer mu.Unlock()
a, ok := articles[r.PathValue("id")]
if !ok {
http.NotFound(w, r)
return nil
}
return a
}

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

mux := http.NewServeMux()
mux.HandleFunc("GET /articles/{id}", func(w http.ResponseWriter, r *http.Request) {
a := s.article(w, r)
if a == nil || !s.authorize(w, r, a, "read") {
return
}
json.NewEncoder(w).Encode(a)
})
mux.HandleFunc("PUT /articles/{id}", func(w http.ResponseWriter, r *http.Request) {
a := s.article(w, r)
if a == nil || !s.authorize(w, r, a, "update") {
return
}
mu.Lock()
json.NewDecoder(r.Body).Decode(&struct {
Title *string `json:"title"`
}{&a.Title})
mu.Unlock()
json.NewEncoder(w).Encode(a)
})
mux.HandleFunc("POST /articles/{id}/publish", func(w http.ResponseWriter, r *http.Request) {
a := s.article(w, r)
if a == nil || !s.authorize(w, r, a, "publish") {
return
}
mu.Lock()
a.Status = "published"
mu.Unlock()
json.NewEncoder(w).Encode(a)
})

log.Fatal(http.ListenAndServe(":8080", mux))
}

Casbin 通过反射读取结构体字段,所以策略中用到的字段(Name、Owner、Status 等)必须是导出的。

备注

示例用 X-User 请求头保持简短。 Casbin 只负责授权;实际应用中,用户来自你的认证层,例如经过校验的 JWT 或会话。

5. 试一试​

go run .

文章 1 是 alice 的草稿,文章 2 是她已发布的文章,都属于 engineering 部门。

curl -i -H "X-User: dave"  http://localhost:8080/articles/1                 # 403, someone else's draft
curl -i -H "X-User: dave" http://localhost:8080/articles/2 # 200, published
curl -i -X PUT -H "X-User: alice" -d '{"title":"New title"}' \
http://localhost:8080/articles/1 # 200, her own draft
curl -i -X PUT -H "X-User: alice" -d '{"title":"x"}' \
http://localhost:8080/articles/2 # 403, no longer a draft
curl -i -X PUT -H "X-User: bob" -d '{"title":"x"}' \
http://localhost:8080/articles/2 # 200, editor in engineering
curl -i -X PUT -H "X-User: carol" -d '{"title":"x"}' \
http://localhost:8080/articles/2 # 403, editor in sales
curl -i -X POST -H "X-User: alice" http://localhost:8080/articles/1/publish # 403, authors cannot publish
curl -i -X POST -H "X-User: bob" http://localhost:8080/articles/1/publish # 200
curl -i -H "X-User: dave" http://localhost:8080/articles/1 # 200, now published

6. 在运行时修改规则​

条件就是策略中的行,所以可以在服务器运行时添加和删除,并通过 适配器 保存到数据库:

// Admins may edit any article.
e.AddPolicy("r.sub.Role == 'admin'", "article", "update")

// Nobody may publish any more.
e.RemoveFilteredPolicy(1, "article", "publish")

要知道是哪条规则允许了请求,使用 EnforceEx。它返回匹配的规则,可用于审计日志:

ok, rule, _ := e.EnforceEx(users["bob"], *articles["2"], "update")
// true [r.sub.Role == 'editor' && r.sub.Department == r.obj.Department article update]

ABAC 还是 RBAC?​

  • 当权限跟随岗位职责、且不依赖访问的是哪条记录时,使用 RBAC。它更容易审计:“列出编辑能做的所有事情”就是一个简单的查询。见 Go RBAC 教程。
  • 当答案取决于记录本身(所有者、部门、状态或敏感度)时,使用 ABAC。
  • 大多数应用两者结合使用:角色决定用户能使用哪些功能,属性再把范围缩小到他能操作的记录。访问控制模型对比 中给出了结合两者的模型。

下一步​

  • 完整的 ABAC 参考。 ABAC 详细介绍 JSON 请求参数、引号规则和 eval()。
  • 基于时间和地点的规则。 增加更多请求字段,例如用 ipMatch 检查 IP 地址,或把当前时间作为属性传入。见 函数。
  • 拒绝规则。 加一个 eft 列并使用拒绝优先的效果,这样一条规则就能拦下其他规则允许的操作。见 Effector。
  • 在浏览器中试验。 在线编辑器 可以评估带 JSON 属性的 ABAC 请求。

Comments are powered by giscus and GitHub Discussions. They load from giscus.app and GitHub only after you click the button below.