应放在HTTP中间件处理功能级权限,数据级权限须在业务逻辑二次校验;RBAC优于ABAC,权限码用“资源:动作”格式,JWT仅存user_id和role_ids并配合Redis缓存带版本号的权限列表。

权限校验该放在 HTTP 中间件还是业务逻辑里?
绝大多数 Golang Web 项目应把权限校验放在 HTTP middleware 层,而不是每个 handler 里重复写 if !user.HasRole("admin")。中间件统一拦截、提前拒绝,既减少冗余代码,也避免遗漏导致越权访问。
但注意:RBAC 中的「数据级权限」(例如用户只能删自己创建的文章)不能只靠中间件解决,必须在业务逻辑中二次校验。中间件适合做「功能级权限」(能否访问 /api/users),业务层负责「实例级权限」(能否删 id=123 这条记录)。
- 中间件适合校验
role、permission code(如"user:delete")、JWT scope - 中间件不适合校验数据库关联字段(如
article.user_id == current_user.ID) - 使用
gorilla/mux或chi时,中间件可按路由组注册,比如adminRouter.Use(authMiddleware("admin"))
如何设计可扩展的权限模型:RBAC vs ABAC?
中小项目优先用 RBAC(基于角色的访问控制),结构清晰、易维护;ABAC(基于属性的访问控制)灵活但复杂度高,除非有明确需求(如动态策略引擎、多租户细粒度策略),否则不建议一上来就上 OPA 或自研策略 DSL。
RABC 实现关键是把「权限」抽象为字符串码("order:read"、"product:write"),而非硬编码角色名。角色只是权限集合的别名:
立即学习“go语言免费学习笔记(深入)”;
type Role struct {
Name string
Permissions []string `json:"permissions"`
}
// 数据库中存:
// role: "editor" → permissions: ["post:read", "post:write"]
// role: "viewer" → permissions: ["post:read"]
- 避免用
role == "admin"做判断,改用user.HasPermission("user:delete") - 权限码建议用
资源:动作格式,便于未来加命名空间(如"tenant1:order:read") - 缓存用户权限时,不要只缓存角色名,缓存展开后的完整权限码列表(避免每次查角色再 JOIN 权限表)
JWT token 里该不该放权限信息?
可以放,但必须设短过期时间(如 15–30 分钟),且服务端要支持权限变更后主动使 token 失效(通过 Redis 黑名单或版本号机制)。绝不能把权限长期固化在 JWT payload 里,否则管理员删了某人权限,对方 token 还能用到过期为止。
推荐做法是:JWT 只放 user_id 和 role_ids(非权限码),每次请求在中间件中查缓存获取最新权限列表:
func authMiddleware(requiredPerm string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
userID := getIDFromJWT(r)
perms, _ := cache.Get("perms:" + strconv.Itoa(userID)) // 从 Redis 查
if !contains(perms.([]string), requiredPerm) {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
next.ServeHTTP(w, r)
})
}
- JWT 中放
role_ids比放完整权限列表更安全:权限列表可能很长,JWT 膨胀影响传输和签名性能 - Redis 缓存 key 建议带版本号,如
"perms:123:v2",用户权限变更时只更新版本号,旧 token 自动失效 -
开发环境可加
debug=true参数,返回实际校验的权限列表,方便排查为什么某个接口被拒
常见越权漏洞怎么快速发现?
最常被忽略的是 ID 类型混淆和未校验上下文。比如 GET /api/orders/{id} 接口只校验了用户是否登录,但没确认该 id 是否属于当前用户——攻击者换一个别人订单 ID 就能直接读取。
- 所有带路径参数(
{id})、查询参数(?user_id=123)、请求体字段({"target_user_id": 456})的地方,都必须做归属校验 - 用
int做 ID 时,注意 0 或负数是否被当作“未设置”,绕过校验(应统一用uint或校验 > 0) - 数据库查询时,不要先
SELECT * FROM orders WHERE id = ?再判断 owner,而应直接WHERE id = ? AND user_id = ? - 测试阶段可用自动化脚本批量替换请求中的 ID 为其他用户 ID,看是否返回 200 或 403
权限不是加个中间件就完事。真正的难点永远在「这个请求里的那个 ID,到底归谁管」——这句话得在每个 handler 入口手动问一遍。










