120 lines
3.4 KiB
Go
120 lines
3.4 KiB
Go
// 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 ""
|
||
}
|