Web 框架集成
Casbin 的判定入口只有一个 Enforce(),集成到 Web 框架的本质就是写一个中间件:从请求中取出 sub/obj/act,调用 Enforce,拒绝则 403。本章覆盖 Gin 中间件(配合 JWT)、gRPC 拦截器,以及权限管理接口的设计。
集成的整体流程
请求 → 认证中间件(JWT/Session → 得到 userID)
→ 授权中间件(Casbin:userID + Path + Method → Enforce)
→ 业务 Handler| 要素 | 来源 |
|---|---|
| sub | 认证中间件解析出的用户 ID / 角色(存入 Context) |
| obj | c.Request.URL.Path(RESTful)或业务资源 ID |
| act | c.Request.Method 或业务动作名 |
Gin:手写授权中间件(推荐)
配合 02 章 的 RESTful RBAC 模型(keyMatch2 + regexMatch):
package authz
import (
"net/http"
"github.com/casbin/casbin/v3"
"github.com/gin-gonic/gin"
)
func NewAuthz(e *casbin.SyncedEnforcer) gin.HandlerFunc {
return func(c *gin.Context) {
// 1. sub:由前置的 JWT 中间件放入 Context
sub, exists := c.Get("userID")
if !exists {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"msg": "未登录"})
return
}
// 2. obj / act:URL 路径与 HTTP 方法
obj := c.Request.URL.Path // 不含 query string,避免 keyMatch 踩坑
act := c.Request.Method
// 3. 判定
ok, err := e.Enforce(sub.(string), obj, act)
if err != nil {
// 模型/参数错误属于服务端问题,不能吞成 403
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"msg": "权限系统错误"})
return
}
if !ok {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"msg": "无权访问"})
return
}
c.Next()
}
}// 路由注册:认证在前,授权在后
r := gin.Default()
api := r.Group("/api")
api.Use(jwtAuth()) // 解析 token → c.Set("userID", claims.Sub)
api.Use(authz.NewAuthz(e)) // Casbin 判定
{
api.GET("/articles/:id", getArticle)
api.POST("/articles", createArticle)
}🚨 陷阱:中间件顺序写反(授权在认证前)时 c.Get("userID") 永远不存在,所有请求 401。另外登录、健康检查等公开路由不要挂授权中间件——放在 Group 之外,或在策略里给匿名角色放行。
与 JWT 的衔接:sub 放什么?
| 方案 | sub 内容 | g 行 | 特点 |
|---|---|---|---|
| A(推荐) | 用户 ID | g, user:1001, role:editor 存 Casbin |
角色变更即时生效;策略量大 |
| B | 角色名(JWT claims 里带角色) | 不需要 | 无 g 行、判定快;角色变更要等 token 过期 |
// 方案 B:token 里带角色,直接用角色做 sub
roles := claims.Roles // ["editor", "viewer"]
allowed := false
for _, role := range roles {
if ok, _ := e.Enforce(role, obj, act); ok {
allowed = true
break
}
}💡 最佳实践:默认选方案 A。踢人、降权要即时生效是刚需,方案 B 的"等 token 过期"在安全事件时不可接受。方案 B 适合角色极稳定的内部系统。
官方中间件 gin-contrib/authz
import "github.com/gin-contrib/authz"
r.Use(authz.NewAuthorizer(e)) // 内部:sub 取 HTTP Basic Auth 用户名它的 sub 取自 Basic Auth,JWT 体系下不适用——实际项目基本都是手写中间件,上面 30 行就是全部。
业务层判定(非 URL 粒度)
URL 粒度管不住"只能改自己的文章"这类对象级权限,在 Handler/Service 层再判一次:
func updateArticle(c *gin.Context) {
article := mustGetArticle(c) // 查出文章
userID := c.GetString("userID")
// 对象级判定:ABAC,article 作为结构体传入
ok, err := enforcer.Enforce(userID, article, "edit")
if err != nil || !ok {
c.JSON(http.StatusForbidden, gin.H{"msg": "不能编辑他人文章"})
return
}
// ... 执行更新
}💡 最佳实践:两层防线——中间件管 API 面(粗粒度 RBAC),业务层管对象面(细粒度 ABAC)。模型上可以用两个 matcher/两个 enforcer,也可以设计一个复合模型。
gRPC 拦截器
import (
"context"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
func AuthzInterceptor(e *casbin.SyncedEnforcer) grpc.UnaryServerInterceptor {
return func(ctx context.Context, req any, info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler) (any, error) {
userID, err := userFromContext(ctx) // 从 metadata 的 token 解析
if err != nil {
return nil, status.Error(codes.Unauthenticated, "未登录")
}
// obj 用 gRPC 全方法名,如 /pkg.ArticleService/CreateArticle
ok, err := e.Enforce(userID, info.FullMethod, "call")
if err != nil {
return nil, status.Error(codes.Internal, "权限系统错误")
}
if !ok {
return nil, status.Error(codes.PermissionDenied, "无权调用")
}
return handler(ctx, req)
}
}
// 注册
s := grpc.NewServer(grpc.ChainUnaryInterceptor(authInterceptor, AuthzInterceptor(e)))策略以方法名为资源:
p, role:editor, /pkg.ArticleService/*, call
p, role:viewer, /pkg.ArticleService/GetArticle, callmatcher 用 keyMatch(r.obj, p.obj) 支持 Service/* 通配。
权限管理接口(管理后台)
角色/权限的管理页面最终落到 04 章 的 API。典型接口设计:
// GET /admin/roles/:role/permissions —— 查看角色权限
func getRolePerms(c *gin.Context) {
perms, _ := e.GetFilteredPolicy(0, c.Param("role"))
c.JSON(200, perms)
}
// PUT /admin/users/:id/roles —— 重设用户角色
func setUserRoles(c *gin.Context) {
var req struct{ Roles []string `json:"roles"` }
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(400, gin.H{"msg": err.Error()})
return
}
user := "user:" + c.Param("id")
e.DeleteRolesForUser(user) // 先清后设
e.AddRolesForUser(user, req.Roles)
c.Status(204)
}🚨 陷阱:权限管理接口本身也要挂授权中间件(只有超管角色能调),并且要审计日志——谁在什么时候改了什么策略。EnforceEx 的命中策略、修改前后的 diff 都值得记。
常见陷阱
🚨 公开路由被拦:登录/注册/healthz 挂了授权中间件导致 401/403 死循环。公开路由独立分组,或显式给 role:anonymous 授权。
🚨 obj 用了带 query 的 URI:c.Request.RequestURI 含 ?page=1,keyMatch2 匹配失败。永远用 c.Request.URL.Path。
🚨 Enforce 的 error 当 403 处理:模型写错、参数个数不对返回的是 error。把 error 也回 403 会掩盖配置事故——error 回 500 并告警,!ok 才回 403。
🚨 每个请求 NewEnforcer:在中间件里每次新建 Enforcer(加载全部策略)是性能灾难。Enforcer 是进程级单例,随服务启动初始化一次。
🚨 JWT 里塞角色又在 Casbin 里存角色:两处角色来源不一致时行为诡异。选定方案 A 或 B 之一,不要混用。
🚨 裸 Enforcer 用于 Web 服务:管理接口写策略与业务请求 Enforce 并发,data race。中间件场景一律 SyncedEnforcer(见 08-分布式与高性能)。