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:
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
|
||||
}
|
||||
Reference in New Issue
Block a user