feat(auth): 添加完整的用户认证API项目

- 实现用户注册、登录、JWT令牌认证功能
- 集成Gin、GORM、Viper、Zap等框架
- 添加密码加密、数据库操作、中间件等完整功能
- 配置多环境支持、日志轮转、CORS处理
- 创建完整的项目结构和配置文件体系
This commit is contained in:
liumangmang
2025-12-30 18:00:42 +08:00
parent 7f4527d501
commit b010f82221
139 changed files with 2772 additions and 103 deletions

View File

@@ -0,0 +1,826 @@
---
title: 综合实战项目
icon: mdi:shield-account
date: 2025-12-23
category:
- Go
- 后端
- 工程化
- 实战项目
tag:
- API
- 用户认证
- 密码加密
- JWT
- 综合实战
---
整合 Gin、GORM、Viper、Zap 等框架,开发一个完整的用户注册/登录 API。这个项目包含用户认证、密码加密、JWT 令牌和数据库操作,是学习 Go Web 开发的完美案例。
<!-- more -->
---
# 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 创建项目
```bash
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 创建目录
```bash
mkdir -p config logs
```
---
## 三、配置文件
### 3.1 config/app.yaml
```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
```yaml
app:
port: 80
env: prod
logging:
level: warn
```
---
## 四、模型定义models.go
```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
```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
```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
```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
```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
```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
```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
```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
```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 注册用户
```bash
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 用户登录
```bash
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 获取用户信息(需要认证)
```bash
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 开发的所有核心知识,是学习和面试的好材料。