应将鉴权中间件挂载到各版本group而非根引擎,通过c.fullpath()动态识别路径前缀并查策略表实现细粒度权限控制,认证与授权须分离且严格分两步执行。

直接挂全局中间件 r.Use(AuthMiddleware()) 会导致所有版本、所有路径共用同一套校验逻辑,根本无法区分「普通用户能访问 /v1/users,但不能碰 /v2/admin」这类细粒度需求——越权漏洞就藏在这里。
中间件必须挂到 Group 而不是根引擎上
把鉴权中间件注册在 r.Use() 是最常见也最危险的写法。它会让 /v1/ 和 /v2/ 共享同一个 c.Abort() 流程,一旦某个中间件提前终止上下文,后续中间件(比如日志或跨域)可能收不到通知,甚至业务 handler 根本不执行。
- 正确姿势:每个版本 Group 自己挂自己的中间件,例如
v1 := r.Group("/v1"); v1.Use(UserOnlyMiddleware()) - 错误叠加:如果再额外给
v2 := r.Group("/v2")加v2.Use(AdminOnlyMiddleware()),而根路由又用了r.Use(AuthMiddleware()),token 解析可能被重复执行两次,c.GetHeader("Authorization")拿到空值,401 和 403 混乱输出 - Group 外禁止写同前缀路由:比如
r.GET("/v1/:id", handler)会贪婪匹配/v1/users,导致v1.Group("/users")下所有子路由 404
权限校验要从路径动态提取版本,别硬编码
写一个中间件叫 V1AuthMiddleware、另一个叫 V2AuthMiddleware 看似清晰,实则难以维护。真正该做的是让一个中间件自己识别请求属于哪个版本,再查对应策略表。
- 用
c.FullPath()判断前缀,比c.Request.URL.Path更稳妥(已处理路由参数如/v1/users/:id) - 检查
strings.HasPrefix(c.FullPath(), "/v1/")后,从 context 取角色:role := c.GetString("role")(由前置 auth 中间件注入) - 查策略 map:
v1Policy["/users"] == []string{"user", "admin"},不满足就c.AbortWithStatusJSON(403, gin.H{"error": "forbidden"}) - 切忌分两步写:
c.JSON(403, ...); c.Abort()—— 如果忘记c.Abort(),后续 handler 仍会执行,越权就发生了
认证和授权必须严格分两步走
很多越权问题源于混淆了「你是谁」和「你能做什么」。中间件里只做授权(role → permission 映射),认证(token 解析、签名验证、exp 检查)必须是前置独立中间件。
- 认证中间件负责:解析
Authorizationheader、校验 JWT signature/exp/nbf、把user_id和role写进c.Set() - 授权中间件只读取这些值,不做任何 token 操作;否则 token 失效后仍可能因缓存角色而放行非法请求
- 若用 RBAC,权限数据建议存在内存 map 或 Redis 中,避免每次查 DB;但需注意 key 过期,比如用户角色变更后,旧 token 对应的权限缓存要主动失效
最难绷的是路径解析时机——c.FullPath() 在路由匹配完成后才可用,所以中间件必须挂载在 Group 内,且不能被更外层的 c.Abort() 截断;否则你写的权限逻辑根本没机会运行。











