应在中间件中用闭包包装http.Handler,解析Authorization头中的JWT,校验签名与过期,将用户ID注入request.Context,再调用next.ServeHTTP;禁止用全局变量存用户,须遵循拒绝默认原则并二次校验资源所有权。

中间件里怎么加 http.Handler 身份校验逻辑
Go 的 http.ServeMux 本身不支持拦截,得靠中间件包装。最直接的方式是写一个闭包函数,接收原始 http.Handler,返回带鉴权的新 http.Handler:
func AuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
if token == "" {
http.Error(w, "missing auth token", http.StatusUnauthorized)
return
}
// 解析并验证 JWT(用 github.com/golang-jwt/jwt/v5)
claims := &Claims{}
_, err := jwt.ParseWithClaims(token, claims, func(t *jwt.Token) (interface{}, error) {
return []byte(os.Getenv("JWT_SECRET")), nil
})
if err != nil {
http.Error(w, "invalid token", http.StatusUnauthorized)
return
}
// 把用户信息塞进 context,下游可取
ctx := context.WithValue(r.Context(), "user_id", claims.UserID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
注意:别用 context.WithValue 存敏感字段(比如密码),只放已验证过的只读标识;另外 JWT 验证必须检查 exp 和 iss,否则容易绕过。
权限控制该放在路由层还是 handler 内部
两者都行,但推荐在 handler 开头做细粒度判断——因为路由路径(如 /api/users)无法区分 GET(列表)和 POST(创建)的权限差异。
- 路由层适合粗筛:比如所有
/admin/前缀的请求强制要求role == "admin" - handler 内部更适合 RBAC 场景:比如
UpdateUser要求user_id == req.UserID || hasRole("editor") - 避免在中间件里查 DB 做权限判定,会拖慢所有请求;把角色/权限缓存在
context或struct字段里复用
示例中可在 handler 里这样取上下文数据:userID := r.Context().Value("user_id").(string),但务必先做类型断言检查,否则 panic。
立即学习“go语言免费学习笔记(深入)”;
用 Gin 或 Echo 时怎么复用鉴权逻辑
Gin 的 gin.HandlerFunc 和 Echo 的 echo.MiddlewareFunc 本质都是 http.Handler 的封装,所以核心逻辑可以抽成独立函数,再适配框架签名:
// 通用校验函数,不依赖框架
func CheckPermission(ctx context.Context, requiredRole string) error {
userRole := ctx.Value("role").(string)
if userRole != requiredRole && userRole != "admin" {
return errors.New("insufficient permission")
}
return nil
}
// Gin 用法
func GinAuth() gin.HandlerFunc {
return func(c *gin.Context) {
if err := CheckPermission(c.Request.Context(), "user"); err != nil {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "forbidden"})
return
}
c.Next()
}
}
关键点:不要在中间件里调 c.Abort() 后还执行 c.Next();Gin 的 c.Request.Context() 是继承自原始 http.Request 的,所以之前塞的 user_id 还能拿到。
为什么 http.HandlerFunc 包装比全局变量更安全
有人图省事把用户信息存全局 map(map[string]*User),靠 token 当 key 查——这有三个硬伤:
- 并发不安全:没加
sync.RWMutex就读写 map,运行时直接 panic - 内存泄漏:token 不主动清理,map 持续膨胀
- 无法绑定生命周期:context 可随请求结束自动失效,全局变量得自己维护 TTL
真正该共享的是验证逻辑(比如 JWT 解析、role 查询函数),不是用户数据本身。每次请求都走一遍轻量校验,比维护一个易出错的状态要可靠得多。
权限判断最常被忽略的是「拒绝默认」原则:没明确允许的操作,一律拒绝;还有就是忘记对 URL 参数和 body 数据做二次所有权校验(比如删文章时,不能只信 path 中的 /posts/123,得查数据库确认该用户是否拥有 ID 123 的文章)。


















