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 }