2026-06-11 01:02:16 +08:00

170 lines
4.8 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
import (
"context"
"fmt"
"strconv"
"strings"
jwt "github.com/golang-jwt/jwt/v5"
"hyapp/pkg/appcode"
)
type contextKey string
const (
userContextKey contextKey = "gateway_user_id"
claimsContextKey contextKey = "gateway_claims"
)
// Claims 是 gateway 从 access token 中消费的最小用户状态快照。
// 它只用于入口鉴权和 profile gate不替代 user-service 的用户主数据。
type Claims struct {
AppCode string
UserID int64
SessionID string
ProfileCompleted bool
OnboardingStatus string
AccessTokenExpiresAtMS int64
}
// Verifier 负责在 gateway 入口层校验用户 JWT。
type Verifier struct {
secret []byte
}
// NewVerifier 初始化入口 JWT 校验器。
func NewVerifier(secret string) *Verifier {
return &Verifier{secret: []byte(secret)}
}
// VerifyUserID 只负责从 Authorization header 中提取并校验用户身份。
// HTTP 状态码和响应 envelope 属于 transport 层auth 包不直接写 ResponseWriter。
func (v *Verifier) VerifyUserID(header string) (int64, error) {
claims, err := v.Verify(header)
if err != nil {
return 0, err
}
return claims.UserID, nil
}
// Verify 校验 access token 并返回 gateway 入口需要的用户状态快照。
func (v *Verifier) Verify(header string) (Claims, error) {
if !strings.HasPrefix(header, "Bearer ") {
return Claims{}, fmt.Errorf("missing bearer token")
}
tokenString := strings.TrimPrefix(header, "Bearer ")
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")
}
return v.secret, nil
})
if err != nil || !token.Valid {
return Claims{}, fmt.Errorf("invalid token")
}
claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
return Claims{}, fmt.Errorf("invalid claims")
}
rawUserID, ok := claims["user_id"].(string)
if !ok || strings.TrimSpace(rawUserID) == "" {
return Claims{}, fmt.Errorf("missing user_id")
}
userID, err := strconv.ParseInt(strings.TrimSpace(rawUserID), 10, 64)
if err != nil || userID <= 0 {
return Claims{}, fmt.Errorf("invalid user_id")
}
profileCompleted, _ := claims["profile_completed"].(bool)
onboardingStatus, _ := claims["onboarding_status"].(string)
sessionID, _ := claims["sid"].(string)
appCode, _ := claims["app_code"].(string)
expiresAtMS := claimUnixSecondsMS(claims["exp"])
return Claims{
AppCode: appcode.Normalize(appCode),
UserID: userID,
SessionID: strings.TrimSpace(sessionID),
ProfileCompleted: profileCompleted,
OnboardingStatus: strings.TrimSpace(onboardingStatus),
AccessTokenExpiresAtMS: expiresAtMS,
}, nil
}
func claimUnixSecondsMS(value any) int64 {
switch typed := value.(type) {
case float64:
if typed <= 0 {
return 0
}
return int64(typed) * 1000
case int64:
if typed <= 0 {
return 0
}
return typed * 1000
case int:
if typed <= 0 {
return 0
}
return int64(typed) * 1000
case string:
parsed, err := strconv.ParseInt(strings.TrimSpace(typed), 10, 64)
if err != nil || parsed <= 0 {
return 0
}
return parsed * 1000
default:
return 0
}
}
// WithUserID 把已鉴权用户写入请求上下文,供 gateway 组装 RequestMeta。
func WithUserID(ctx context.Context, userID int64) context.Context {
return context.WithValue(ctx, userContextKey, userID)
}
// WithClaims 把 access token 中的准入快照写入上下文。
func WithClaims(ctx context.Context, claims Claims) context.Context {
ctx = appcode.WithContext(ctx, claims.AppCode)
ctx = WithUserID(ctx, claims.UserID)
return context.WithValue(ctx, claimsContextKey, claims)
}
// UserIDFromContext 提取鉴权后的用户 ID。
func UserIDFromContext(ctx context.Context) int64 {
userID, _ := ctx.Value(userContextKey).(int64)
return userID
}
// SessionIDFromContext 提取 access token 中的服务端 session_id。
func SessionIDFromContext(ctx context.Context) string {
claims, _ := ctx.Value(claimsContextKey).(Claims)
return strings.TrimSpace(claims.SessionID)
}
// ProfileCompletedFromContext 返回 gateway profile gate 的判断依据。
func ProfileCompletedFromContext(ctx context.Context) bool {
claims, _ := ctx.Value(claimsContextKey).(Claims)
return claims.ProfileCompleted
}
// OnboardingStatusFromContext 暴露 token 中的注册状态,便于后续审计或灰度判断。
func OnboardingStatusFromContext(ctx context.Context) string {
claims, _ := ctx.Value(claimsContextKey).(Claims)
return claims.OnboardingStatus
}
// AccessTokenExpiresAtMSFromContext 返回当前 access token 的过期时间;旧测试 token 没有 exp 时返回 0。
func AccessTokenExpiresAtMSFromContext(ctx context.Context) int64 {
claims, _ := ctx.Value(claimsContextKey).(Claims)
return claims.AccessTokenExpiresAtMS
}