Golang如何实现Web服务的用户权限控制

应使用中间件统一鉴权而非在每个handler中硬编码:解析Authorization头或session,校验JWT的exp/iat等标准声明,将用户信息注入context;结合gorilla/mux按角色分路由并配置requireRole中间件;权限数据化存储于roles、permissions及关联表,按endpoint+method粒度缓存鉴权结果。

net/http + 中间件做基础权限拦截

Go 原生 http.ServeMux 不带鉴权能力,得靠中间件手动拦截。核心思路是:在请求进入业务 handler 前,检查 request.Header.Get("Authorization") 或 session/cookie,验证通过才 next.ServeHTTP(w, r),否则返回 401403

常见错误是把权限逻辑写进每个 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 header", http.StatusUnauthorized)
            return
        }
        // 解析 JWT 或查 session store
        userID, role, err := validateToken(token)
        if err != nil || role == "" {
            http.Error(w, "invalid token", http.StatusForbidden)
            return
        }
        // 注入上下文,供后续 handler 使用
        ctx := context.WithValue(r.Context(), "user_id", userID)
        ctx = context.WithValue(ctx, "role", role)
        r = r.WithContext(ctx)
        next.ServeHTTP(w, r)
    })
}

注册时链式调用:http.Handle("/admin/", authMiddleware(http.HandlerFunc(adminHandler)))。注意路径匹配规则 —— /admin/ 会匹配 /admin/users,但 /admin(不带尾斜杠)不会被匹配。

gorilla/mux 实现角色路由级控制

gorilla/mux 支持为 route 设置自定义 matcher,比原生 mux 更适合做 RBAC。关键不是“加中间件”,而是“按角色注册不同子路由器”。

  • 管理员路由全部挂到 adminRouter,并统一加 requireRole("admin") 中间件
  • 普通用户路由挂到 userRouter,中间件检查 role == "user" || role == "admin"
  • 避免用 if role == "admin" { ... } else if role == "user" { ... } 在 handler 内硬编码分支

示例中容易踩的坑:误以为 router.Use() 能对子 router 生效 —— 实际上它只影响直接注册在该 router 上的 handler,子 router 需单独调用 Use()

JWT 验证时别忽略 expiat 校验

很多 Go 项目用 golang-jwt/jwt(v5+)解析 token,但只校验签名,漏掉时间字段,导致过期 token 仍被接受。

必须显式启用标准声明验证:

token, err := jwt.ParseWithClaims(
    tokenString,
    &Claims{},
    func(token *jwt.Token) (interface{}, error) {
        return []byte(jwtSecret), nil
    },
)
if err != nil {
    return nil, err
}
if !token.Valid {
    return nil, errors.New("invalid token")
}
// ⚠️ 这步不能少:检查 exp/iat/nbf
claims, ok := token.Claims.(*Claims)
if !ok || !claims.VerifyExpiresAt(time.Now().Unix(), true) {
    return nil, errors.New("token expired")
}

VerifyExpiresAt 第二个参数设为 true 才启用严格校验;iat(issued at)建议也校验,防止回放攻击 —— 比如限制 token 只能在签发后 5 分钟内使用。

数据库权限表设计要支持动态策略

硬编码 if role == "admin" { allow } else { deny } 无法应对运营需求变化。真实系统应把权限落地为数据:一张 roles 表、一张 permissions 表、一张 role_permissions 关联表。

每次请求鉴权时,不是查“用户角色”,而是查“该用户能访问哪些 endpoint + method”:

  • 缓存 key 可设为 perm:::,比如 perm:123:POST:/api/v1/orders
  • 避免每次请求都查库,用 redisgo-cache 缓存结果,TTL 设为 5–10 分钟
  • 权限变更时主动清缓存,而不是等自然过期

最常被忽略的是 HTTP 方法粒度 —— 同一个路径,GET /users 可能全员可读,但 DELETE /users/123 必须 owner 或 admin。权限表里 method 字段不能省。