Files
learn-golang/Web开发/06go-auth-api/21综合实战项目.md
liumangmang b010f82221 feat(auth): 添加完整的用户认证API项目
- 实现用户注册、登录、JWT令牌认证功能
- 集成Gin、GORM、Viper、Zap等框架
- 添加密码加密、数据库操作、中间件等完整功能
- 配置多环境支持、日志轮转、CORS处理
- 创建完整的项目结构和配置文件体系
2025-12-30 18:00:42 +08:00

18 KiB
Raw Blame History

title, icon, date, category, tag
title icon date category tag
综合实战项目 mdi:shield-account 2025-12-23
Go
后端
工程化
实战项目
API
用户认证
密码加密
JWT
综合实战

整合 Gin、GORM、Viper、Zap 等框架,开发一个完整的用户注册/登录 API。这个项目包含用户认证、密码加密、JWT 令牌和数据库操作,是学习 Go Web 开发的完美案例。


Go 综合实战:用户注册/登录 API 完整指南

这是一个完整的 Web 应用示例整合了前面学到的所有知识Gin、GORM、Viper、Zap、中间件等。


一、项目结构

go-auth-api/
├── config/
│   ├── app.yaml
│   ├── app.dev.yaml
│   └── app.prod.yaml
├── logs/
│   └── app.log
├── main.go
├── config.go
├── logger.go
├── db.go
├── models.go
├── handlers.go
├── middleware.go
├── jwt.go
├── utils.go
└── go.mod

二、项目初始化

2.1 创建项目

cd ~/GolandProjects
mkdir go-auth-api && cd go-auth-api
go mod init go-auth-api

# 安装依赖
go get -u github.com/gin-gonic/gin
go get -u gorm.io/gorm
go get -u gorm.io/driver/sqlite
go get -u github.com/spf13/viper
go get -u go.uber.org/zap
go get -u github.com/golang-jwt/jwt/v4
go get -u golang.org/x/crypto
go get -u gopkg.in/natefinch/lumberjack.v2

版本说明:代码兼容 Go 1.18+ 版本(建议 1.22+ 体验最佳性能)

2.2 创建目录

mkdir -p config logs

三、配置文件

3.1 config/app.yaml

app:
  name: AuthAPI
  version: 1.0.0
  port: 8080
  env: dev

database:
  driver: sqlite
  path: auth.db

jwt:
  secret: your-secret-key-change-in-production
  expire: 86400  # 24 小时

password:
  bcrypt_cost: 10

logging:
  level: info
  format: json

3.2 config/app.prod.yaml

app:
  port: 80
  env: prod

logging:
  level: warn

四、模型定义models.go

package main

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

type User struct {
    ID        uint           `gorm:"primaryKey" json:"id"`
    Name      string         `gorm:"size:100;not null" json:"name"`
    Email     string         `gorm:"size:100;unique;not null" json:"email"`
    Password  string         `gorm:"size:255;not null" json:"-"` // 不在 JSON 中显示
    Phone     string         `gorm:"size:20" json:"phone,omitempty"`
    Age       int            `json:"age,omitempty"`
    Active    bool           `gorm:"default:true" json:"active"`
    CreatedAt time.Time      `json:"created_at"`
    UpdatedAt time.Time      `json:"updated_at"`
    DeletedAt gorm.DeletedAt `json:"-"`
}

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

// 请求体
type RegisterRequest struct {
    Name     string `json:"name" binding:"required"`
    Email    string `json:"email" binding:"required,email"`
    Password string `json:"password" binding:"required,min=6"`
    Phone    string `json:"phone" binding:"omitempty,len=11"`
}

type LoginRequest struct {
    Email    string `json:"email" binding:"required,email"`
    Password string `json:"password" binding:"required"`
}

type LoginResponse struct {
    Token string `json:"token"`
    User  User   `json:"user"`
}

五、数据库初始化db.go

package main

import (
    "fmt"
    "gorm.io/driver/sqlite"
    "gorm.io/gorm"
)

var DB *gorm.DB

func InitDB(cfg *Config) error {
    var dsn string
    
    if cfg.Database.Driver == "sqlite" {
        dsn = cfg.Database.Path
    }
    
    var err error
    DB, err = gorm.Open(sqlite.Open(dsn), &gorm.Config{})
    if err != nil {
        return fmt.Errorf("failed to connect database: %w", err)
    }
    
    // 自动迁移
    if err = DB.AutoMigrate(&User{}); err != nil {
        return fmt.Errorf("failed to migrate database: %w", err)
    }
    
    Logger.Info("Database initialized successfully")
    return nil
}

六、JWT 和密码工具jwt.go + utils.go

6.1 jwt.go

package main

import (
    "fmt"
    "time"
    "github.com/golang-jwt/jwt/v4"
)

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

func GenerateToken(userID uint, email string, secret string, expire int64) (string, error) {
    claims := Claims{
        UserID: userID,
        Email:  email,
        RegisteredClaims: jwt.RegisteredClaims{
            ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(expire) * time.Second)),
            IssuedAt:  jwt.NewNumericDate(time.Now()),
        },
    }
    
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    tokenString, err := token.SignedString([]byte(secret))
    if err != nil {
        return "", err
    }
    
    return tokenString, nil
}

func VerifyToken(tokenString string, secret string) (*Claims, error) {
    claims := &Claims{}
    
    token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
        if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
            return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
        }
        return []byte(secret), nil
    })
    
    if err != nil {
        return nil, err
    }
    
    if !token.Valid {
        return nil, fmt.Errorf("invalid token")
    }
    
    return claims, nil
}

6.2 utils.go

package main

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

// 密码加密
func HashPassword(password string) (string, error) {
    hash, err := bcrypt.GenerateFromPassword([]byte(password), 10)
    if err != nil {
        return "", err
    }
    return string(hash), nil
}

// 验证密码
func VerifyPassword(hashedPassword, password string) bool {
    err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
    return err == nil
}

// 检查邮箱是否已注册
func EmailExists(email string) bool {
    var count int64
    DB.Model(&User{}).Where("email = ?", email).Count(&count)
    return count > 0
}

七、中间件middleware.go

package main

import (
    "strings"
    "github.com/gin-gonic/gin"
    "go.uber.org/zap"
)

// 请求日志中间件
func LoggingMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        Logger.Info("Request",
            zap.String("method", c.Request.Method),
            zap.String("path", c.Request.URL.Path),
            zap.String("ip", c.ClientIP()),
        )
        
        c.Next()
        
        Logger.Info("Response",
            zap.String("path", c.Request.URL.Path),
            zap.Int("status", c.Writer.Status()),
        )
    }
}

// JWT 认证中间件
func AuthMiddleware(cfg *Config) gin.HandlerFunc {
    return func(c *gin.Context) {
        authHeader := c.GetHeader("Authorization")
        
        if authHeader == "" {
            c.JSON(401, gin.H{"error": "Missing authorization header"})
            c.Abort()
            return
        }
        
        parts := strings.SplitN(authHeader, " ", 2)
        if len(parts) != 2 || parts[0] != "Bearer" {
            c.JSON(401, gin.H{"error": "Invalid authorization header"})
            c.Abort()
            return
        }
        
        claims, err := VerifyToken(parts[1], cfg.JWT.Secret)
        if err != nil {
            Logger.Error("Token verification failed", zap.Error(err))
            c.JSON(401, gin.H{"error": "Invalid token"})
            c.Abort()
            return
        }
        
        // 将用户信息存储在上下文中
        c.Set("user_id", claims.UserID)
        c.Set("email", claims.Email)
        
        c.Next()
    }
}

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

八、业务逻辑处理器handlers.go

package main

import (
    "github.com/gin-gonic/gin"
    "go.uber.org/zap"
)

// 注册用户
func Register(cfg *Config) gin.HandlerFunc {
    return func(c *gin.Context) {
        var req RegisterRequest
        
        if err := c.ShouldBindJSON(&req); err != nil {
            Logger.Error("Invalid registration request", zap.Error(err))
            c.JSON(400, gin.H{"error": err.Error()})
            return
        }
        
        // 检查邮箱是否已存在
        if EmailExists(req.Email) {
            c.JSON(400, gin.H{"error": "Email already registered"})
            return
        }
        
        // 密码加密
        hashedPassword, err := HashPassword(req.Password)
        if err != nil {
            Logger.Error("Failed to hash password", zap.Error(err))
            c.JSON(500, gin.H{"error": "Internal server error"})
            return
        }
        
        // 创建用户
        user := User{
            Name:     req.Name,
            Email:    req.Email,
            Password: hashedPassword,
            Phone:    req.Phone,
        }
        
        if err := DB.Create(&user).Error; err != nil {
            Logger.Error("Failed to create user", zap.Error(err))
            c.JSON(500, gin.H{"error": "Failed to register user"})
            return
        }
        
        Logger.Info("User registered successfully", zap.String("email", user.Email))
        
        c.JSON(201, gin.H{
            "message": "User registered successfully",
            "user_id": user.ID,
        })
    }
}

// 用户登录
func Login(cfg *Config) gin.HandlerFunc {
    return func(c *gin.Context) {
        var req LoginRequest
        
        if err := c.ShouldBindJSON(&req); err != nil {
            c.JSON(400, gin.H{"error": err.Error()})
            return
        }
        
        // 查找用户
        var user User
        if err := DB.Where("email = ?", req.Email).First(&user).Error; err != nil {
            Logger.Warn("User not found", zap.String("email", req.Email))
            c.JSON(401, gin.H{"error": "Invalid email or password"})
            return
        }
        
        // 验证密码
        if !VerifyPassword(user.Password, req.Password) {
            Logger.Warn("Invalid password", zap.String("email", req.Email))
            c.JSON(401, gin.H{"error": "Invalid email or password"})
            return
        }
        
        // 生成 JWT token
        token, err := GenerateToken(user.ID, user.Email, cfg.JWT.Secret, int64(cfg.JWT.Expire))
        if err != nil {
            Logger.Error("Failed to generate token", zap.Error(err))
            c.JSON(500, gin.H{"error": "Failed to generate token"})
            return
        }
        
        Logger.Info("User logged in successfully", zap.String("email", user.Email))
        
        c.JSON(200, LoginResponse{
            Token: token,
            User: User{
                ID:    user.ID,
                Name:  user.Name,
                Email: user.Email,
                Phone: user.Phone,
                Age:   user.Age,
            },
        })
    }
}

// 获取用户信息
func GetProfile(c *gin.Context) {
    userID, _ := c.Get("user_id")
    
    var user User
    if err := DB.First(&user, userID.(uint)).Error; err != nil {
        c.JSON(404, gin.H{"error": "User not found"})
        return
    }
    
    c.JSON(200, user)
}

// 更新用户信息
func UpdateProfile(c *gin.Context) {
    userID, _ := c.Get("user_id")
    
    var req struct {
        Name  string `json:"name"`
        Phone string `json:"phone"`
        Age   int    `json:"age"`
    }
    
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(400, gin.H{"error": err.Error()})
        return
    }
    
    if err := DB.Model(&User{}).Where("id = ?", userID.(uint)).Updates(req).Error; err != nil {
        Logger.Error("Failed to update user", zap.Error(err))
        c.JSON(500, gin.H{"error": "Failed to update profile"})
        return
    }
    
    Logger.Info("User profile updated", zap.Uint("user_id", userID.(uint)))
    c.JSON(200, gin.H{"message": "Profile updated successfully"})
}

// 健康检查
func HealthCheck(c *gin.Context) {
    c.JSON(200, gin.H{
        "status": "ok",
        "app":    "AuthAPI",
    })
}

九、配置加载config.go

package main

import (
    "fmt"
    "os"
    "github.com/spf13/viper"
)

type Config struct {
    App struct {
        Name    string `mapstructure:"name"`
        Version string `mapstructure:"version"`
        Port    int    `mapstructure:"port"`
        Env     string `mapstructure:"env"`
    } `mapstructure:"app"`
    
    Database struct {
        Driver string `mapstructure:"driver"`
        Path   string `mapstructure:"path"`
    } `mapstructure:"database"`
    
    JWT struct {
        Secret string `mapstructure:"secret"`
        Expire int    `mapstructure:"expire"`
    } `mapstructure:"jwt"`
    
    Logging struct {
        Level  string `mapstructure:"level"`
        Format string `mapstructure:"format"`
    } `mapstructure:"logging"`
}

var GlobalConfig *Config

func LoadConfig() (*Config, error) {
    env := os.Getenv("GO_ENV")
    if env == "" {
        env = "dev"
    }
    
    viper.SetConfigName("app")
    viper.SetConfigType("yaml")
    viper.AddConfigPath("./config")
    
    if err := viper.ReadInConfig(); err != nil {
        return nil, fmt.Errorf("failed to read config: %w", err)
    }
    
    viper.SetConfigName("app." + env)
    viper.MergeInConfig()
    
    var cfg Config
    if err := viper.Unmarshal(&cfg); err != nil {
        return nil, fmt.Errorf("failed to unmarshal config: %w", err)
    }
    
    GlobalConfig = &cfg
    return &cfg, nil
}

十、日志初始化logger.go

package main

import (
    "os"
    "go.uber.org/zap"
    "go.uber.org/zap/zapcore"
    "gopkg.in/natefinch/lumberjack.v2"
)

var Logger *zap.Logger

func InitLogger(env string) error {
    var level zapcore.Level
    
    switch env {
    case "prod":
        level = zapcore.WarnLevel
    case "test":
        level = zapcore.DebugLevel
    default:
        level = zapcore.InfoLevel
    }
    
    logFile := &lumberjack.Logger{
        Filename:   "logs/app.log",
        MaxSize:    100,
        MaxBackups: 10,
        MaxAge:     7,
        Compress:   true,
    }
    
    encoderConfig := zapcore.EncoderConfig{
        TimeKey:       "ts",
        LevelKey:      "level",
        MessageKey:    "msg",
        CallerKey:     "caller",
        EncodeLevel:   zapcore.LowercaseLevelEncoder,
        EncodeTime:    zapcore.ISO8601TimeEncoder,
        EncodeCaller:  zapcore.ShortCallerEncoder,
    }
    
    core := zapcore.NewCore(
        zapcore.NewJSONEncoder(encoderConfig),
        zapcore.NewMultiWriteSyncer(
            zapcore.AddSync(os.Stdout),
            zapcore.AddSync(logFile),
        ),
        level,
    )
    
    Logger = zap.New(core, zap.AddCaller())
    zap.ReplaceGlobals(Logger)
    
    return nil
}

十一、主程序main.go

package main

import (
    "fmt"
    "github.com/gin-gonic/gin"
)

func main() {
    // 加载配置
    cfg, err := LoadConfig()
    if err != nil {
        panic(err)
    }
    
    // 初始化日志
    if err = InitLogger(cfg.App.Env); err != nil {
        panic(err)
    }
    defer Logger.Sync()
    
    // 初始化数据库
    if err = InitDB(cfg); err != nil {
        Logger.Fatal("Failed to initialize database", zap.Error(err))
    }
    
    // 创建 Gin 应用
    r := gin.Default()
    
    // 应用中间件
    r.Use(LoggingMiddleware())
    r.Use(CORSMiddleware())
    
    // 公开路由
    public := r.Group("/api")
    {
        public.GET("/health", HealthCheck)
        public.POST("/register", Register(cfg))
        public.POST("/login", Login(cfg))
    }
    
    // 受保护的路由
    protected := r.Group("/api")
    protected.Use(AuthMiddleware(cfg))
    {
        protected.GET("/profile", GetProfile)
        protected.PUT("/profile", UpdateProfile)
    }
    
    // 启动服务器
    addr := fmt.Sprintf(":%d", cfg.App.Port)
    Logger.Info("Server starting",
        zap.String("app", cfg.App.Name),
        zap.Int("port", cfg.App.Port),
        zap.String("env", cfg.App.Env),
    )
    
    if err = r.Run(addr); err != nil {
        Logger.Fatal("Server error", zap.Error(err))
    }
}

十二、API 使用示例

12.1 注册用户

curl -X POST http://localhost:8080/api/register \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Alice",
    "email": "alice@example.com",
    "password": "password123",
    "phone": "13800138000"
  }'

# 响应
{
  "message": "User registered successfully",
  "user_id": 1
}

12.2 用户登录

curl -X POST http://localhost:8080/api/login \
  -H "Content-Type: application/json" \
  -d '{
    "email": "alice@example.com",
    "password": "password123"
  }'

# 响应
{
  "token": "eyJhbGciOiJIUzI1NiIs...",
  "user": {
    "id": 1,
    "name": "Alice",
    "email": "alice@example.com",
    "phone": "13800138000"
  }
}

12.3 获取用户信息(需要认证)

curl -X GET http://localhost:8080/api/profile \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."

# 响应
{
  "id": 1,
  "name": "Alice",
  "email": "alice@example.com",
  "phone": "13800138000",
  "age": 0,
  "active": true,
  "created_at": "2025-12-23T14:30:45Z"
}

十三、项目验收清单

  • 用户注册(密码加密、邮箱验证)
  • 用户登录JWT 令牌生成)
  • 用户认证JWT 验证中间件)
  • 用户信息管理(获取、更新)
  • 配置管理(多环境)
  • 日志记录(结构化、日志轮转)
  • 错误处理(友好的错误消息)
  • 代码组织(清晰的文件结构)

十四、进阶扩展方向

  1. 添加邮箱验证 - 发送验证码验证邮箱
  2. 实现刷新令牌 - 增加安全性
  3. 添加速率限制 - 防止暴力破解
  4. 用户权限管理 - 基于角色的访问控制
  5. 社交登录 - 集成 OAuth2GitHub、Google
  6. 单元测试 - 为关键业务逻辑编写测试
  7. Docker 打包 - 容器化部署
  8. CI/CD 流程 - 自动化测试和部署

祝你编码愉快!🚀 这个项目整合了 Go Web 开发的所有核心知识,是学习和面试的好材料。