healthapp
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

10 KiB

03-用户认证模块

目标

实现用户注册、登录、Token 刷新等认证功能。


前置要求

  • 数据库和模型已完成
  • JWT 依赖已安装

实施步骤

步骤 1:创建统一响应工具

创建 server/pkg/response/response.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

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

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

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

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

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

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

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,添加路由启动:

// ... 前面的初始化代码 ...

// 初始化 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

注册新用户

请求体:

{
  "phone": "13800138000",
  "password": "123456",
  "nickname": "小明"
}

响应:

{
  "code": 0,
  "message": "success",
  "data": {
    "token": "eyJhbGc...",
    "user_id": 1,
    "nickname": "小明",
    "survey_completed": false
  }
}

POST /api/auth/login

用户登录

请求体:

{
  "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