# 03-用户认证模块 ## 目标 实现用户注册、登录、Token 刷新等认证功能。 --- ## 前置要求 - 数据库和模型已完成 - JWT 依赖已安装 --- ## 实施步骤 ### 步骤 1:创建统一响应工具 创建 `server/pkg/response/response.go`: ```go package response 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 Error(c *gin.Context, code int, message string) { c.JSON(http.StatusOK, Response{ Code: code, Message: message, }) } func Unauthorized(c *gin.Context, message string) { c.JSON(http.StatusUnauthorized, Response{ Code: 401, Message: message, }) } ``` ### 步骤 2:创建 JWT 工具 创建 `server/pkg/jwt/jwt.go`: ```go package jwt import ( "errors" "time" "github.com/golang-jwt/jwt/v5" ) var jwtSecret []byte var expireHours int func Init(secret string, hours int) { jwtSecret = []byte(secret) expireHours = hours } type Claims struct { UserID uint `json:"user_id"` jwt.RegisteredClaims } func GenerateToken(userID uint) (string, error) { claims := Claims{ UserID: userID, RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(expireHours) * time.Hour)), IssuedAt: jwt.NewNumericDate(time.Now()), }, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) return token.SignedString(jwtSecret) } func ParseToken(tokenString string) (*Claims, error) { token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) { return jwtSecret, 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") } ``` ### 步骤 3:创建认证中间件 创建 `server/internal/api/middleware/auth.go`: ```go package middleware import ( "strings" "health-ai/pkg/jwt" "health-ai/pkg/response" "github.com/gin-gonic/gin" ) func AuthRequired() gin.HandlerFunc { return func(c *gin.Context) { authHeader := c.GetHeader("Authorization") if authHeader == "" { response.Unauthorized(c, "未提供认证信息") c.Abort() return } parts := strings.SplitN(authHeader, " ", 2) if len(parts) != 2 || parts[0] != "Bearer" { response.Unauthorized(c, "认证格式错误") c.Abort() return } claims, err := jwt.ParseToken(parts[1]) if err != nil { response.Unauthorized(c, "Token无效或已过期") c.Abort() return } c.Set("userID", claims.UserID) c.Next() } } func GetUserID(c *gin.Context) uint { userID, _ := c.Get("userID") return userID.(uint) } ``` ### 步骤 4:创建用户 Repository 创建 `server/internal/repository/interface.go`: ```go package repository import "health-ai/internal/model" type UserRepository interface { Create(user *model.User) error GetByID(id uint) (*model.User, error) GetByPhone(phone string) (*model.User, error) GetByEmail(email string) (*model.User, error) Update(user *model.User) error } ``` 创建 `server/internal/repository/impl/user.go`: ```go package impl import ( "health-ai/internal/database" "health-ai/internal/model" ) type UserRepositoryImpl struct{} func NewUserRepository() *UserRepositoryImpl { return &UserRepositoryImpl{} } func (r *UserRepositoryImpl) Create(user *model.User) error { return database.DB.Create(user).Error } func (r *UserRepositoryImpl) GetByID(id uint) (*model.User, error) { var user model.User err := database.DB.First(&user, id).Error return &user, err } func (r *UserRepositoryImpl) GetByPhone(phone string) (*model.User, error) { var user model.User err := database.DB.Where("phone = ?", phone).First(&user).Error return &user, err } func (r *UserRepositoryImpl) GetByEmail(email string) (*model.User, error) { var user model.User err := database.DB.Where("email = ?", email).First(&user).Error return &user, err } func (r *UserRepositoryImpl) Update(user *model.User) error { return database.DB.Save(user).Error } ``` ### 步骤 5:创建认证 Service 创建 `server/internal/service/auth.go`: ```go package service import ( "errors" "health-ai/internal/model" "health-ai/internal/repository/impl" "health-ai/pkg/jwt" "golang.org/x/crypto/bcrypt" ) type AuthService struct { userRepo *impl.UserRepositoryImpl } func NewAuthService() *AuthService { return &AuthService{ userRepo: impl.NewUserRepository(), } } type RegisterRequest struct { Phone string `json:"phone" binding:"required"` Password string `json:"password" binding:"required,min=6"` Nickname string `json:"nickname"` } type LoginRequest struct { Phone string `json:"phone" binding:"required"` Password string `json:"password" binding:"required"` } type AuthResponse struct { Token string `json:"token"` UserID uint `json:"user_id"` Nickname string `json:"nickname"` SurveyCompleted bool `json:"survey_completed"` } func (s *AuthService) Register(req *RegisterRequest) (*AuthResponse, error) { // 检查手机号是否已注册 existing, _ := s.userRepo.GetByPhone(req.Phone) if existing.ID > 0 { return nil, errors.New("手机号已注册") } // 加密密码 hash, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) if err != nil { return nil, err } // 创建用户 user := &model.User{ Phone: req.Phone, PasswordHash: string(hash), Nickname: req.Nickname, } if user.Nickname == "" { user.Nickname = "用户" + req.Phone[len(req.Phone)-4:] } if err := s.userRepo.Create(user); err != nil { return nil, err } // 生成 Token token, err := jwt.GenerateToken(user.ID) if err != nil { return nil, err } return &AuthResponse{ Token: token, UserID: user.ID, Nickname: user.Nickname, SurveyCompleted: user.SurveyCompleted, }, nil } func (s *AuthService) Login(req *LoginRequest) (*AuthResponse, error) { user, err := s.userRepo.GetByPhone(req.Phone) if err != nil { return nil, errors.New("用户不存在") } if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.Password)); err != nil { return nil, errors.New("密码错误") } token, err := jwt.GenerateToken(user.ID) if err != nil { return nil, err } return &AuthResponse{ Token: token, UserID: user.ID, Nickname: user.Nickname, SurveyCompleted: user.SurveyCompleted, }, nil } ``` ### 步骤 6:创建认证 Handler 创建 `server/internal/api/handler/auth.go`: ```go package handler import ( "health-ai/internal/service" "health-ai/pkg/response" "github.com/gin-gonic/gin" ) type AuthHandler struct { authService *service.AuthService } func NewAuthHandler() *AuthHandler { return &AuthHandler{ authService: service.NewAuthService(), } } func (h *AuthHandler) Register(c *gin.Context) { var req service.RegisterRequest if err := c.ShouldBindJSON(&req); err != nil { response.Error(c, 400, "参数错误: "+err.Error()) return } result, err := h.authService.Register(&req) if err != nil { response.Error(c, 500, err.Error()) return } response.Success(c, result) } func (h *AuthHandler) Login(c *gin.Context) { var req service.LoginRequest if err := c.ShouldBindJSON(&req); err != nil { response.Error(c, 400, "参数错误: "+err.Error()) return } result, err := h.authService.Login(&req) if err != nil { response.Error(c, 500, err.Error()) return } response.Success(c, result) } ``` ### 步骤 7:创建路由配置 创建 `server/internal/api/router.go`: ```go package api import ( "health-ai/internal/api/handler" "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" ) func SetupRouter(mode string) *gin.Engine { gin.SetMode(mode) r := gin.Default() // 跨域配置 r.Use(cors.New(cors.Config{ AllowOrigins: []string{"*"}, AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, AllowHeaders: []string{"Origin", "Content-Type", "Authorization"}, AllowCredentials: true, })) // 健康检查 r.GET("/health", func(c *gin.Context) { c.JSON(200, gin.H{"status": "ok"}) }) // API 路由组 apiGroup := r.Group("/api") { // 认证路由(无需登录) authHandler := handler.NewAuthHandler() authGroup := apiGroup.Group("/auth") { authGroup.POST("/register", authHandler.Register) authGroup.POST("/login", authHandler.Login) } } return r } ``` ### 步骤 8:更新主程序 更新 `server/cmd/server/main.go`,添加路由启动: ```go // ... 前面的初始化代码 ... // 初始化 JWT jwt.Init(config.AppConfig.JWT.Secret, config.AppConfig.JWT.ExpireHours) // 启动服务器 router := api.SetupRouter(config.AppConfig.Server.Mode) addr := fmt.Sprintf(":%d", config.AppConfig.Server.Port) log.Printf("Server running on http://localhost%s", addr) router.Run(addr) ``` --- ## API 接口说明 ### POST /api/auth/register 注册新用户 **请求体:** ```json { "phone": "13800138000", "password": "123456", "nickname": "小明" } ``` **响应:** ```json { "code": 0, "message": "success", "data": { "token": "eyJhbGc...", "user_id": 1, "nickname": "小明", "survey_completed": false } } ``` ### POST /api/auth/login 用户登录 **请求体:** ```json { "phone": "13800138000", "password": "123456" } ``` --- ## 需要创建的文件清单 | 文件路径 | 说明 | |----------|------| | `pkg/response/response.go` | 统一响应 | | `pkg/jwt/jwt.go` | JWT 工具 | | `internal/api/middleware/auth.go` | 认证中间件 | | `internal/repository/interface.go` | Repository 接口 | | `internal/repository/impl/user.go` | 用户 Repository | | `internal/service/auth.go` | 认证 Service | | `internal/api/handler/auth.go` | 认证 Handler | | `internal/api/router.go` | 路由配置 | --- ## 验收标准 - [ ] 服务启动成功,监听 8080 端口 - [ ] `/health` 返回 `{"status": "ok"}` - [ ] 注册接口正常创建用户 - [ ] 登录接口返回有效 Token - [ ] 密码错误返回正确错误信息 --- ## 预计耗时 30-40 分钟 --- ## 下一步 完成后进入 `02-后端开发/04-健康调查模块.md`