Skip to content
Go
Web 框架集成

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, call

matcher 用 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 的 URIc.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-分布式与高性能)。