feat(auth): 完整认证系统 — 注册/登录/JWT/路由(Task 4 + Task 5)
Task 4 — 认证服务层: - pkg/utils/password.go: bcrypt 密码加密与校验 - pkg/utils/jwt.go: Access/Refresh Token 生成与解析(jwt/v5@v5.2.1,兼容 Go 1.23) - app/dto/auth_dto.go: 认证相关请求/响应 DTO(含参数校验 binding tag) - app/auth/service/auth_service.go: 核心业务逻辑(注册、登录、管理员登录、Token 刷新、个人信息、改密码) - pkg/middleware/auth.go: JWT 认证中间件 + 角色权限检查中间件 Task 5 — Controller 与路由注册: - app/auth/controller/auth_controller.go: 前台认证接口(7 个 API) - app/auth/controller/admin_auth_controller.go: 后台管理认证接口 - app/auth/router.go: auth 模块路由定义(模块自包含) - router/router.go: 主路由汇总入口(仅做调度,不含具体路由定义) - cmd/server/main.go: 路由注册迁移到 router.Setup() 已验证全部接口:注册 ✓ 登录 ✓ 获取 profile ✓ 错误密码 ✓ 未认证拒绝 ✓ 非管理员拒绝 ✓ Made-with: Cursor
This commit is contained in:
@@ -0,0 +1,48 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/echochat/backend/app/auth/service"
|
||||||
|
"github.com/echochat/backend/app/dto"
|
||||||
|
"github.com/echochat/backend/pkg/logs"
|
||||||
|
"github.com/echochat/backend/pkg/utils"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AdminAuthController 后台管理认证控制器
|
||||||
|
// 与前台 AuthController 的区别:登录后额外检查管理员角色权限
|
||||||
|
type AdminAuthController struct {
|
||||||
|
authService *service.AuthService
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAdminAuthController 创建后台认证控制器实例
|
||||||
|
func NewAdminAuthController(authService *service.AuthService) *AdminAuthController {
|
||||||
|
return &AdminAuthController{authService: authService}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdminLogin 管理员登录
|
||||||
|
// POST /api/v1/admin/auth/login
|
||||||
|
func (ctrl *AdminAuthController) AdminLogin(c *gin.Context) {
|
||||||
|
funcName := "controller.admin_auth_controller.AdminLogin"
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
|
||||||
|
var req dto.LoginRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
logs.Warn(ctx, funcName, "参数校验失败", zap.Error(err))
|
||||||
|
utils.ResponseBadRequest(c, "参数校验失败: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logs.Info(ctx, funcName, "管理员登录请求",
|
||||||
|
zap.String("account", req.Account),
|
||||||
|
zap.String("ip", c.ClientIP()),
|
||||||
|
)
|
||||||
|
|
||||||
|
resp, err := ctrl.authService.AdminLogin(ctx, &req, c.ClientIP())
|
||||||
|
if err != nil {
|
||||||
|
handleAuthError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.ResponseOK(c, resp)
|
||||||
|
}
|
||||||
217
backend/go-service/app/auth/controller/auth_controller.go
Normal file
217
backend/go-service/app/auth/controller/auth_controller.go
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
// Package controller 提供 auth 模块的 HTTP 接口处理
|
||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/echochat/backend/app/auth/service"
|
||||||
|
"github.com/echochat/backend/app/dto"
|
||||||
|
"github.com/echochat/backend/pkg/logs"
|
||||||
|
"github.com/echochat/backend/pkg/middleware"
|
||||||
|
"github.com/echochat/backend/pkg/utils"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AuthController 前台认证控制器
|
||||||
|
// 处理用户注册、登录、Token 刷新、个人信息管理等接口
|
||||||
|
type AuthController struct {
|
||||||
|
authService *service.AuthService
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAuthController 创建前台认证控制器实例
|
||||||
|
func NewAuthController(authService *service.AuthService) *AuthController {
|
||||||
|
return &AuthController{authService: authService}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register 用户注册
|
||||||
|
// POST /api/v1/auth/register
|
||||||
|
func (ctrl *AuthController) Register(c *gin.Context) {
|
||||||
|
funcName := "controller.auth_controller.Register"
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
|
||||||
|
var req dto.RegisterRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
logs.Warn(ctx, funcName, "参数校验失败", zap.Error(err))
|
||||||
|
utils.ResponseBadRequest(c, "参数校验失败: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logs.Info(ctx, funcName, "注册请求",
|
||||||
|
zap.String("username", req.Username),
|
||||||
|
zap.String("email", logs.MaskEmail(req.Email)),
|
||||||
|
)
|
||||||
|
|
||||||
|
resp, err := ctrl.authService.Register(ctx, &req)
|
||||||
|
if err != nil {
|
||||||
|
handleAuthError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.ResponseCreated(c, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Login 用户登录
|
||||||
|
// POST /api/v1/auth/login
|
||||||
|
func (ctrl *AuthController) Login(c *gin.Context) {
|
||||||
|
funcName := "controller.auth_controller.Login"
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
|
||||||
|
var req dto.LoginRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
logs.Warn(ctx, funcName, "参数校验失败", zap.Error(err))
|
||||||
|
utils.ResponseBadRequest(c, "参数校验失败: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logs.Info(ctx, funcName, "登录请求",
|
||||||
|
zap.String("account", req.Account),
|
||||||
|
zap.String("ip", c.ClientIP()),
|
||||||
|
)
|
||||||
|
|
||||||
|
resp, err := ctrl.authService.Login(ctx, &req, c.ClientIP())
|
||||||
|
if err != nil {
|
||||||
|
handleAuthError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.ResponseOK(c, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logout 用户登出
|
||||||
|
// POST /api/v1/auth/logout(需认证)
|
||||||
|
func (ctrl *AuthController) Logout(c *gin.Context) {
|
||||||
|
funcName := "controller.auth_controller.Logout"
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
|
||||||
|
userID, _ := middleware.GetCurrentUserID(c)
|
||||||
|
logs.Info(ctx, funcName, "用户登出", zap.Int64("user_id", userID))
|
||||||
|
|
||||||
|
// 当前为无状态 JWT,登出由客户端删除 Token 实现
|
||||||
|
// 后续可扩展:将 Token 加入 Redis 黑名单
|
||||||
|
utils.ResponseOK(c, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RefreshToken 刷新 Access Token
|
||||||
|
// POST /api/v1/auth/refresh-token
|
||||||
|
func (ctrl *AuthController) RefreshToken(c *gin.Context) {
|
||||||
|
funcName := "controller.auth_controller.RefreshToken"
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
|
||||||
|
var req dto.RefreshTokenRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
logs.Warn(ctx, funcName, "参数校验失败", zap.Error(err))
|
||||||
|
utils.ResponseBadRequest(c, "参数校验失败: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := ctrl.authService.RefreshToken(ctx, req.RefreshToken)
|
||||||
|
if err != nil {
|
||||||
|
handleAuthError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.ResponseOK(c, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetProfile 获取当前用户信息
|
||||||
|
// GET /api/v1/auth/profile(需认证)
|
||||||
|
func (ctrl *AuthController) GetProfile(c *gin.Context) {
|
||||||
|
funcName := "controller.auth_controller.GetProfile"
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
|
||||||
|
userID, ok := middleware.GetCurrentUserID(c)
|
||||||
|
if !ok {
|
||||||
|
utils.ResponseUnauthorized(c, "无法获取用户信息")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logs.Debug(ctx, funcName, "获取个人信息", zap.Int64("user_id", userID))
|
||||||
|
|
||||||
|
userInfo, err := ctrl.authService.GetProfile(ctx, userID)
|
||||||
|
if err != nil {
|
||||||
|
handleAuthError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.ResponseOK(c, userInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateProfile 更新个人资料
|
||||||
|
// PUT /api/v1/auth/profile(需认证)
|
||||||
|
func (ctrl *AuthController) UpdateProfile(c *gin.Context) {
|
||||||
|
funcName := "controller.auth_controller.UpdateProfile"
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
|
||||||
|
userID, ok := middleware.GetCurrentUserID(c)
|
||||||
|
if !ok {
|
||||||
|
utils.ResponseUnauthorized(c, "无法获取用户信息")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req dto.UpdateProfileRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
logs.Warn(ctx, funcName, "参数校验失败", zap.Error(err))
|
||||||
|
utils.ResponseBadRequest(c, "参数校验失败: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logs.Info(ctx, funcName, "更新个人资料", zap.Int64("user_id", userID))
|
||||||
|
|
||||||
|
userInfo, err := ctrl.authService.UpdateProfile(ctx, userID, &req)
|
||||||
|
if err != nil {
|
||||||
|
handleAuthError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.ResponseOK(c, userInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChangePassword 修改密码
|
||||||
|
// PUT /api/v1/auth/password(需认证)
|
||||||
|
func (ctrl *AuthController) ChangePassword(c *gin.Context) {
|
||||||
|
funcName := "controller.auth_controller.ChangePassword"
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
|
||||||
|
userID, ok := middleware.GetCurrentUserID(c)
|
||||||
|
if !ok {
|
||||||
|
utils.ResponseUnauthorized(c, "无法获取用户信息")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req dto.ChangePasswordRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
logs.Warn(ctx, funcName, "参数校验失败", zap.Error(err))
|
||||||
|
utils.ResponseBadRequest(c, "参数校验失败: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logs.Info(ctx, funcName, "修改密码", zap.Int64("user_id", userID))
|
||||||
|
|
||||||
|
if err := ctrl.authService.ChangePassword(ctx, userID, &req); err != nil {
|
||||||
|
handleAuthError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.ResponseOK(c, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleAuthError 统一处理认证相关业务错误,映射到对应的 HTTP 状态码
|
||||||
|
func handleAuthError(c *gin.Context, err error) {
|
||||||
|
switch err {
|
||||||
|
case service.ErrUserAlreadyExists:
|
||||||
|
utils.ResponseBadRequest(c, err.Error())
|
||||||
|
case service.ErrUserNotFound:
|
||||||
|
utils.ResponseNotFound(c, err.Error())
|
||||||
|
case service.ErrPasswordWrong:
|
||||||
|
utils.ResponseUnauthorized(c, "账号或密码错误")
|
||||||
|
case service.ErrUserDisabled:
|
||||||
|
utils.ResponseForbidden(c, err.Error())
|
||||||
|
case service.ErrUserDeleted:
|
||||||
|
utils.ResponseForbidden(c, err.Error())
|
||||||
|
case service.ErrNotAdmin:
|
||||||
|
utils.ResponseForbidden(c, err.Error())
|
||||||
|
case service.ErrRefreshTokenType:
|
||||||
|
utils.ResponseBadRequest(c, err.Error())
|
||||||
|
default:
|
||||||
|
utils.ResponseError(c, "服务器内部错误")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,12 +3,13 @@ package model
|
|||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
// Role 角色表模型,对应 auth_roles 表
|
// Role 角色表模型,对应 auth_roles 表
|
||||||
|
// 系统预置三种角色:user(普通用户)、admin(管理员)、super_admin(超级管理员)
|
||||||
type Role struct {
|
type Role struct {
|
||||||
ID int `json:"id" gorm:"primaryKey;autoIncrement"`
|
ID int `json:"id" gorm:"primaryKey;autoIncrement"` // 角色唯一标识,自增主键
|
||||||
Code string `json:"code" gorm:"uniqueIndex;size:50;not null"` // 角色代码: user/admin/super_admin
|
Code string `json:"code" gorm:"uniqueIndex;size:50;not null"` // 角色代码,全局唯一,如 user/admin/super_admin(对应 constants.RoleCode*)
|
||||||
Name string `json:"name" gorm:"size:50;not null"` // 角色显示名称
|
Name string `json:"name" gorm:"size:50;not null"` // 角色中文显示名称,如「普通用户」「管理员」
|
||||||
Description string `json:"description" gorm:"size:200;default:''"`
|
Description string `json:"description" gorm:"size:200;default:''"` // 角色描述说明,用于后台管理界面展示
|
||||||
CreatedAt time.Time `json:"created_at" gorm:"not null;autoCreateTime"`
|
CreatedAt time.Time `json:"created_at" gorm:"not null;autoCreateTime"` // 创建时间,由 GORM 自动填充
|
||||||
}
|
}
|
||||||
|
|
||||||
// TableName 指定数据库表名
|
// TableName 指定数据库表名
|
||||||
@@ -17,10 +18,11 @@ func (Role) TableName() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// UserRole 用户角色关联表模型,对应 auth_user_roles 表
|
// UserRole 用户角色关联表模型,对应 auth_user_roles 表
|
||||||
|
// 采用联合主键(user_id + role_id),一个用户可拥有多个角色
|
||||||
type UserRole struct {
|
type UserRole struct {
|
||||||
UserID int64 `json:"user_id" gorm:"primaryKey"`
|
UserID int64 `json:"user_id" gorm:"primaryKey"` // 关联的用户 ID,外键指向 auth_users.id
|
||||||
RoleID int `json:"role_id" gorm:"primaryKey"`
|
RoleID int `json:"role_id" gorm:"primaryKey"` // 关联的角色 ID,外键指向 auth_roles.id
|
||||||
CreatedAt time.Time `json:"created_at" gorm:"not null;autoCreateTime"`
|
CreatedAt time.Time `json:"created_at" gorm:"not null;autoCreateTime"` // 角色分配时间,由 GORM 自动填充
|
||||||
}
|
}
|
||||||
|
|
||||||
// TableName 指定数据库表名
|
// TableName 指定数据库表名
|
||||||
|
|||||||
@@ -5,19 +5,19 @@ import "time"
|
|||||||
|
|
||||||
// User 用户主表模型,对应 auth_users 表
|
// User 用户主表模型,对应 auth_users 表
|
||||||
type User struct {
|
type User struct {
|
||||||
ID int64 `json:"id" gorm:"primaryKey;autoIncrement"`
|
ID int64 `json:"id" gorm:"primaryKey;autoIncrement"` // 用户唯一标识,自增主键
|
||||||
Username string `json:"username" gorm:"uniqueIndex;size:50;not null"`
|
Username string `json:"username" gorm:"uniqueIndex;size:50;not null"` // 登录用户名,全局唯一
|
||||||
Email string `json:"email" gorm:"uniqueIndex;size:100;not null"`
|
Email string `json:"email" gorm:"uniqueIndex;size:100;not null"` // 邮箱地址,全局唯一,用于登录和找回密码
|
||||||
PasswordHash string `json:"-" gorm:"column:password_hash;size:255;not null"` // JSON 序列化时隐藏密码
|
PasswordHash string `json:"-" gorm:"column:password_hash;size:255;not null"` // bcrypt 加密后的密码哈希,JSON 序列化时隐藏
|
||||||
Nickname string `json:"nickname" gorm:"size:50;not null;default:''"`
|
Nickname string `json:"nickname" gorm:"size:50;not null;default:''"` // 用户昵称,用于页面展示,允许重复
|
||||||
Avatar string `json:"avatar" gorm:"size:500;not null;default:''"`
|
Avatar string `json:"avatar" gorm:"size:500;not null;default:''"` // 头像 URL 地址,为空则使用默认头像
|
||||||
Gender int `json:"gender" gorm:"not null;default:0"` // 0=未知, 1=男, 2=女
|
Gender int `json:"gender" gorm:"not null;default:0"` // 性别:0=未知, 1=男, 2=女(对应 constants.Gender*)
|
||||||
Phone *string `json:"phone" gorm:"size:20"` // 可选字段
|
Phone *string `json:"phone" gorm:"size:20"` // 手机号,可选字段,指针类型允许 NULL
|
||||||
Status int `json:"status" gorm:"not null;default:1"` // 1=正常, 2=禁用, 3=注销
|
Status int `json:"status" gorm:"not null;default:1"` // 账号状态:1=正常, 2=禁用, 3=注销(对应 constants.UserStatus*)
|
||||||
LastLoginAt *time.Time `json:"last_login_at"`
|
LastLoginAt *time.Time `json:"last_login_at"` // 最后登录时间,首次注册时为 NULL
|
||||||
LastLoginIP *string `json:"last_login_ip" gorm:"column:last_login_ip;size:50"`
|
LastLoginIP *string `json:"last_login_ip" gorm:"column:last_login_ip;size:50"` // 最后登录 IP 地址,用于安全审计
|
||||||
CreatedAt time.Time `json:"created_at" gorm:"not null;autoCreateTime"`
|
CreatedAt time.Time `json:"created_at" gorm:"not null;autoCreateTime"` // 账号创建时间,由 GORM 自动填充
|
||||||
UpdatedAt time.Time `json:"updated_at" gorm:"not null;autoUpdateTime"`
|
UpdatedAt time.Time `json:"updated_at" gorm:"not null;autoUpdateTime"` // 最后更新时间,由 GORM 自动更新
|
||||||
}
|
}
|
||||||
|
|
||||||
// TableName 指定数据库表名
|
// TableName 指定数据库表名
|
||||||
|
|||||||
@@ -2,12 +2,18 @@
|
|||||||
package auth
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/echochat/backend/app/auth/controller"
|
||||||
"github.com/echochat/backend/app/auth/dao"
|
"github.com/echochat/backend/app/auth/dao"
|
||||||
|
"github.com/echochat/backend/app/auth/service"
|
||||||
"github.com/google/wire"
|
"github.com/google/wire"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AuthSet Auth 模块依赖注入 Provider Set
|
// AuthSet Auth 模块依赖注入 Provider Set
|
||||||
|
// 提供 DAO 层、Service 层和 Controller 层的所有组件
|
||||||
var AuthSet = wire.NewSet(
|
var AuthSet = wire.NewSet(
|
||||||
dao.NewUserDAO,
|
dao.NewUserDAO,
|
||||||
dao.NewRoleDAO,
|
dao.NewRoleDAO,
|
||||||
|
service.NewAuthService,
|
||||||
|
controller.NewAuthController,
|
||||||
|
controller.NewAdminAuthController,
|
||||||
)
|
)
|
||||||
|
|||||||
40
backend/go-service/app/auth/router.go
Normal file
40
backend/go-service/app/auth/router.go
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/echochat/backend/app/auth/controller"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RegisterRoutes 注册 auth 模块的所有路由
|
||||||
|
// 包含前台公开路由、前台需认证路由、后台管理认证路由
|
||||||
|
// authMiddleware 为 JWT 认证中间件,由外部传入以解耦
|
||||||
|
func RegisterRoutes(
|
||||||
|
r *gin.Engine,
|
||||||
|
ctrl *controller.AuthController,
|
||||||
|
adminCtrl *controller.AdminAuthController,
|
||||||
|
authMiddleware gin.HandlerFunc,
|
||||||
|
) {
|
||||||
|
// 前台公开路由(无需认证)
|
||||||
|
public := r.Group("/api/v1/auth")
|
||||||
|
{
|
||||||
|
public.POST("/register", ctrl.Register)
|
||||||
|
public.POST("/login", ctrl.Login)
|
||||||
|
public.POST("/refresh-token", ctrl.RefreshToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 前台需认证路由
|
||||||
|
authed := r.Group("/api/v1/auth")
|
||||||
|
authed.Use(authMiddleware)
|
||||||
|
{
|
||||||
|
authed.POST("/logout", ctrl.Logout)
|
||||||
|
authed.GET("/profile", ctrl.GetProfile)
|
||||||
|
authed.PUT("/profile", ctrl.UpdateProfile)
|
||||||
|
authed.PUT("/password", ctrl.ChangePassword)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 后台管理认证路由(无需认证,登录后在 Service 层检查管理员角色)
|
||||||
|
admin := r.Group("/api/v1/admin/auth")
|
||||||
|
{
|
||||||
|
admin.POST("/login", adminCtrl.AdminLogin)
|
||||||
|
}
|
||||||
|
}
|
||||||
390
backend/go-service/app/auth/service/auth_service.go
Normal file
390
backend/go-service/app/auth/service/auth_service.go
Normal file
@@ -0,0 +1,390 @@
|
|||||||
|
// Package service 提供 auth 模块的核心业务逻辑
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/echochat/backend/app/auth/dao"
|
||||||
|
"github.com/echochat/backend/app/auth/model"
|
||||||
|
"github.com/echochat/backend/app/constants"
|
||||||
|
"github.com/echochat/backend/app/dto"
|
||||||
|
"github.com/echochat/backend/config"
|
||||||
|
"github.com/echochat/backend/pkg/logs"
|
||||||
|
"github.com/echochat/backend/pkg/utils"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrUserAlreadyExists = errors.New("用户名或邮箱已被注册")
|
||||||
|
ErrUserNotFound = errors.New("用户不存在")
|
||||||
|
ErrPasswordWrong = errors.New("密码错误")
|
||||||
|
ErrUserDisabled = errors.New("账号已被禁用")
|
||||||
|
ErrUserDeleted = errors.New("账号已注销")
|
||||||
|
ErrNotAdmin = errors.New("该账号无管理员权限")
|
||||||
|
ErrRefreshTokenType = errors.New("无效的 Refresh Token 类型")
|
||||||
|
)
|
||||||
|
|
||||||
|
// AuthService 认证服务,处理注册、登录、Token 管理、个人信息等业务逻辑
|
||||||
|
type AuthService struct {
|
||||||
|
userDAO *dao.UserDAO
|
||||||
|
roleDAO *dao.RoleDAO
|
||||||
|
jwtCfg *config.JWTConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAuthService 创建认证服务实例
|
||||||
|
func NewAuthService(userDAO *dao.UserDAO, roleDAO *dao.RoleDAO, jwtCfg *config.JWTConfig) *AuthService {
|
||||||
|
return &AuthService{
|
||||||
|
userDAO: userDAO,
|
||||||
|
roleDAO: roleDAO,
|
||||||
|
jwtCfg: jwtCfg,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register 用户注册
|
||||||
|
// 流程:检查用户名/邮箱是否重复 → 加密密码 → 创建用户 → 分配默认角色 → 生成 Token
|
||||||
|
func (s *AuthService) Register(ctx context.Context, req *dto.RegisterRequest) (*dto.LoginResponse, error) {
|
||||||
|
funcName := "service.auth_service.Register"
|
||||||
|
logs.Info(ctx, funcName, "开始处理注册",
|
||||||
|
zap.String("username", req.Username),
|
||||||
|
zap.String("email", logs.MaskEmail(req.Email)),
|
||||||
|
)
|
||||||
|
|
||||||
|
var err error
|
||||||
|
defer func() {
|
||||||
|
if err != nil {
|
||||||
|
logs.Error(ctx, funcName, "注册处理失败",
|
||||||
|
zap.String("username", req.Username),
|
||||||
|
zap.Error(err),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 检查用户名是否已存在
|
||||||
|
existing, _ := s.userDAO.FindByUsername(ctx, req.Username)
|
||||||
|
if existing != nil {
|
||||||
|
err = ErrUserAlreadyExists
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查邮箱是否已存在
|
||||||
|
existing, _ = s.userDAO.FindByEmail(ctx, req.Email)
|
||||||
|
if existing != nil {
|
||||||
|
err = ErrUserAlreadyExists
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加密密码
|
||||||
|
hashedPassword, hashErr := utils.HashPassword(req.Password)
|
||||||
|
if hashErr != nil {
|
||||||
|
err = hashErr
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建用户
|
||||||
|
nickname := req.Nickname
|
||||||
|
if nickname == "" {
|
||||||
|
nickname = req.Username
|
||||||
|
}
|
||||||
|
user := &model.User{
|
||||||
|
Username: req.Username,
|
||||||
|
Email: req.Email,
|
||||||
|
PasswordHash: hashedPassword,
|
||||||
|
Nickname: nickname,
|
||||||
|
Status: constants.UserStatusActive,
|
||||||
|
}
|
||||||
|
if err = s.userDAO.Create(ctx, user); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分配默认角色(普通用户)
|
||||||
|
defaultRole, roleErr := s.roleDAO.FindByCode(ctx, constants.RoleUser)
|
||||||
|
if roleErr != nil {
|
||||||
|
logs.Warn(ctx, funcName, "查找默认角色失败,跳过角色分配", zap.Error(roleErr))
|
||||||
|
} else {
|
||||||
|
if assignErr := s.roleDAO.AssignRole(ctx, user.ID, defaultRole.ID); assignErr != nil {
|
||||||
|
logs.Warn(ctx, funcName, "分配默认角色失败", zap.Error(assignErr))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取角色列表并生成 Token
|
||||||
|
roles, _ := s.roleDAO.GetUserRoleCodes(ctx, user.ID)
|
||||||
|
resp, err := s.buildLoginResponse(user, roles)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
logs.Info(ctx, funcName, "注册成功", zap.Int64("user_id", user.ID))
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Login 用户登录
|
||||||
|
// 流程:查找用户 → 校验密码 → 检查账号状态 → 更新登录信息 → 生成 Token
|
||||||
|
func (s *AuthService) Login(ctx context.Context, req *dto.LoginRequest, clientIP string) (*dto.LoginResponse, error) {
|
||||||
|
funcName := "service.auth_service.Login"
|
||||||
|
logs.Info(ctx, funcName, "开始处理登录",
|
||||||
|
zap.String("account", req.Account),
|
||||||
|
)
|
||||||
|
|
||||||
|
var err error
|
||||||
|
defer func() {
|
||||||
|
if err != nil {
|
||||||
|
logs.Error(ctx, funcName, "登录处理失败",
|
||||||
|
zap.String("account", req.Account),
|
||||||
|
zap.Error(err),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 按用户名或邮箱查找用户
|
||||||
|
user, findErr := s.userDAO.FindByAccount(ctx, req.Account)
|
||||||
|
if findErr != nil {
|
||||||
|
if errors.Is(findErr, gorm.ErrRecordNotFound) {
|
||||||
|
err = ErrUserNotFound
|
||||||
|
} else {
|
||||||
|
err = findErr
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 校验密码
|
||||||
|
if !utils.CheckPassword(req.Password, user.PasswordHash) {
|
||||||
|
err = ErrPasswordWrong
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查账号状态
|
||||||
|
if err = s.checkUserStatus(user.Status); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新最后登录信息
|
||||||
|
_ = s.userDAO.UpdateLastLogin(ctx, user.ID, clientIP)
|
||||||
|
|
||||||
|
// 获取角色列表并生成 Token
|
||||||
|
roles, _ := s.roleDAO.GetUserRoleCodes(ctx, user.ID)
|
||||||
|
resp, err := s.buildLoginResponse(user, roles)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
logs.Info(ctx, funcName, "登录成功",
|
||||||
|
zap.Int64("user_id", user.ID),
|
||||||
|
zap.String("username", user.Username),
|
||||||
|
)
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdminLogin 管理后台登录
|
||||||
|
// 与普通登录相同,但额外检查用户是否拥有 admin 或 super_admin 角色
|
||||||
|
func (s *AuthService) AdminLogin(ctx context.Context, req *dto.LoginRequest, clientIP string) (*dto.LoginResponse, error) {
|
||||||
|
funcName := "service.auth_service.AdminLogin"
|
||||||
|
logs.Info(ctx, funcName, "开始处理管理员登录",
|
||||||
|
zap.String("account", req.Account),
|
||||||
|
)
|
||||||
|
|
||||||
|
resp, err := s.Login(ctx, req, clientIP)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否拥有管理员角色
|
||||||
|
hasAdmin := false
|
||||||
|
for _, role := range resp.User.Roles {
|
||||||
|
if role == constants.RoleAdmin || role == constants.RoleSuperAdmin {
|
||||||
|
hasAdmin = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !hasAdmin {
|
||||||
|
logs.Warn(ctx, funcName, "非管理员尝试后台登录",
|
||||||
|
zap.String("account", req.Account),
|
||||||
|
)
|
||||||
|
return nil, ErrNotAdmin
|
||||||
|
}
|
||||||
|
|
||||||
|
logs.Info(ctx, funcName, "管理员登录成功",
|
||||||
|
zap.String("account", req.Account),
|
||||||
|
)
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RefreshToken 刷新 Access Token
|
||||||
|
// 验证 Refresh Token 有效性后,重新生成一对新的 Token
|
||||||
|
func (s *AuthService) RefreshToken(ctx context.Context, refreshToken string) (*dto.LoginResponse, error) {
|
||||||
|
funcName := "service.auth_service.RefreshToken"
|
||||||
|
|
||||||
|
// 解析 Refresh Token
|
||||||
|
claims, err := utils.ParseToken(s.jwtCfg, refreshToken)
|
||||||
|
if err != nil {
|
||||||
|
logs.Warn(ctx, funcName, "Refresh Token 解析失败", zap.Error(err))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证 Token 类型
|
||||||
|
if claims.Subject != "refresh" {
|
||||||
|
return nil, ErrRefreshTokenType
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查找用户(确保用户仍然有效)
|
||||||
|
user, err := s.userDAO.FindByID(ctx, claims.UserID)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, ErrUserNotFound
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = s.checkUserStatus(user.Status); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取角色并生成新 Token
|
||||||
|
roles, _ := s.roleDAO.GetUserRoleCodes(ctx, user.ID)
|
||||||
|
resp, err := s.buildLoginResponse(user, roles)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
logs.Info(ctx, funcName, "Token 刷新成功", zap.Int64("user_id", user.ID))
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetProfile 获取用户个人信息
|
||||||
|
func (s *AuthService) GetProfile(ctx context.Context, userID int64) (*dto.UserInfo, error) {
|
||||||
|
funcName := "service.auth_service.GetProfile"
|
||||||
|
logs.Debug(ctx, funcName, "获取用户信息", zap.Int64("user_id", userID))
|
||||||
|
|
||||||
|
user, err := s.userDAO.FindByID(ctx, userID)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, ErrUserNotFound
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
roles, _ := s.roleDAO.GetUserRoleCodes(ctx, user.ID)
|
||||||
|
|
||||||
|
return s.buildUserInfo(user, roles), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateProfile 更新用户个人资料
|
||||||
|
func (s *AuthService) UpdateProfile(ctx context.Context, userID int64, req *dto.UpdateProfileRequest) (*dto.UserInfo, error) {
|
||||||
|
funcName := "service.auth_service.UpdateProfile"
|
||||||
|
logs.Info(ctx, funcName, "更新用户资料", zap.Int64("user_id", userID))
|
||||||
|
|
||||||
|
user, err := s.userDAO.FindByID(ctx, userID)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, ErrUserNotFound
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Nickname != "" {
|
||||||
|
user.Nickname = req.Nickname
|
||||||
|
}
|
||||||
|
if req.Avatar != "" {
|
||||||
|
user.Avatar = req.Avatar
|
||||||
|
}
|
||||||
|
if req.Gender != nil {
|
||||||
|
user.Gender = *req.Gender
|
||||||
|
}
|
||||||
|
if req.Phone != "" {
|
||||||
|
user.Phone = &req.Phone
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = s.userDAO.Update(ctx, user); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
roles, _ := s.roleDAO.GetUserRoleCodes(ctx, user.ID)
|
||||||
|
logs.Info(ctx, funcName, "用户资料更新成功", zap.Int64("user_id", userID))
|
||||||
|
return s.buildUserInfo(user, roles), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChangePassword 修改密码
|
||||||
|
func (s *AuthService) ChangePassword(ctx context.Context, userID int64, req *dto.ChangePasswordRequest) error {
|
||||||
|
funcName := "service.auth_service.ChangePassword"
|
||||||
|
logs.Info(ctx, funcName, "修改密码", zap.Int64("user_id", userID))
|
||||||
|
|
||||||
|
user, err := s.userDAO.FindByID(ctx, userID)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return ErrUserNotFound
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 校验旧密码
|
||||||
|
if !utils.CheckPassword(req.OldPassword, user.PasswordHash) {
|
||||||
|
return ErrPasswordWrong
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加密新密码
|
||||||
|
hashedPassword, err := utils.HashPassword(req.NewPassword)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
user.PasswordHash = hashedPassword
|
||||||
|
if err = s.userDAO.Update(ctx, user); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
logs.Info(ctx, funcName, "密码修改成功", zap.Int64("user_id", userID))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkUserStatus 检查用户账号状态
|
||||||
|
func (s *AuthService) checkUserStatus(status int) error {
|
||||||
|
switch status {
|
||||||
|
case constants.UserStatusDisabled:
|
||||||
|
return ErrUserDisabled
|
||||||
|
case constants.UserStatusDeleted:
|
||||||
|
return ErrUserDeleted
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildLoginResponse 构建登录响应(生成 Token + 用户信息)
|
||||||
|
func (s *AuthService) buildLoginResponse(user *model.User, roles []string) (*dto.LoginResponse, error) {
|
||||||
|
if roles == nil {
|
||||||
|
roles = []string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := utils.GenerateToken(s.jwtCfg, user.ID, user.Username, roles)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshToken, err := utils.GenerateRefreshToken(s.jwtCfg, user.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &dto.LoginResponse{
|
||||||
|
Token: token,
|
||||||
|
RefreshToken: refreshToken,
|
||||||
|
ExpiresIn: int64(s.jwtCfg.AccessExpireMin) * 60,
|
||||||
|
User: *s.buildUserInfo(user, roles),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildUserInfo 从 model.User 构建 dto.UserInfo
|
||||||
|
func (s *AuthService) buildUserInfo(user *model.User, roles []string) *dto.UserInfo {
|
||||||
|
if roles == nil {
|
||||||
|
roles = []string{}
|
||||||
|
}
|
||||||
|
return &dto.UserInfo{
|
||||||
|
ID: user.ID,
|
||||||
|
Username: user.Username,
|
||||||
|
Email: user.Email,
|
||||||
|
Nickname: user.Nickname,
|
||||||
|
Avatar: user.Avatar,
|
||||||
|
Gender: user.Gender,
|
||||||
|
Roles: roles,
|
||||||
|
}
|
||||||
|
}
|
||||||
54
backend/go-service/app/dto/auth_dto.go
Normal file
54
backend/go-service/app/dto/auth_dto.go
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
// Package dto 定义数据传输对象,用于 Controller 层的请求/响应参数绑定
|
||||||
|
package dto
|
||||||
|
|
||||||
|
// RegisterRequest 用户注册请求参数
|
||||||
|
type RegisterRequest struct {
|
||||||
|
Username string `json:"username" binding:"required,min=3,max=50"` // 用户名,3-50 字符
|
||||||
|
Email string `json:"email" binding:"required,email"` // 邮箱地址,必须符合邮箱格式
|
||||||
|
Password string `json:"password" binding:"required,min=6,max=50"` // 密码,6-50 字符
|
||||||
|
Nickname string `json:"nickname" binding:"max=50"` // 昵称,可选,最多 50 字符
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoginRequest 用户登录请求参数
|
||||||
|
type LoginRequest struct {
|
||||||
|
Account string `json:"account" binding:"required"` // 登录账号,支持用户名或邮箱
|
||||||
|
Password string `json:"password" binding:"required"` // 登录密码
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoginResponse 登录成功响应数据
|
||||||
|
type LoginResponse struct {
|
||||||
|
Token string `json:"token"` // Access Token,用于接口认证
|
||||||
|
RefreshToken string `json:"refresh_token"` // Refresh Token,用于刷新 Access Token
|
||||||
|
ExpiresIn int64 `json:"expires_in"` // Access Token 有效期(秒)
|
||||||
|
User UserInfo `json:"user"` // 当前登录用户基本信息
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserInfo 用户基本信息(用于响应返回,不含敏感字段)
|
||||||
|
type UserInfo struct {
|
||||||
|
ID int64 `json:"id"` // 用户 ID
|
||||||
|
Username string `json:"username"` // 用户名
|
||||||
|
Email string `json:"email"` // 邮箱
|
||||||
|
Nickname string `json:"nickname"` // 昵称
|
||||||
|
Avatar string `json:"avatar"` // 头像 URL
|
||||||
|
Gender int `json:"gender"` // 性别:0=未知, 1=男, 2=女
|
||||||
|
Roles []string `json:"roles"` // 角色代码列表
|
||||||
|
}
|
||||||
|
|
||||||
|
// RefreshTokenRequest 刷新 Token 请求参数
|
||||||
|
type RefreshTokenRequest struct {
|
||||||
|
RefreshToken string `json:"refresh_token" binding:"required"` // 原 Refresh Token
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateProfileRequest 更新个人资料请求参数
|
||||||
|
type UpdateProfileRequest struct {
|
||||||
|
Nickname string `json:"nickname" binding:"max=50"` // 新昵称
|
||||||
|
Avatar string `json:"avatar" binding:"max=500"` // 新头像 URL
|
||||||
|
Gender *int `json:"gender" binding:"omitempty,oneof=0 1 2"` // 新性别,指针类型区分「未传」和「传 0」
|
||||||
|
Phone string `json:"phone" binding:"max=20"` // 新手机号
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChangePasswordRequest 修改密码请求参数
|
||||||
|
type ChangePasswordRequest struct {
|
||||||
|
OldPassword string `json:"old_password" binding:"required"` // 旧密码
|
||||||
|
NewPassword string `json:"new_password" binding:"required,min=6,max=50"` // 新密码,6-50 字符
|
||||||
|
}
|
||||||
@@ -3,6 +3,8 @@
|
|||||||
package provider
|
package provider
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/echochat/backend/app/auth/controller"
|
||||||
|
"github.com/echochat/backend/app/auth/service"
|
||||||
"github.com/echochat/backend/config"
|
"github.com/echochat/backend/config"
|
||||||
"github.com/echochat/backend/pkg/db"
|
"github.com/echochat/backend/pkg/db"
|
||||||
"github.com/google/wire"
|
"github.com/google/wire"
|
||||||
@@ -10,19 +12,32 @@ import (
|
|||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
// App 应用根容器,持有所有基础设施组件
|
// App 应用根容器,持有基础设施组件和各模块的 Controller/Service
|
||||||
type App struct {
|
type App struct {
|
||||||
Config *config.Config
|
Config *config.Config
|
||||||
DB *gorm.DB
|
DB *gorm.DB
|
||||||
Redis *redis.Client
|
Redis *redis.Client
|
||||||
|
AuthService *service.AuthService // Auth 认证服务
|
||||||
|
AuthController *controller.AuthController // 前台认证控制器
|
||||||
|
AdminAuthController *controller.AdminAuthController // 后台认证控制器
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewApp 创建应用实例
|
// NewApp 创建应用实例
|
||||||
func NewApp(cfg *config.Config, gormDB *gorm.DB, redisClient *redis.Client) *App {
|
func NewApp(
|
||||||
|
cfg *config.Config,
|
||||||
|
gormDB *gorm.DB,
|
||||||
|
redisClient *redis.Client,
|
||||||
|
authService *service.AuthService,
|
||||||
|
authCtrl *controller.AuthController,
|
||||||
|
adminAuthCtrl *controller.AdminAuthController,
|
||||||
|
) *App {
|
||||||
return &App{
|
return &App{
|
||||||
Config: cfg,
|
Config: cfg,
|
||||||
DB: gormDB,
|
DB: gormDB,
|
||||||
Redis: redisClient,
|
Redis: redisClient,
|
||||||
|
AuthService: authService,
|
||||||
|
AuthController: authCtrl,
|
||||||
|
AdminAuthController: adminAuthCtrl,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,10 +51,16 @@ func provideRedisConfig(cfg *config.Config) *config.RedisConfig {
|
|||||||
return &cfg.Redis
|
return &cfg.Redis
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// provideJWTConfig 从全局 Config 中提取 JWTConfig
|
||||||
|
func provideJWTConfig(cfg *config.Config) *config.JWTConfig {
|
||||||
|
return &cfg.JWT
|
||||||
|
}
|
||||||
|
|
||||||
// InfraSet 基础设施层 Provider Set
|
// InfraSet 基础设施层 Provider Set
|
||||||
var InfraSet = wire.NewSet(
|
var InfraSet = wire.NewSet(
|
||||||
provideDBConfig,
|
provideDBConfig,
|
||||||
provideRedisConfig,
|
provideRedisConfig,
|
||||||
|
provideJWTConfig,
|
||||||
db.NewPostgres,
|
db.NewPostgres,
|
||||||
db.NewRedis,
|
db.NewRedis,
|
||||||
NewApp,
|
NewApp,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
package provider
|
package provider
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/echochat/backend/app/auth"
|
||||||
"github.com/echochat/backend/config"
|
"github.com/echochat/backend/config"
|
||||||
"github.com/google/wire"
|
"github.com/google/wire"
|
||||||
)
|
)
|
||||||
@@ -12,6 +13,7 @@ import (
|
|||||||
func InitializeApp(cfg *config.Config) (*App, error) {
|
func InitializeApp(cfg *config.Config) (*App, error) {
|
||||||
wire.Build(
|
wire.Build(
|
||||||
InfraSet,
|
InfraSet,
|
||||||
|
auth.AuthSet,
|
||||||
)
|
)
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,9 @@
|
|||||||
package provider
|
package provider
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/echochat/backend/app/auth/controller"
|
||||||
|
"github.com/echochat/backend/app/auth/dao"
|
||||||
|
"github.com/echochat/backend/app/auth/service"
|
||||||
"github.com/echochat/backend/config"
|
"github.com/echochat/backend/config"
|
||||||
"github.com/echochat/backend/pkg/db"
|
"github.com/echochat/backend/pkg/db"
|
||||||
)
|
)
|
||||||
@@ -25,6 +28,12 @@ func InitializeApp(cfg *config.Config) (*App, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
app := NewApp(cfg, gormDB, client)
|
userDAO := dao.NewUserDAO(gormDB)
|
||||||
|
roleDAO := dao.NewRoleDAO(gormDB)
|
||||||
|
jwtConfig := provideJWTConfig(cfg)
|
||||||
|
authService := service.NewAuthService(userDAO, roleDAO, jwtConfig)
|
||||||
|
authController := controller.NewAuthController(authService)
|
||||||
|
adminAuthController := controller.NewAdminAuthController(authService)
|
||||||
|
app := NewApp(cfg, gormDB, client, authService, authController, adminAuthController)
|
||||||
return app, nil
|
return app, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ import (
|
|||||||
"github.com/echochat/backend/config"
|
"github.com/echochat/backend/config"
|
||||||
"github.com/echochat/backend/pkg/logs"
|
"github.com/echochat/backend/pkg/logs"
|
||||||
"github.com/echochat/backend/pkg/middleware"
|
"github.com/echochat/backend/pkg/middleware"
|
||||||
"github.com/echochat/backend/pkg/utils"
|
|
||||||
"github.com/echochat/backend/app/provider"
|
"github.com/echochat/backend/app/provider"
|
||||||
|
"github.com/echochat/backend/router"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
@@ -45,7 +45,6 @@ func main() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
logs.Fatal(ctx, "main", "初始化应用失败", zap.Error(err))
|
logs.Fatal(ctx, "main", "初始化应用失败", zap.Error(err))
|
||||||
}
|
}
|
||||||
_ = app
|
|
||||||
|
|
||||||
// 4. 创建 Gin Engine
|
// 4. 创建 Gin Engine
|
||||||
if cfg.Server.Mode == "release" {
|
if cfg.Server.Mode == "release" {
|
||||||
@@ -61,14 +60,8 @@ func main() {
|
|||||||
middleware.Recovery(),
|
middleware.Recovery(),
|
||||||
)
|
)
|
||||||
|
|
||||||
// 6. 注册路由
|
// 6. 注册路由(由 router.Setup 统一汇总各模块路由)
|
||||||
engine.GET("/health", func(c *gin.Context) {
|
router.Setup(engine, app)
|
||||||
utils.ResponseOK(c, gin.H{
|
|
||||||
"status": "ok",
|
|
||||||
"service": "echochat",
|
|
||||||
"time": time.Now().Format("2006-01-02 15:04:05"),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// 7. 启动 HTTP 服务(优雅关闭)
|
// 7. 启动 HTTP 服务(优雅关闭)
|
||||||
addr := fmt.Sprintf(":%d", cfg.Server.Port)
|
addr := fmt.Sprintf(":%d", cfg.Server.Port)
|
||||||
|
|||||||
@@ -4,11 +4,13 @@ go 1.23.12
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/gin-gonic/gin v1.11.0
|
github.com/gin-gonic/gin v1.11.0
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.2.1
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/google/wire v0.7.0
|
github.com/google/wire v0.7.0
|
||||||
github.com/redis/go-redis/v9 v9.18.0
|
github.com/redis/go-redis/v9 v9.18.0
|
||||||
github.com/spf13/viper v1.21.0
|
github.com/spf13/viper v1.21.0
|
||||||
go.uber.org/zap v1.27.1
|
go.uber.org/zap v1.27.1
|
||||||
|
golang.org/x/crypto v0.40.0
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||||
gorm.io/driver/postgres v1.6.0
|
gorm.io/driver/postgres v1.6.0
|
||||||
gorm.io/gorm v1.31.1
|
gorm.io/gorm v1.31.1
|
||||||
@@ -57,7 +59,6 @@ require (
|
|||||||
go.uber.org/multierr v1.10.0 // indirect
|
go.uber.org/multierr v1.10.0 // indirect
|
||||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||||
golang.org/x/arch v0.20.0 // indirect
|
golang.org/x/arch v0.20.0 // indirect
|
||||||
golang.org/x/crypto v0.40.0 // indirect
|
|
||||||
golang.org/x/mod v0.26.0 // indirect
|
golang.org/x/mod v0.26.0 // indirect
|
||||||
golang.org/x/net v0.42.0 // indirect
|
golang.org/x/net v0.42.0 // indirect
|
||||||
golang.org/x/sync v0.16.0 // indirect
|
golang.org/x/sync v0.16.0 // indirect
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
|||||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||||
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
||||||
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
|||||||
129
backend/go-service/pkg/middleware/auth.go
Normal file
129
backend/go-service/pkg/middleware/auth.go
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/echochat/backend/config"
|
||||||
|
"github.com/echochat/backend/pkg/logs"
|
||||||
|
"github.com/echochat/backend/pkg/utils"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ContextKeyUserID = "user_id" // Context 中存储当前用户 ID 的 Key
|
||||||
|
ContextKeyUsername = "username" // Context 中存储当前用户名的 Key
|
||||||
|
ContextKeyRoles = "roles" // Context 中存储当前用户角色列表的 Key
|
||||||
|
)
|
||||||
|
|
||||||
|
// JWTAuth JWT 认证中间件
|
||||||
|
// 从 Authorization Header 中提取 Bearer Token,验证后将用户信息注入 Gin Context
|
||||||
|
func JWTAuth(jwtCfg *config.JWTConfig) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
funcName := "middleware.JWTAuth"
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
|
||||||
|
authHeader := c.GetHeader("Authorization")
|
||||||
|
if authHeader == "" {
|
||||||
|
utils.ResponseUnauthorized(c, "缺少认证信息")
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提取 Bearer Token
|
||||||
|
parts := strings.SplitN(authHeader, " ", 2)
|
||||||
|
if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" {
|
||||||
|
utils.ResponseUnauthorized(c, "认证格式错误,应为 Bearer {token}")
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenStr := parts[1]
|
||||||
|
claims, err := utils.ParseToken(jwtCfg, tokenStr)
|
||||||
|
if err != nil {
|
||||||
|
logs.Warn(ctx, funcName, "Token 验证失败",
|
||||||
|
zap.Error(err),
|
||||||
|
zap.String("ip", c.ClientIP()),
|
||||||
|
)
|
||||||
|
utils.ResponseUnauthorized(c, "认证已过期或无效,请重新登录")
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证 Token 类型(只允许 access token 访问接口)
|
||||||
|
if claims.Subject != "access" {
|
||||||
|
utils.ResponseUnauthorized(c, "无效的 Token 类型")
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将用户信息注入 Gin Context,供后续 Controller/Service 使用
|
||||||
|
c.Set(ContextKeyUserID, claims.UserID)
|
||||||
|
c.Set(ContextKeyUsername, claims.Username)
|
||||||
|
c.Set(ContextKeyRoles, claims.Roles)
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequireRole 角色权限检查中间件
|
||||||
|
// 检查当前用户是否拥有指定角色之一(OR 逻辑),需在 JWTAuth 之后使用
|
||||||
|
func RequireRole(roles ...string) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
funcName := "middleware.RequireRole"
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
|
||||||
|
userRoles, exists := c.Get(ContextKeyRoles)
|
||||||
|
if !exists {
|
||||||
|
utils.ResponseForbidden(c, "无权限访问")
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
roleList, ok := userRoles.([]string)
|
||||||
|
if !ok {
|
||||||
|
utils.ResponseForbidden(c, "无权限访问")
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, required := range roles {
|
||||||
|
for _, userRole := range roleList {
|
||||||
|
if userRole == required {
|
||||||
|
c.Next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
userID, _ := c.Get(ContextKeyUserID)
|
||||||
|
logs.Warn(ctx, funcName, "角色权限不足",
|
||||||
|
zap.Any("user_id", userID),
|
||||||
|
zap.Strings("required", roles),
|
||||||
|
zap.Strings("actual", roleList),
|
||||||
|
)
|
||||||
|
utils.ResponseForbidden(c, "权限不足,需要角色: "+strings.Join(roles, " 或 "))
|
||||||
|
c.Abort()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCurrentUserID 从 Gin Context 获取当前登录用户 ID
|
||||||
|
// 供 Controller 层调用的便捷方法
|
||||||
|
func GetCurrentUserID(c *gin.Context) (int64, bool) {
|
||||||
|
val, exists := c.Get(ContextKeyUserID)
|
||||||
|
if !exists {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
userID, ok := val.(int64)
|
||||||
|
return userID, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCurrentUsername 从 Gin Context 获取当前登录用户名
|
||||||
|
func GetCurrentUsername(c *gin.Context) (string, bool) {
|
||||||
|
val, exists := c.Get(ContextKeyUsername)
|
||||||
|
if !exists {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
username, ok := val.(string)
|
||||||
|
return username, ok
|
||||||
|
}
|
||||||
86
backend/go-service/pkg/utils/jwt.go
Normal file
86
backend/go-service/pkg/utils/jwt.go
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/echochat/backend/config"
|
||||||
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Claims 自定义 JWT Claims,在标准 Claims 基础上扩展业务字段
|
||||||
|
type Claims struct {
|
||||||
|
UserID int64 `json:"user_id"` // 用户 ID
|
||||||
|
Username string `json:"username"` // 用户名
|
||||||
|
Roles []string `json:"roles"` // 用户角色代码列表
|
||||||
|
jwt.RegisteredClaims
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrTokenExpired = errors.New("token 已过期")
|
||||||
|
ErrTokenInvalid = errors.New("token 无效")
|
||||||
|
)
|
||||||
|
|
||||||
|
// GenerateToken 生成 Access Token
|
||||||
|
// 包含 UserID、Username、Roles,有效期由配置中的 access_expire_min 决定
|
||||||
|
func GenerateToken(cfg *config.JWTConfig, userID int64, username string, roles []string) (string, error) {
|
||||||
|
expireTime := time.Now().Add(time.Duration(cfg.AccessExpireMin) * time.Minute)
|
||||||
|
|
||||||
|
claims := &Claims{
|
||||||
|
UserID: userID,
|
||||||
|
Username: username,
|
||||||
|
Roles: roles,
|
||||||
|
RegisteredClaims: jwt.RegisteredClaims{
|
||||||
|
ExpiresAt: jwt.NewNumericDate(expireTime),
|
||||||
|
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||||
|
Issuer: cfg.Issuer,
|
||||||
|
Subject: "access",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||||
|
return token.SignedString([]byte(cfg.Secret))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateRefreshToken 生成 Refresh Token
|
||||||
|
// 仅包含 UserID,有效期由配置中的 refresh_expire_day 决定
|
||||||
|
func GenerateRefreshToken(cfg *config.JWTConfig, userID int64) (string, error) {
|
||||||
|
expireTime := time.Now().Add(time.Duration(cfg.RefreshExpireDay) * 24 * time.Hour)
|
||||||
|
|
||||||
|
claims := &Claims{
|
||||||
|
UserID: userID,
|
||||||
|
RegisteredClaims: jwt.RegisteredClaims{
|
||||||
|
ExpiresAt: jwt.NewNumericDate(expireTime),
|
||||||
|
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||||
|
Issuer: cfg.Issuer,
|
||||||
|
Subject: "refresh",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||||
|
return token.SignedString([]byte(cfg.Secret))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseToken 解析并验证 JWT Token
|
||||||
|
// 验证签名、过期时间、签发者,返回解析后的 Claims
|
||||||
|
func ParseToken(cfg *config.JWTConfig, tokenStr string) (*Claims, error) {
|
||||||
|
token, err := jwt.ParseWithClaims(tokenStr, &Claims{}, func(token *jwt.Token) (interface{}, error) {
|
||||||
|
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||||
|
return nil, ErrTokenInvalid
|
||||||
|
}
|
||||||
|
return []byte(cfg.Secret), nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, jwt.ErrTokenExpired) {
|
||||||
|
return nil, ErrTokenExpired
|
||||||
|
}
|
||||||
|
return nil, ErrTokenInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
claims, ok := token.Claims.(*Claims)
|
||||||
|
if !ok || !token.Valid {
|
||||||
|
return nil, ErrTokenInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
return claims, nil
|
||||||
|
}
|
||||||
17
backend/go-service/pkg/utils/password.go
Normal file
17
backend/go-service/pkg/utils/password.go
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import "golang.org/x/crypto/bcrypt"
|
||||||
|
|
||||||
|
// HashPassword 使用 bcrypt 对明文密码进行加密
|
||||||
|
// cost 使用默认值 10,在安全性和性能之间取得平衡
|
||||||
|
func HashPassword(password string) (string, error) {
|
||||||
|
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||||
|
return string(bytes), err
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckPassword 校验明文密码是否与 bcrypt 哈希匹配
|
||||||
|
// 返回 true 表示密码正确,false 表示密码错误或哈希无效
|
||||||
|
func CheckPassword(password, hash string) bool {
|
||||||
|
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
39
backend/go-service/router/router.go
Normal file
39
backend/go-service/router/router.go
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
// Package router 主路由汇总入口
|
||||||
|
// 不含任何具体路由定义,仅调用各模块的 RegisterRoutes 函数
|
||||||
|
// 新增模块时在 Setup 函数中添加一行调用即可
|
||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/echochat/backend/app/auth"
|
||||||
|
"github.com/echochat/backend/app/provider"
|
||||||
|
"github.com/echochat/backend/pkg/middleware"
|
||||||
|
"github.com/echochat/backend/pkg/utils"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Setup 注册所有路由
|
||||||
|
// 包含健康检查和各业务模块的路由注册
|
||||||
|
func Setup(engine *gin.Engine, app *provider.App) {
|
||||||
|
// 健康检查(不经过业务中间件)
|
||||||
|
engine.GET("/health", func(c *gin.Context) {
|
||||||
|
utils.ResponseOK(c, gin.H{
|
||||||
|
"status": "ok",
|
||||||
|
"service": "echochat",
|
||||||
|
"time": time.Now().Format("2006-01-02 15:04:05"),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// JWT 认证中间件实例
|
||||||
|
jwtAuth := middleware.JWTAuth(&app.Config.JWT)
|
||||||
|
|
||||||
|
// --- 各模块路由注册 ---
|
||||||
|
auth.RegisterRoutes(engine, app.AuthController, app.AdminAuthController, jwtAuth)
|
||||||
|
|
||||||
|
// [未来] im.RegisterRoutes(engine, app.ImController, jwtAuth)
|
||||||
|
// [未来] meeting.RegisterRoutes(engine, app.MeetingController, jwtAuth)
|
||||||
|
// [未来] contact.RegisterRoutes(engine, app.ContactController, jwtAuth)
|
||||||
|
// [未来] notify.RegisterRoutes(engine, app.NotifyController, jwtAuth)
|
||||||
|
// [未来] admin.RegisterRoutes(engine, app.AdminController, jwtAuth)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user