权限中间件必须在认证中间件之后注册,因认证中间件负责将user_id、role等信息写入context,权限中间件仅读取;顺序颠倒会导致空值panic。

权限中间件必须在认证之后注册
认证中间件(如 JWT 解析)负责把 user_id、role 等身份信息塞进 context.Context,权限中间件只负责读——如果顺序颠倒,ctx.Value("user_id") 就是 nil,后续所有校验都 panic。Gin 中用 router.Use(authMiddleware, permMiddleware);chi 中用 r.Use(AuthMiddleware).Use(PermMiddleware)。
容易踩的坑:
- 用字符串当 context key:
ctx.WithValue(ctx, "user_id", 123)→ 其他包也可能用"user_id"覆盖值 - 正确做法是定义自定义类型:
type userKey string; ctx.WithValue(ctx, userKey("user_id"), 123) - 认证中间件没做错误拦截:用户传错 token 时没调用
c.Abort()或http.Error(),请求继续往下走,权限中间件拿到空用户直接 panic
路径匹配别依赖 r.URL.Path 做字符串判断
r.URL.Path 是原始路径,不带路由变量标准化,比如 Gin 的 /users/:id 路由,实际请求可能是 /users/123 或 /users/123/,甚至 /users/123?foo=bar。直接用 strings.Contains(r.URL.Path, "/admin") 会被绕过,也不支持多级嵌套角色策略。
推荐做法:
- Gin 用
c.FullPath()获取注册时的路由模板(如/users/:id) - 手动正则替换
/:([^/]+)→/{\1},得到标准路径键/users/{id} - 预热加载时,就把
"GET:/users/{id}"映射到权限集,运行时只查 map - 显式放行公共路径:
if c.FullPath() == "/health" || c.Request.Method == "OPTIONS" { c.Next(); return }
RBAC 权限映射必须预热加载进内存
每次请求都查数据库(SELECT * FROM role_permissions WHERE role_id = ?)会拖垮服务。QPS 上千时,单次鉴权可能触发 3–5 次 SQL 查询,延迟飙升。
启动时一次性加载策略表,构建两级 map:
map[string]map[string][]string // 外层 key: "GET:/api/posts/{id}", 内层 key: "action", value: []string{"editor", "admin"}
关键细节:
- 动态路由需归一化:把
/posts/:id和/posts/:post_id都转成/posts/{id}再存,避免重复键 - 若权限会热更新,用
sync.RWMutex包裹 map,写操作加写锁,读操作只加读锁 - 别把权限字符串拼成
"post_read_123"这种带 ID 的 key——它无法复用,缓存命中率归零
Casbin 不是开箱即用的“银弹”
Casbin 默认行为是“查一次策略,跑一次匹配”,不是自动拦截。它只返回 true 或 false,你得自己写 if !e.Enforce(...) { c.AbortWithStatusJSON(403, ...) }。常见失效全是调用姿势问题。
真实代价集中在三处:
- 用
gorm-adapter时,e.Enforce("alice", "post:1001", "delete")可能触发 4 次 SQL 查询(用户→角色→角色权限→权限规则) -
enforcer实例不是 goroutine 安全的:多个请求并发调用e.Enforce,若未在初始化时加锁保护或复用不当,会 panic 报"invalid memory address" - 策略文件 reload 不及时:改了
model.conf或policy.csv,但没调用e.LoadPolicy(),新规则永远不生效
复杂点往往不在模型设计,而在边界:比如 /api/users/me 这类路径,需要从 JWT 中提取 user_id 动态拼出 "user:read:123",这时就不能靠静态 map 查表,得在 service 层做二次判断——这个逻辑容易被忽略,又没法用 Casbin 自动覆盖。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











