fix(auth): 有状态 JWT + 统一响应格式,严格遵循设计方案
1. 有状态 JWT(Token 存 Redis):
- 新增 token_store.go: Token 的 Redis 存取管理
- echo:auth:token:{user_id} → Access Token (TTL = access_expire_min)
- echo:auth:refresh:{user_id} → Refresh Token (TTL = refresh_expire_day)
- 登录/注册时自动将 Token 存入 Redis(覆盖旧 Token,实现单设备登录)
- JWT 中间件增加 Redis 有效性校验(TokenValidator 接口解耦)
- 登出时从 Redis 删除 Token,使其立即失效
- 刷新 Token 时校验 Redis 中的 Refresh Token
2. 统一成功响应为 "success" + 200:
- 注册接口改用 ResponseOK(原 ResponseCreated/201)
- 所有成功响应统一为 {"code": 0, "message": "success"}
3. API 文档同步更新:
- frontend/auth.md: 登出说明改为 Redis 方案、注册响应统一
- README.md: 移除 "created" 示例
已验证:登出后 Token 立即失效 ✓ 重新登录后新 Token 有效 ✓
Made-with: Cursor
This commit is contained in:
@@ -46,7 +46,7 @@ func (ctrl *AuthController) Register(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
utils.ResponseCreated(c, resp)
|
utils.ResponseOK(c, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Login 用户登录
|
// Login 用户登录
|
||||||
@@ -82,11 +82,19 @@ func (ctrl *AuthController) Logout(c *gin.Context) {
|
|||||||
funcName := "controller.auth_controller.Logout"
|
funcName := "controller.auth_controller.Logout"
|
||||||
ctx := c.Request.Context()
|
ctx := c.Request.Context()
|
||||||
|
|
||||||
userID, _ := middleware.GetCurrentUserID(c)
|
userID, ok := middleware.GetCurrentUserID(c)
|
||||||
|
if !ok {
|
||||||
|
utils.ResponseUnauthorized(c, "无法获取用户信息")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
logs.Info(ctx, funcName, "用户登出", zap.Int64("user_id", userID))
|
logs.Info(ctx, funcName, "用户登出", zap.Int64("user_id", userID))
|
||||||
|
|
||||||
// 当前为无状态 JWT,登出由客户端删除 Token 实现
|
if err := ctrl.authService.Logout(ctx, userID); err != nil {
|
||||||
// 后续可扩展:将 Token 加入 Redis 黑名单
|
utils.ResponseError(c, "登出失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
utils.ResponseOK(c, nil)
|
utils.ResponseOK(c, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
var AuthSet = wire.NewSet(
|
var AuthSet = wire.NewSet(
|
||||||
dao.NewUserDAO,
|
dao.NewUserDAO,
|
||||||
dao.NewRoleDAO,
|
dao.NewRoleDAO,
|
||||||
|
service.NewTokenStore,
|
||||||
service.NewAuthService,
|
service.NewAuthService,
|
||||||
controller.NewAuthController,
|
controller.NewAuthController,
|
||||||
controller.NewAdminAuthController,
|
controller.NewAdminAuthController,
|
||||||
|
|||||||
@@ -28,17 +28,19 @@ var (
|
|||||||
|
|
||||||
// AuthService 认证服务,处理注册、登录、Token 管理、个人信息等业务逻辑
|
// AuthService 认证服务,处理注册、登录、Token 管理、个人信息等业务逻辑
|
||||||
type AuthService struct {
|
type AuthService struct {
|
||||||
userDAO *dao.UserDAO
|
userDAO *dao.UserDAO
|
||||||
roleDAO *dao.RoleDAO
|
roleDAO *dao.RoleDAO
|
||||||
jwtCfg *config.JWTConfig
|
jwtCfg *config.JWTConfig
|
||||||
|
tokenStore *TokenStore
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewAuthService 创建认证服务实例
|
// NewAuthService 创建认证服务实例
|
||||||
func NewAuthService(userDAO *dao.UserDAO, roleDAO *dao.RoleDAO, jwtCfg *config.JWTConfig) *AuthService {
|
func NewAuthService(userDAO *dao.UserDAO, roleDAO *dao.RoleDAO, jwtCfg *config.JWTConfig, tokenStore *TokenStore) *AuthService {
|
||||||
return &AuthService{
|
return &AuthService{
|
||||||
userDAO: userDAO,
|
userDAO: userDAO,
|
||||||
roleDAO: roleDAO,
|
roleDAO: roleDAO,
|
||||||
jwtCfg: jwtCfg,
|
jwtCfg: jwtCfg,
|
||||||
|
tokenStore: tokenStore,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,7 +112,7 @@ func (s *AuthService) Register(ctx context.Context, req *dto.RegisterRequest) (*
|
|||||||
|
|
||||||
// 获取角色列表并生成 Token
|
// 获取角色列表并生成 Token
|
||||||
roles, _ := s.roleDAO.GetUserRoleCodes(ctx, user.ID)
|
roles, _ := s.roleDAO.GetUserRoleCodes(ctx, user.ID)
|
||||||
resp, err := s.buildLoginResponse(user, roles)
|
resp, err := s.buildLoginResponse(ctx, user, roles)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -162,9 +164,9 @@ func (s *AuthService) Login(ctx context.Context, req *dto.LoginRequest, clientIP
|
|||||||
// 更新最后登录信息
|
// 更新最后登录信息
|
||||||
_ = s.userDAO.UpdateLastLogin(ctx, user.ID, clientIP)
|
_ = s.userDAO.UpdateLastLogin(ctx, user.ID, clientIP)
|
||||||
|
|
||||||
// 获取角色列表并生成 Token
|
// 获取角色列表并生成 Token(同时存入 Redis)
|
||||||
roles, _ := s.roleDAO.GetUserRoleCodes(ctx, user.ID)
|
roles, _ := s.roleDAO.GetUserRoleCodes(ctx, user.ID)
|
||||||
resp, err := s.buildLoginResponse(user, roles)
|
resp, err := s.buildLoginResponse(ctx, user, roles)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -227,6 +229,14 @@ func (s *AuthService) RefreshToken(ctx context.Context, refreshToken string) (*d
|
|||||||
return nil, ErrRefreshTokenType
|
return nil, ErrRefreshTokenType
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 验证 Refresh Token 是否与 Redis 中存储的一致
|
||||||
|
if !s.tokenStore.ValidateRefreshToken(ctx, claims.UserID, refreshToken) {
|
||||||
|
logs.Warn(ctx, funcName, "Refresh Token 已失效(不在 Redis 中)",
|
||||||
|
zap.Int64("user_id", claims.UserID),
|
||||||
|
)
|
||||||
|
return nil, ErrRefreshTokenType
|
||||||
|
}
|
||||||
|
|
||||||
// 查找用户(确保用户仍然有效)
|
// 查找用户(确保用户仍然有效)
|
||||||
user, err := s.userDAO.FindByID(ctx, claims.UserID)
|
user, err := s.userDAO.FindByID(ctx, claims.UserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -240,9 +250,9 @@ func (s *AuthService) RefreshToken(ctx context.Context, refreshToken string) (*d
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取角色并生成新 Token
|
// 获取角色并生成新 Token(同时存入 Redis 覆盖旧 Token)
|
||||||
roles, _ := s.roleDAO.GetUserRoleCodes(ctx, user.ID)
|
roles, _ := s.roleDAO.GetUserRoleCodes(ctx, user.ID)
|
||||||
resp, err := s.buildLoginResponse(user, roles)
|
resp, err := s.buildLoginResponse(ctx, user, roles)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -251,6 +261,20 @@ func (s *AuthService) RefreshToken(ctx context.Context, refreshToken string) (*d
|
|||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Logout 用户登出
|
||||||
|
// 从 Redis 中删除该用户的 Access Token 和 Refresh Token
|
||||||
|
func (s *AuthService) Logout(ctx context.Context, userID int64) error {
|
||||||
|
funcName := "service.auth_service.Logout"
|
||||||
|
logs.Info(ctx, funcName, "用户登出", zap.Int64("user_id", userID))
|
||||||
|
return s.tokenStore.RemoveTokens(ctx, userID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateAccessToken 校验 Access Token 是否在 Redis 中有效
|
||||||
|
// 供 JWT 中间件调用,实现有状态 JWT 验证
|
||||||
|
func (s *AuthService) ValidateAccessToken(ctx context.Context, userID int64, token string) bool {
|
||||||
|
return s.tokenStore.ValidateAccessToken(ctx, userID, token)
|
||||||
|
}
|
||||||
|
|
||||||
// GetProfile 获取用户个人信息
|
// GetProfile 获取用户个人信息
|
||||||
func (s *AuthService) GetProfile(ctx context.Context, userID int64) (*dto.UserInfo, error) {
|
func (s *AuthService) GetProfile(ctx context.Context, userID int64) (*dto.UserInfo, error) {
|
||||||
funcName := "service.auth_service.GetProfile"
|
funcName := "service.auth_service.GetProfile"
|
||||||
@@ -349,8 +373,8 @@ func (s *AuthService) checkUserStatus(status int) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildLoginResponse 构建登录响应(生成 Token + 用户信息)
|
// buildLoginResponse 构建登录响应(生成 Token + 存入 Redis + 用户信息)
|
||||||
func (s *AuthService) buildLoginResponse(user *model.User, roles []string) (*dto.LoginResponse, error) {
|
func (s *AuthService) buildLoginResponse(ctx context.Context, user *model.User, roles []string) (*dto.LoginResponse, error) {
|
||||||
if roles == nil {
|
if roles == nil {
|
||||||
roles = []string{}
|
roles = []string{}
|
||||||
}
|
}
|
||||||
@@ -365,6 +389,11 @@ func (s *AuthService) buildLoginResponse(user *model.User, roles []string) (*dto
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 将 Token 存入 Redis(有状态 JWT,支持主动失效和单设备登录)
|
||||||
|
if err = s.tokenStore.SaveTokens(ctx, user.ID, token, refreshToken); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
return &dto.LoginResponse{
|
return &dto.LoginResponse{
|
||||||
Token: token,
|
Token: token,
|
||||||
RefreshToken: refreshToken,
|
RefreshToken: refreshToken,
|
||||||
|
|||||||
119
backend/go-service/app/auth/service/token_store.go
Normal file
119
backend/go-service/app/auth/service/token_store.go
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/echochat/backend/config"
|
||||||
|
"github.com/echochat/backend/pkg/logs"
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Redis Key 前缀,遵循设计方案中 echo:auth:* 的命名规范
|
||||||
|
const (
|
||||||
|
keyPrefixAccessToken = "echo:auth:token:" // echo:auth:token:{user_id}
|
||||||
|
keyPrefixRefreshToken = "echo:auth:refresh:" // echo:auth:refresh:{user_id}
|
||||||
|
)
|
||||||
|
|
||||||
|
// TokenStore 管理 Token 在 Redis 中的存取
|
||||||
|
// 实现有状态 JWT:登录存入、验证时校验、登出时删除
|
||||||
|
type TokenStore struct {
|
||||||
|
redis *redis.Client
|
||||||
|
jwtCfg *config.JWTConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTokenStore 创建 TokenStore 实例
|
||||||
|
func NewTokenStore(redisClient *redis.Client, jwtCfg *config.JWTConfig) *TokenStore {
|
||||||
|
return &TokenStore{
|
||||||
|
redis: redisClient,
|
||||||
|
jwtCfg: jwtCfg,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveTokens 将 Access Token 和 Refresh Token 存入 Redis
|
||||||
|
// 每次登录/注册时调用,覆盖旧 Token(实现单设备登录)
|
||||||
|
func (s *TokenStore) SaveTokens(ctx context.Context, userID int64, accessToken, refreshToken string) error {
|
||||||
|
funcName := "service.token_store.SaveTokens"
|
||||||
|
|
||||||
|
accessKey := fmt.Sprintf("%s%d", keyPrefixAccessToken, userID)
|
||||||
|
refreshKey := fmt.Sprintf("%s%d", keyPrefixRefreshToken, userID)
|
||||||
|
|
||||||
|
accessTTL := time.Duration(s.jwtCfg.AccessExpireMin) * time.Minute
|
||||||
|
refreshTTL := time.Duration(s.jwtCfg.RefreshExpireDay) * 24 * time.Hour
|
||||||
|
|
||||||
|
pipe := s.redis.Pipeline()
|
||||||
|
pipe.Set(ctx, accessKey, accessToken, accessTTL)
|
||||||
|
pipe.Set(ctx, refreshKey, refreshToken, refreshTTL)
|
||||||
|
|
||||||
|
if _, err := pipe.Exec(ctx); err != nil {
|
||||||
|
logs.Error(ctx, funcName, "保存 Token 到 Redis 失败",
|
||||||
|
zap.Int64("user_id", userID),
|
||||||
|
zap.Error(err),
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
logs.Debug(ctx, funcName, "Token 已存入 Redis",
|
||||||
|
zap.Int64("user_id", userID),
|
||||||
|
zap.Duration("access_ttl", accessTTL),
|
||||||
|
zap.Duration("refresh_ttl", refreshTTL),
|
||||||
|
)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateAccessToken 校验 Access Token 是否与 Redis 中存储的一致
|
||||||
|
// 返回 true 表示有效,false 表示已被登出/覆盖
|
||||||
|
func (s *TokenStore) ValidateAccessToken(ctx context.Context, userID int64, token string) bool {
|
||||||
|
funcName := "service.token_store.ValidateAccessToken"
|
||||||
|
|
||||||
|
key := fmt.Sprintf("%s%d", keyPrefixAccessToken, userID)
|
||||||
|
stored, err := s.redis.Get(ctx, key).Result()
|
||||||
|
if err == redis.Nil {
|
||||||
|
logs.Debug(ctx, funcName, "Token 不存在(已登出或过期)",
|
||||||
|
zap.Int64("user_id", userID),
|
||||||
|
)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
logs.Error(ctx, funcName, "Redis 查询 Token 失败",
|
||||||
|
zap.Int64("user_id", userID),
|
||||||
|
zap.Error(err),
|
||||||
|
)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return stored == token
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateRefreshToken 校验 Refresh Token 是否与 Redis 中存储的一致
|
||||||
|
func (s *TokenStore) ValidateRefreshToken(ctx context.Context, userID int64, token string) bool {
|
||||||
|
key := fmt.Sprintf("%s%d", keyPrefixRefreshToken, userID)
|
||||||
|
stored, err := s.redis.Get(ctx, key).Result()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return stored == token
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveTokens 从 Redis 删除用户的所有 Token(登出时调用)
|
||||||
|
func (s *TokenStore) RemoveTokens(ctx context.Context, userID int64) error {
|
||||||
|
funcName := "service.token_store.RemoveTokens"
|
||||||
|
|
||||||
|
accessKey := fmt.Sprintf("%s%d", keyPrefixAccessToken, userID)
|
||||||
|
refreshKey := fmt.Sprintf("%s%d", keyPrefixRefreshToken, userID)
|
||||||
|
|
||||||
|
if err := s.redis.Del(ctx, accessKey, refreshKey).Err(); err != nil {
|
||||||
|
logs.Error(ctx, funcName, "删除 Token 失败",
|
||||||
|
zap.Int64("user_id", userID),
|
||||||
|
zap.Error(err),
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
logs.Info(ctx, funcName, "Token 已从 Redis 删除",
|
||||||
|
zap.Int64("user_id", userID),
|
||||||
|
)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -31,7 +31,8 @@ func InitializeApp(cfg *config.Config) (*App, error) {
|
|||||||
userDAO := dao.NewUserDAO(gormDB)
|
userDAO := dao.NewUserDAO(gormDB)
|
||||||
roleDAO := dao.NewRoleDAO(gormDB)
|
roleDAO := dao.NewRoleDAO(gormDB)
|
||||||
jwtConfig := provideJWTConfig(cfg)
|
jwtConfig := provideJWTConfig(cfg)
|
||||||
authService := service.NewAuthService(userDAO, roleDAO, jwtConfig)
|
tokenStore := service.NewTokenStore(client, jwtConfig)
|
||||||
|
authService := service.NewAuthService(userDAO, roleDAO, jwtConfig, tokenStore)
|
||||||
authController := controller.NewAuthController(authService)
|
authController := controller.NewAuthController(authService)
|
||||||
adminAuthController := controller.NewAdminAuthController(authService)
|
adminAuthController := controller.NewAdminAuthController(authService)
|
||||||
app := NewApp(cfg, gormDB, client, authService, authController, adminAuthController)
|
app := NewApp(cfg, gormDB, client, authService, authController, adminAuthController)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package middleware
|
package middleware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/echochat/backend/config"
|
"github.com/echochat/backend/config"
|
||||||
@@ -16,9 +17,15 @@ const (
|
|||||||
ContextKeyRoles = "roles" // Context 中存储当前用户角色列表的 Key
|
ContextKeyRoles = "roles" // Context 中存储当前用户角色列表的 Key
|
||||||
)
|
)
|
||||||
|
|
||||||
// JWTAuth JWT 认证中间件
|
// TokenValidator Token 有效性校验接口
|
||||||
// 从 Authorization Header 中提取 Bearer Token,验证后将用户信息注入 Gin Context
|
// 由 AuthService 实现,中间件通过此接口检查 Token 是否在 Redis 中有效
|
||||||
func JWTAuth(jwtCfg *config.JWTConfig) gin.HandlerFunc {
|
type TokenValidator interface {
|
||||||
|
ValidateAccessToken(ctx context.Context, userID int64, token string) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// JWTAuth JWT 认证中间件(有状态 JWT)
|
||||||
|
// 验证流程:解析 Token → 检查类型 → 校验 Redis 有效性 → 注入用户信息
|
||||||
|
func JWTAuth(jwtCfg *config.JWTConfig, validator TokenValidator) gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
funcName := "middleware.JWTAuth"
|
funcName := "middleware.JWTAuth"
|
||||||
ctx := c.Request.Context()
|
ctx := c.Request.Context()
|
||||||
@@ -30,7 +37,6 @@ func JWTAuth(jwtCfg *config.JWTConfig) gin.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 提取 Bearer Token
|
|
||||||
parts := strings.SplitN(authHeader, " ", 2)
|
parts := strings.SplitN(authHeader, " ", 2)
|
||||||
if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" {
|
if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" {
|
||||||
utils.ResponseUnauthorized(c, "认证格式错误,应为 Bearer {token}")
|
utils.ResponseUnauthorized(c, "认证格式错误,应为 Bearer {token}")
|
||||||
@@ -50,14 +56,23 @@ func JWTAuth(jwtCfg *config.JWTConfig) gin.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证 Token 类型(只允许 access token 访问接口)
|
|
||||||
if claims.Subject != "access" {
|
if claims.Subject != "access" {
|
||||||
utils.ResponseUnauthorized(c, "无效的 Token 类型")
|
utils.ResponseUnauthorized(c, "无效的 Token 类型")
|
||||||
c.Abort()
|
c.Abort()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 将用户信息注入 Gin Context,供后续 Controller/Service 使用
|
// 校验 Token 是否在 Redis 中有效(有状态 JWT 核心逻辑)
|
||||||
|
if !validator.ValidateAccessToken(ctx, claims.UserID, tokenStr) {
|
||||||
|
logs.Warn(ctx, funcName, "Token 已失效(已登出或被覆盖)",
|
||||||
|
zap.Int64("user_id", claims.UserID),
|
||||||
|
zap.String("ip", c.ClientIP()),
|
||||||
|
)
|
||||||
|
utils.ResponseUnauthorized(c, "认证已失效,请重新登录")
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
c.Set(ContextKeyUserID, claims.UserID)
|
c.Set(ContextKeyUserID, claims.UserID)
|
||||||
c.Set(ContextKeyUsername, claims.Username)
|
c.Set(ContextKeyUsername, claims.Username)
|
||||||
c.Set(ContextKeyRoles, claims.Roles)
|
c.Set(ContextKeyRoles, claims.Roles)
|
||||||
@@ -108,7 +123,6 @@ func RequireRole(roles ...string) gin.HandlerFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetCurrentUserID 从 Gin Context 获取当前登录用户 ID
|
// GetCurrentUserID 从 Gin Context 获取当前登录用户 ID
|
||||||
// 供 Controller 层调用的便捷方法
|
|
||||||
func GetCurrentUserID(c *gin.Context) (int64, bool) {
|
func GetCurrentUserID(c *gin.Context) (int64, bool) {
|
||||||
val, exists := c.Get(ContextKeyUserID)
|
val, exists := c.Get(ContextKeyUserID)
|
||||||
if !exists {
|
if !exists {
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ func Setup(engine *gin.Engine, app *provider.App) {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// JWT 认证中间件实例
|
// JWT 认证中间件实例(有状态 JWT,通过 AuthService 校验 Redis)
|
||||||
jwtAuth := middleware.JWTAuth(&app.Config.JWT)
|
jwtAuth := middleware.JWTAuth(&app.Config.JWT, app.AuthService)
|
||||||
|
|
||||||
// --- 各模块路由注册 ---
|
// --- 各模块路由注册 ---
|
||||||
auth.RegisterRoutes(engine, app.AuthController, app.AdminAuthController, jwtAuth)
|
auth.RegisterRoutes(engine, app.AuthController, app.AdminAuthController, jwtAuth)
|
||||||
|
|||||||
@@ -80,17 +80,6 @@ yyyy-MM-dd HH:mm:ss
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**创建成功响应(如注册):**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"code": 0,
|
|
||||||
"message": "created",
|
|
||||||
"data": { ... },
|
|
||||||
"trace_id": "6478824e-2926-4d35-aa5f-047c8cfbb36b",
|
|
||||||
"time": "2026-02-27 18:00:00"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**错误响应:**
|
**错误响应:**
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -43,11 +43,11 @@
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**成功响应(201 Created):**
|
**成功响应(200 OK):**
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"code": 0,
|
"code": 0,
|
||||||
"message": "created",
|
"message": "success",
|
||||||
"data": {
|
"data": {
|
||||||
"token": "eyJhbGciOiJIUzI1NiIs...",
|
"token": "eyJhbGciOiJIUzI1NiIs...",
|
||||||
"refresh_token": "eyJhbGciOiJIUzI1NiIs...",
|
"refresh_token": "eyJhbGciOiJIUzI1NiIs...",
|
||||||
@@ -139,7 +139,7 @@
|
|||||||
|
|
||||||
**权限:** 需认证
|
**权限:** 需认证
|
||||||
|
|
||||||
**说明:** 当前采用无状态 JWT 方案,服务端不存储 Token 状态。退出登录由客户端主动删除本地存储的 Token 即可。后续可扩展为将 Token 加入 Redis 黑名单实现服务端主动失效。
|
**说明:** 采用有状态 JWT 方案,Token 存储在 Redis 中(`echo:auth:token:{user_id}` 和 `echo:auth:refresh:{user_id}`)。登出时服务端会从 Redis 中删除该用户的 Access Token 和 Refresh Token,使其立即失效。客户端也应同步清除本地存储的 Token。
|
||||||
|
|
||||||
**成功响应:**
|
**成功响应:**
|
||||||
```json
|
```json
|
||||||
|
|||||||
Reference in New Issue
Block a user