2026-04-25 01:18:30 +08:00

120 lines
3.4 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 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 解码时数字默认是 float64user_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 ""
}