用户认证系统实战 #

本节将实现一个完整的用户认证系统,包括用户注册、登录、JWT令牌验证、权限控制等核心功能。

项目结构 #

text
auth-system/
├── main.go
├── config/
│   └── config.go
├── models/
│   └── user.go
├── handlers/
│   └── auth_handler.go
├── middleware/
│   ├── jwt.go
│   └── permission.go
├── services/
│   ├── auth_service.go
│   └── jwt_service.go
├── utils/
│   ├── hash.go
│   └── response.go
└── router/
    └── router.go

配置管理 #

go
// config/config.go
package config

import (
    "os"
    "time"
)

type Config struct {
    Server   ServerConfig
    Database DatabaseConfig
    JWT      JWTConfig
}

type ServerConfig struct {
    Port string
    Mode string
}

type DatabaseConfig struct {
    Host     string
    Port     string
    User     string
    Password string
    DBName   string
}

type JWTConfig struct {
    Secret     string
    ExpireTime time.Duration
    Issuer     string
}

func Load() *Config {
    return &Config{
        Server: ServerConfig{
            Port: getEnv("SERVER_PORT", ":8080"),
            Mode: getEnv("GIN_MODE", "debug"),
        },
        Database: DatabaseConfig{
            Host:     getEnv("DB_HOST", "localhost"),
            Port:     getEnv("DB_PORT", "3306"),
            User:     getEnv("DB_USER", "root"),
            Password: getEnv("DB_PASSWORD", ""),
            DBName:   getEnv("DB_NAME", "auth_db"),
        },
        JWT: JWTConfig{
            Secret:     getEnv("JWT_SECRET", "your-secret-key"),
            ExpireTime: 24 * time.Hour,
            Issuer:     "auth-system",
        },
    }
}

func getEnv(key, defaultValue string) string {
    if value := os.Getenv(key); value != "" {
        return value
    }
    return defaultValue
}

数据模型 #

go
// models/user.go
package models

import (
    "time"
    "gorm.io/gorm"
)

type User struct {
    ID        uint           `json:"id" gorm:"primaryKey"`
    Username  string         `json:"username" gorm:"uniqueIndex;size:50;not null"`
    Email     string         `json:"email" gorm:"uniqueIndex;size:100;not null"`
    Password  string         `json:"-" gorm:"size:255;not null"`
    Nickname  string         `json:"nickname" gorm:"size:50"`
    Avatar    string         `json:"avatar" gorm:"size:255"`
    Role      string         `json:"role" gorm:"size:20;default:'user'"` // admin, user
    Status    int            `json:"status" gorm:"default:1"`
    CreatedAt time.Time      `json:"created_at"`
    UpdatedAt time.Time      `json:"updated_at"`
    DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
}

func (User) TableName() string {
    return "users"
}

type Role string

const (
    RoleAdmin Role = "admin"
    RoleUser  Role = "user"
)

func (r Role) String() string {
    return string(r)
}

func (u *User) IsAdmin() bool {
    return u.Role == RoleAdmin.String()
}

JWT 服务 #

go
// services/jwt_service.go
package services

import (
    "auth-system/config"
    "auth-system/models"
    "errors"
    "time"

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

type Claims struct {
    UserID   uint   `json:"user_id"`
    Username string `json:"username"`
    Role     string `json:"role"`
    jwt.RegisteredClaims
}

type JWTService struct {
    config *config.JWTConfig
}

func NewJWTService(config *config.JWTConfig) *JWTService {
    return &JWTService{config: config}
}

func (s *JWTService) GenerateToken(user *models.User) (string, error) {
    claims := Claims{
        UserID:   user.ID,
        Username: user.Username,
        Role:     user.Role,
        RegisteredClaims: jwt.RegisteredClaims{
            ExpiresAt: jwt.NewNumericDate(time.Now().Add(s.config.ExpireTime)),
            IssuedAt:  jwt.NewNumericDate(time.Now()),
            Issuer:    s.config.Issuer,
        },
    }
    
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    return token.SignedString([]byte(s.config.Secret))
}

func (s *JWTService) ParseToken(tokenString string) (*Claims, error) {
    token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
        return []byte(s.config.Secret), nil
    })
    
    if err != nil {
        return nil, err
    }
    
    if claims, ok := token.Claims.(*Claims); ok && token.Valid {
        return claims, nil
    }
    
    return nil, errors.New("invalid token")
}

func (s *JWTService) RefreshToken(tokenString string) (string, error) {
    claims, err := s.ParseToken(tokenString)
    if err != nil {
        return "", err
    }
    
    newClaims := Claims{
        UserID:   claims.UserID,
        Username: claims.Username,
        Role:     claims.Role,
        RegisteredClaims: jwt.RegisteredClaims{
            ExpiresAt: jwt.NewNumericDate(time.Now().Add(s.config.ExpireTime)),
            IssuedAt:  jwt.NewNumericDate(time.Now()),
            Issuer:    s.config.Issuer,
        },
    }
    
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, newClaims)
    return token.SignedString([]byte(s.config.Secret))
}

密码工具 #

go
// utils/hash.go
package utils

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

func HashPassword(password string) (string, error) {
    bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
    return string(bytes), err
}

func CheckPassword(password, hash string) bool {
    err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
    return err == nil
}

认证服务 #

go
// services/auth_service.go
package services

import (
    "auth-system/models"
    "auth-system/utils"
    "errors"
    "gorm.io/gorm"
)

type AuthService struct {
    db          *gorm.DB
    jwtService  *JWTService
}

func NewAuthService(db *gorm.DB, jwtService *JWTService) *AuthService {
    return &AuthService{
        db:         db,
        jwtService: jwtService,
    }
}

type RegisterRequest struct {
    Username string `json:"username" binding:"required,min=3,max=50,alphanum"`
    Email    string `json:"email" binding:"required,email"`
    Password string `json:"password" binding:"required,min=6,max=20"`
    Nickname string `json:"nickname" binding:"max=50"`
}

type LoginRequest struct {
    Username string `json:"username" binding:"required"`
    Password string `json:"password" binding:"required"`
}

type LoginResponse struct {
    Token     string       `json:"token"`
    ExpiresAt int64        `json:"expires_at"`
    User      *models.User `json:"user"`
}

func (s *AuthService) Register(req *RegisterRequest) (*models.User, error) {
    var count int64
    s.db.Model(&models.User{}).Where("username = ?", req.Username).Count(&count)
    if count > 0 {
        return nil, errors.New("用户名已存在")
    }
    
    s.db.Model(&models.User{}).Where("email = ?", req.Email).Count(&count)
    if count > 0 {
        return nil, errors.New("邮箱已被注册")
    }
    
    hashedPassword, err := utils.HashPassword(req.Password)
    if err != nil {
        return nil, errors.New("密码加密失败")
    }
    
    user := &models.User{
        Username: req.Username,
        Email:    req.Email,
        Password: hashedPassword,
        Nickname: req.Nickname,
        Role:     models.RoleUser.String(),
        Status:   1,
    }
    
    if err := s.db.Create(user).Error; err != nil {
        return nil, err
    }
    
    return user, nil
}

func (s *AuthService) Login(req *LoginRequest) (*LoginResponse, error) {
    var user models.User
    if err := s.db.Where("username = ? OR email = ?", req.Username, req.Username).First(&user).Error; err != nil {
        if errors.Is(err, gorm.ErrRecordNotFound) {
            return nil, errors.New("用户不存在")
        }
        return nil, err
    }
    
    if user.Status != 1 {
        return nil, errors.New("用户已被禁用")
    }
    
    if !utils.CheckPassword(req.Password, user.Password) {
        return nil, errors.New("密码错误")
    }
    
    token, err := s.jwtService.GenerateToken(&user)
    if err != nil {
        return nil, errors.New("生成令牌失败")
    }
    
    return &LoginResponse{
        Token:     token,
        ExpiresAt: 0,
        User:      &user,
    }, nil
}

func (s *AuthService) GetUserByID(id uint) (*models.User, error) {
    var user models.User
    if err := s.db.First(&user, id).Error; err != nil {
        if errors.Is(err, gorm.ErrRecordNotFound) {
            return nil, errors.New("用户不存在")
        }
        return nil, err
    }
    return &user, nil
}

func (s *AuthService) ChangePassword(userID uint, oldPassword, newPassword string) error {
    user, err := s.GetUserByID(userID)
    if err != nil {
        return err
    }
    
    if !utils.CheckPassword(oldPassword, user.Password) {
        return errors.New("原密码错误")
    }
    
    hashedPassword, err := utils.HashPassword(newPassword)
    if err != nil {
        return errors.New("密码加密失败")
    }
    
    return s.db.Model(user).Update("password", hashedPassword).Error
}

JWT 中间件 #

go
// middleware/jwt.go
package middleware

import (
    "auth-system/services"
    "auth-system/utils"
    "strings"
    "time"

    "github.com/gin-gonic/gin"
)

func JWTAuth(jwtService *services.JWTService) gin.HandlerFunc {
    return func(c *gin.Context) {
        authHeader := c.GetHeader("Authorization")
        if authHeader == "" {
            utils.Unauthorized(c, "请先登录")
            c.Abort()
            return
        }
        
        parts := strings.SplitN(authHeader, " ", 2)
        if len(parts) != 2 || parts[0] != "Bearer" {
            utils.Unauthorized(c, "认证格式错误")
            c.Abort()
            return
        }
        
        claims, err := jwtService.ParseToken(parts[1])
        if err != nil {
            utils.Unauthorized(c, "令牌无效或已过期")
            c.Abort()
            return
        }
        
        c.Set("user_id", claims.UserID)
        c.Set("username", claims.Username)
        c.Set("role", claims.Role)
        c.Set("claims", claims)
        
        c.Next()
    }
}

func GetUserID(c *gin.Context) uint {
    if userID, exists := c.Get("user_id"); exists {
        return userID.(uint)
    }
    return 0
}

func GetUsername(c *gin.Context) string {
    if username, exists := c.Get("username"); exists {
        return username.(string)
    }
    return ""
}

func GetRole(c *gin.Context) string {
    if role, exists := c.Get("role"); exists {
        return role.(string)
    }
    return ""
}

func GetClaims(c *gin.Context) *services.Claims {
    if claims, exists := c.Get("claims"); exists {
        return claims.(*services.Claims)
    }
    return nil
}

权限中间件 #

go
// middleware/permission.go
package middleware

import (
    "auth-system/utils"
    "github.com/gin-gonic/gin"
)

func RequireRole(roles ...string) gin.HandlerFunc {
    return func(c *gin.Context) {
        userRole := GetRole(c)
        
        for _, role := range roles {
            if userRole == role {
                c.Next()
                return
            }
        }
        
        utils.Forbidden(c, "权限不足")
        c.Abort()
    }
}

func RequireAdmin() gin.HandlerFunc {
    return RequireRole("admin")
}

func OptionalAuth(jwtService *services.JWTService) gin.HandlerFunc {
    return func(c *gin.Context) {
        authHeader := c.GetHeader("Authorization")
        if authHeader == "" {
            c.Next()
            return
        }
        
        parts := strings.SplitN(authHeader, " ", 2)
        if len(parts) != 2 || parts[0] != "Bearer" {
            c.Next()
            return
        }
        
        claims, err := jwtService.ParseToken(parts[1])
        if err != nil {
            c.Next()
            return
        }
        
        c.Set("user_id", claims.UserID)
        c.Set("username", claims.Username)
        c.Set("role", claims.Role)
        c.Set("claims", claims)
        
        c.Next()
    }
}

响应工具 #

go
// utils/response.go
package utils

import (
    "net/http"
    "github.com/gin-gonic/gin"
)

type Response struct {
    Code    int         `json:"code"`
    Message string      `json:"message"`
    Data    interface{} `json:"data,omitempty"`
}

func Success(c *gin.Context, data interface{}) {
    c.JSON(http.StatusOK, Response{
        Code:    0,
        Message: "success",
        Data:    data,
    })
}

func Created(c *gin.Context, data interface{}) {
    c.JSON(http.StatusCreated, Response{
        Code:    0,
        Message: "created successfully",
        Data:    data,
    })
}

func BadRequest(c *gin.Context, message string) {
    c.JSON(http.StatusBadRequest, Response{
        Code:    http.StatusBadRequest,
        Message: message,
    })
}

func Unauthorized(c *gin.Context, message string) {
    c.JSON(http.StatusUnauthorized, Response{
        Code:    http.StatusUnauthorized,
        Message: message,
    })
}

func Forbidden(c *gin.Context, message string) {
    c.JSON(http.StatusForbidden, Response{
        Code:    http.StatusForbidden,
        Message: message,
    })
}

func NotFound(c *gin.Context, message string) {
    c.JSON(http.StatusNotFound, Response{
        Code:    http.StatusNotFound,
        Message: message,
    })
}

func ValidationError(c *gin.Context, errors interface{}) {
    c.JSON(http.StatusUnprocessableEntity, Response{
        Code:    http.StatusUnprocessableEntity,
        Message: "validation failed",
        Data:    errors,
    })
}

func InternalError(c *gin.Context, message string) {
    c.JSON(http.StatusInternalServerError, Response{
        Code:    http.StatusInternalServerError,
        Message: message,
    })
}

认证处理器 #

go
// handlers/auth_handler.go
package handlers

import (
    "auth-system/middleware"
    "auth-system/services"
    "auth-system/utils"
    "github.com/gin-gonic/gin"
)

type AuthHandler struct {
    authService *services.AuthService
    jwtService  *services.JWTService
}

func NewAuthHandler(authService *services.AuthService, jwtService *services.JWTService) *AuthHandler {
    return &AuthHandler{
        authService: authService,
        jwtService:  jwtService,
    }
}

func (h *AuthHandler) Register(c *gin.Context) {
    var req services.RegisterRequest
    if err := c.ShouldBindJSON(&req); err != nil {
        utils.BadRequest(c, "请求参数错误")
        return
    }
    
    user, err := h.authService.Register(&req)
    if err != nil {
        utils.BadRequest(c, err.Error())
        return
    }
    
    utils.Created(c, user)
}

func (h *AuthHandler) Login(c *gin.Context) {
    var req services.LoginRequest
    if err := c.ShouldBindJSON(&req); err != nil {
        utils.BadRequest(c, "请求参数错误")
        return
    }
    
    resp, err := h.authService.Login(&req)
    if err != nil {
        utils.Unauthorized(c, err.Error())
        return
    }
    
    utils.Success(c, resp)
}

func (h *AuthHandler) GetProfile(c *gin.Context) {
    userID := middleware.GetUserID(c)
    
    user, err := h.authService.GetUserByID(userID)
    if err != nil {
        utils.NotFound(c, err.Error())
        return
    }
    
    utils.Success(c, user)
}

func (h *AuthHandler) ChangePassword(c *gin.Context) {
    var req struct {
        OldPassword string `json:"old_password" binding:"required"`
        NewPassword string `json:"new_password" binding:"required,min=6,max=20"`
    }
    
    if err := c.ShouldBindJSON(&req); err != nil {
        utils.BadRequest(c, "请求参数错误")
        return
    }
    
    userID := middleware.GetUserID(c)
    
    if err := h.authService.ChangePassword(userID, req.OldPassword, req.NewPassword); err != nil {
        utils.BadRequest(c, err.Error())
        return
    }
    
    utils.Success(c, gin.H{"message": "密码修改成功"})
}

func (h *AuthHandler) RefreshToken(c *gin.Context) {
    authHeader := c.GetHeader("Authorization")
    if authHeader == "" {
        utils.Unauthorized(c, "请先登录")
        return
    }
    
    parts := strings.SplitN(authHeader, " ", 2)
    if len(parts) != 2 {
        utils.Unauthorized(c, "认证格式错误")
        return
    }
    
    newToken, err := h.jwtService.RefreshToken(parts[1])
    if err != nil {
        utils.Unauthorized(c, "令牌刷新失败")
        return
    }
    
    utils.Success(c, gin.H{"token": newToken})
}

路由配置 #

go
// router/router.go
package router

import (
    "auth-system/config"
    "auth-system/handlers"
    "auth-system/middleware"
    "auth-system/models"
    "auth-system/services"
    "github.com/gin-gonic/gin"
    "gorm.io/gorm"
)

func Setup(db *gorm.DB, cfg *config.Config) *gin.Engine {
    gin.SetMode(cfg.Server.Mode)
    r := gin.Default()
    
    jwtService := services.NewJWTService(&cfg.JWT)
    authService := services.NewAuthService(db, jwtService)
    authHandler := handlers.NewAuthHandler(authService, jwtService)
    
    db.AutoMigrate(&models.User{})
    
    api := r.Group("/api/v1")
    {
        auth := api.Group("/auth")
        {
            auth.POST("/register", authHandler.Register)
            auth.POST("/login", authHandler.Login)
            auth.POST("/refresh", authHandler.RefreshToken)
        }
        
        user := api.Group("/user")
        user.Use(middleware.JWTAuth(jwtService))
        {
            user.GET("/profile", authHandler.GetProfile)
            user.PUT("/password", authHandler.ChangePassword)
        }
        
        admin := api.Group("/admin")
        admin.Use(middleware.JWTAuth(jwtService))
        admin.Use(middleware.RequireAdmin())
        {
            admin.GET("/users", func(c *gin.Context) {
                c.JSON(200, gin.H{"message": "admin panel"})
            })
        }
    }
    
    return r
}

主程序 #

go
// main.go
package main

import (
    "auth-system/config"
    "auth-system/router"
    "fmt"
    "log"
    
    "gorm.io/driver/mysql"
    "gorm.io/gorm"
)

func main() {
    cfg := config.Load()
    
    dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",
        cfg.Database.User,
        cfg.Database.Password,
        cfg.Database.Host,
        cfg.Database.Port,
        cfg.Database.DBName,
    )
    
    db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
    if err != nil {
        log.Fatal("数据库连接失败:", err)
    }
    
    r := router.Setup(db, cfg)
    
    log.Println("服务器启动在", cfg.Server.Port)
    if err := r.Run(cfg.Server.Port); err != nil {
        log.Fatal("服务器启动失败:", err)
    }
}

API 使用示例 #

用户注册 #

bash
curl -X POST http://localhost:8080/api/v1/auth/register \
  -H "Content-Type: application/json" \
  -d '{
    "username": "johndoe",
    "email": "john@example.com",
    "password": "password123",
    "nickname": "John"
  }'

用户登录 #

bash
curl -X POST http://localhost:8080/api/v1/auth/login \
  -H "Content-Type: application/json" \
  -d '{
    "username": "johndoe",
    "password": "password123"
  }'

响应:

json
{
  "code": 0,
  "message": "success",
  "data": {
    "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "expires_at": 1711622400,
    "user": {
      "id": 1,
      "username": "johndoe",
      "email": "john@example.com",
      "nickname": "John",
      "role": "user"
    }
  }
}

获取用户信息 #

bash
curl http://localhost:8080/api/v1/user/profile \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."

修改密码 #

bash
curl -X PUT http://localhost:8080/api/v1/user/password \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \
  -H "Content-Type: application/json" \
  -d '{
    "old_password": "password123",
    "new_password": "newpassword456"
  }'

刷新令牌 #

bash
curl -X POST http://localhost:8080/api/v1/auth/refresh \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."

安全最佳实践 #

1. 密码安全 #

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

func HashPassword(password string) (string, error) {
    bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
    return string(bytes), err
}

func CheckPassword(password, hash string) bool {
    err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
    return err == nil
}

2. JWT 安全配置 #

go
type JWTConfig struct {
    Secret     string
    ExpireTime time.Duration
    Issuer     string
}

func Load() *Config {
    return &Config{
        JWT: JWTConfig{
            Secret:     os.Getenv("JWT_SECRET"),
            ExpireTime: 24 * time.Hour,
            Issuer:     "auth-system",
        },
    }
}

3. 限流保护 #

go
import "github.com/ulule/limiter/v3"

func RateLimit() gin.HandlerFunc {
    store := memory.NewStore()
    rate := limiter.Rate{
        Period: 1 * time.Minute,
        Limit:  60,
    }
    instance := limiter.New(store, rate)
    
    return func(c *gin.Context) {
        context, err := instance.Get(c, c.ClientIP())
        if err != nil {
            c.AbortWithStatus(500)
            return
        }
        
        if context.Reached {
            c.AbortWithStatusJSON(429, gin.H{
                "error": "too many requests",
            })
            return
        }
        
        c.Next()
    }
}

4. CORS 配置 #

go
func CORS() gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Header("Access-Control-Allow-Origin", "*")
        c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
        c.Header("Access-Control-Allow-Headers", "Origin, Content-Type, Authorization")
        
        if c.Request.Method == "OPTIONS" {
            c.AbortWithStatus(204)
            return
        }
        
        c.Next()
    }
}

小结 #

本节实现了一个完整的用户认证系统:

  1. 用户注册登录:支持用户名/邮箱登录
  2. JWT令牌:生成、验证、刷新
  3. 密码加密:使用bcrypt
  4. 权限控制:基于角色的访问控制
  5. 安全实践:限流、CORS等

这个认证系统可以直接用于生产环境,也可以根据需求扩展更多功能。

最后更新:2026-03-28