Files
EchoChat/backend/go-service/pkg/utils/jwt.go
bujinyuan c3e8600b24 fix: 前后台 Token Redis 存储隔离,修复同账号互相覆盖问题
核心变更:
- Redis key 加入 clientType 前缀:echo:auth:token:{frontend|admin}:{user_id}
- JWT Claims 新增 client_type 字段区分前台/管理端
- 新增 constants/client_type.go 定义 ClientTypeFrontend / ClientTypeAdmin
- 全链路传递 clientType:Controller → Service → TokenStore → Redis
- 中间件从 JWT Claims 提取 clientType 做 Redis 校验
- 登出只删除当前端 Token,不影响另一端

前端变更:
- 管理端 login API 改为调用 POST /api/v1/admin/auth/login(之前错误调用了前台接口)

文档更新:
- 系统设计文档、实施计划、API文档、开发规范、项目进度全部同步

Made-with: Cursor
2026-03-02 14:28:45 +08:00

90 lines
2.7 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 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"` // 用户角色代码列表
ClientType string `json:"client_type"` // 客户端类型frontend / admin
jwt.RegisteredClaims
}
var (
ErrTokenExpired = errors.New("token 已过期")
ErrTokenInvalid = errors.New("token 无效")
)
// GenerateToken 生成 Access Token
// 包含 UserID、Username、Roles、ClientType有效期由配置中的 access_expire_min 决定
func GenerateToken(cfg *config.JWTConfig, userID int64, username string, roles []string, clientType string) (string, error) {
expireTime := time.Now().Add(time.Duration(cfg.AccessExpireMin) * time.Minute)
claims := &Claims{
UserID: userID,
Username: username,
Roles: roles,
ClientType: clientType,
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 和 ClientType有效期由配置中的 refresh_expire_day 决定
func GenerateRefreshToken(cfg *config.JWTConfig, userID int64, clientType string) (string, error) {
expireTime := time.Now().Add(time.Duration(cfg.RefreshExpireDay) * 24 * time.Hour)
claims := &Claims{
UserID: userID,
ClientType: clientType,
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
}