// Package auth 提供 im-service 长连接接入所需的最小 JWT 校验能力。 package auth import ( "fmt" "strconv" "time" jwt "github.com/golang-jwt/jwt/v5" ) // Claims 是 im-service 只关心的最小鉴权结果。 type Claims struct { // UserID 是消息投递、ACK 和 room-service guard 查询的用户身份。 UserID int64 // DeviceID 用于区分同一用户多端连接;当前版本不做复杂多端漫游。 DeviceID string } // HMACVerifier 负责校验和签发本地开发使用的 HS256 JWT。 type HMACVerifier struct { // secret 是 HS256 对称密钥;生产环境应来自服务配置或密钥系统。 secret []byte } // NewHMACVerifier 初始化 JWT 校验器。 func NewHMACVerifier(secret string) *HMACVerifier { return &HMACVerifier{ secret: []byte(secret), } } // Verify 校验 token,并提取 user_id 和 device_id。 func (v *HMACVerifier) Verify(tokenString string) (Claims, error) { // 只接受 HS256,防止客户端用其他 alg 绕过本服务的签名假设。 token, err := jwt.Parse(tokenString, func(token *jwt.Token) (any, error) { if token.Method.Alg() != jwt.SigningMethodHS256.Alg() { return nil, fmt.Errorf("unexpected signing method: %s", token.Method.Alg()) } return v.secret, nil }) if err != nil { return Claims{}, err } if !token.Valid { // jwt 库已经完成 exp 等标准校验;这里把无效 token 显式转成业务可读错误。 return Claims{}, fmt.Errorf("invalid token") } mapClaims, ok := token.Claims.(jwt.MapClaims) if !ok { // 当前服务约定使用 map claims,结构不匹配说明 token 不是本系统签发形态。 return Claims{}, fmt.Errorf("invalid claims") } userID, err := readInt64(mapClaims, "user_id") if err != nil { // user_id 是连接身份的硬依赖,缺失时不能创建匿名 session。 return Claims{}, err } return Claims{ UserID: userID, DeviceID: readString(mapClaims, "device_id"), }, nil } // Mint 只用于测试和本地联调,方便快速生成合法 token。 func (v *HMACVerifier) Mint(userID int64, deviceID string, ttl time.Duration) (string, error) { now := time.Now() // 字段名保持与 Verify 读取逻辑一致,测试 token 可以覆盖完整连接鉴权路径。 claims := jwt.MapClaims{ "user_id": userID, "device_id": deviceID, "iat": now.Unix(), "exp": now.Add(ttl).Unix(), } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) return token.SignedString(v.secret) } // readInt64 从 JWT map claims 中读取数值身份字段,兼容 JSON number、int64 和字符串形态。 func readInt64(values jwt.MapClaims, key string) (int64, error) { raw, exists := values[key] if !exists { return 0, fmt.Errorf("%s missing", key) } switch typed := raw.(type) { case float64: // jwt.MapClaims 经过 JSON 解码时数字默认是 float64,user_id 在这里收敛成 int64。 return int64(typed), nil case int64: return typed, nil case string: // 字符串兼容某些网关或测试工具把用户 ID 写成文本的情况。 return strconv.ParseInt(typed, 10, 64) default: return 0, fmt.Errorf("%s has unsupported type", key) } } // readString 读取可选字符串 claim;缺失或类型不匹配都按空字符串处理。 func readString(values jwt.MapClaims, key string) string { raw, exists := values[key] if !exists { return "" } if text, ok := raw.(string); ok { return text } return "" }