- 实现用户注册、登录、JWT令牌认证功能 - 集成Gin、GORM、Viper、Zap等框架 - 添加密码加密、数据库操作、中间件等完整功能 - 配置多环境支持、日志轮转、CORS处理 - 创建完整的项目结构和配置文件体系
827 lines
18 KiB
Markdown
827 lines
18 KiB
Markdown
---
|
||
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. **社交登录** - 集成 OAuth2(GitHub、Google)
|
||
6. **单元测试** - 为关键业务逻辑编写测试
|
||
7. **Docker 打包** - 容器化部署
|
||
8. **CI/CD 流程** - 自动化测试和部署
|
||
|
||
---
|
||
|
||
祝你编码愉快!🚀 这个项目整合了 Go Web 开发的所有核心知识,是学习和面试的好材料。
|
||
|