Files
EchoChat/backend/go-service/app/auth/service/token_store.go
bujinyuan b607434c57 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
2026-02-28 16:57:46 +08:00

120 lines
3.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}