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