下载工作台
Gin Web 开发

JWT 认证与 RBAC

试读上半部分 · 解锁后可读全文

第 6 章 · JWT 认证与 RBAC

本章目标:实现用户注册/登录;使用 golang-jwt/jwt/v5 签发 Bearer Token;密码 bcrypt 哈希存入 password_hash;编写 Auth 中间件RBAC(user/admin);保护商品写接口;提供 curl 联调步骤;对照 fastapi-web ch07 OAuth2 Password Flow 与 flask-web ch06 Flask-Login。

学时建议:5~6 小时(含 2 小时 Token 联调)

前置:完成 gin-web ch05;User 模型(ch04)。


6.1 场景说明:保护写操作

商品读接口对匿名开放(仅 is_published=true);创建、更新、删除必须 admin 角色。

接口认证角色
GET /api/v1/products匿名
GET /api/v1/products/:slug匿名(仅已发布)
POST/PATCH/DELETE /api/v1/productsBearer JWTadmin
POST /api/v1/auth/register注册默认 user
POST /api/v1/auth/login返回 token
GET /api/v1/auth/meBearer JWTuser/admin
POST /api/v1/auth/login
  ──► 200 { "access_token": "eyJ...", "token_type": "Bearer" }

POST /api/v1/products
  Header: Authorization: Bearer eyJ...
  ──► 201(admin) / 40301(user) / 40101(无 token)

对照章节

主题flask-web ch06fastapi-web ch07gin-web ch06
认证Session CookieOAuth2 Password + JWTBearer JWT
密码werkzeug hashpasslib bcryptbcrypt
保护路由@login_requiredDepends(get_current_user)Auth + RequireRole

6.2 逐步操作表

步骤操作验证
1go get jwt/v5x/crypto依赖就绪
2实现 auth/password.goauth/jwt.go单元测试 Hash/Sign
3repository/user.goservice/user.go按 email 查用户
4handler/auth.go register/login/me路由注册
5middleware/auth.go401/403 分支
6种子 admin@example.comDB 有 admin
7curl login 拿 tokenaccess_token 非空
8admin token POST 商品201

6.3 依赖

go get github.com/golang-jwt/jwt/v5@v5.2.1
go get golang.org/x/crypto@v0.28.0

6.4 密码 bcrypt

// internal/auth/password.go
package auth

import "golang.org/x/crypto/bcrypt"

const bcryptCost = bcrypt.DefaultCost

func HashPassword(plain string) (string, error) {
    b, err := bcrypt.GenerateFromPassword([]byte(plain), bcryptCost)
    return string(b), err
}

func CheckPassword(hash, plain string) bool {
    return bcrypt.CompareHashAndPassword([]byte(hash), []byte(plain)) == nil
}
规则说明
仅存 hashUser 表列 password_hash
不明文日志禁止 log 密码
costDefaultCost(10),可调高增安全

flask-web ch06 generate_password_hash 等价。


6.5 JWT 签发与校验

// internal/auth/jwt.go
package auth

import (
    "errors"
    "time"

    jwt "github.com/golang-jwt/jwt/v5"
)

type Claims struct {
    UserID uint   `json:"uid"`
    Email  string `json:"email"`
    Role   string `json:"role"`
    jwt.RegisteredClaims
}

func SignToken(secret string, uid uint, email, role string, ttl time.Duration) (string, error) {
    now := time.Now()
    claims := Claims{
        UserID: uid,
        Email:  email,
        Role:   role,
        RegisteredClaims: jwt.RegisteredClaims{
            Issuer:    "api-go-demo",
            ExpiresAt: jwt.NewNumericDate(now.Add(ttl)),
            IssuedAt:  jwt.NewNumericDate(now),
            Subject:   email,
        },
    }
    return jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString([]byte(secret))
}

func ParseToken(secret, tokenStr string) (*Claims, error) {
    token, err := jwt.ParseWithClaims(tokenStr, &Claims{}, func(t *jwt.Token) (any, error) {
        if t.Method != jwt.SigningMethodHS256 {
            return nil, errors.New("unexpected signing method")
        }
        return []byte(secret), nil
    })
    if err != nil {
        return nil, err
    }
    claims, ok := token.Claims.(*Claims)
    if !ok || !token.Valid {
        return nil, jwt.ErrTokenInvalidClaims
    }
    return claims, nil
}

配置:JWT_SECRET(prod ≥ 32 字节)、Access TTL 1h、算法 HS256。


6.6 UserRepository 与 UserService

// internal/repository/user.go — 核心方法
func (r *UserRepository) GetByEmail(ctx context.Context, email string) (*model.User, error) { /* ... */ }
func (r *UserRepository) Create(ctx context.Context, u *model.User) error { /* ... */ }
// internal/service/user.go — Register / Authenticate(见仓库完整版)

6.7 Auth 中间件

// internal/middleware/auth.go
package middleware

import (
    "strings"

    "github.com/gin-gonic/gin"
    "example.com/api-go-demo/internal/auth"
)

func Auth(secret string) gin.HandlerFunc {
    return func(c *gin.Context) {
        hdr := c.GetHeader("Authorization")
        if !strings.HasPrefix(hdr, "Bearer ") {
            c.AbortWithStatusJSON(401, gin.H{"code": 40101, "message": "未登录", "data": nil})
            return
        }
        token := strings.TrimPrefix(hdr, "Bearer ")
        claims, err := auth.ParseToken(secret, token)
        if err != nil {
            c.AbortWithStatusJSON(401, gin.H{"code": 40101, "message": "无效或过期的令牌", "data": nil})
            return
        }
        c.Set("user_id", claims.UserID)
        c.Set("email", claims.Email)
        c.Set("role", claims.Role)
        c.Next()
    }
}

func RequireRole(roles ...string) gin.HandlerFunc {
    allowed := make(map[string]struct{}, len(roles))
    for _, r := range roles {
        allowed[r] = struct{}{}
    }
    return func(c *gin.Context) {
        roleVal, ok := c.Get("role")
        if !ok {
            c.AbortWithStatusJSON(401, gin.H{"code": 40101, "message": "未登录", "data": nil})
            return
        }
        role, _ := roleVal.(string)
        if _, ok := allowed[role]; !ok {
            c.AbortWithStatusJSON(403, gin.H{"code": 40301, "message": "无权限", "data": nil})
            return
        }
        c.Next()
    }
}

40301flask-web ch07fastapi-web ch12 错误码对齐。


以下内容需解锁后阅读

试读已结束。解锁本章 ¥5.00,或开通年度会员畅读全部教程。
年度会员 ¥199.00/年; 小紫 AI 工作台有效会员 ¥99.00/年

正文仅在服务端鉴权后下发,未付费无法获取下半部分内容。