123 lines
3.5 KiB
Go
123 lines
3.5 KiB
Go
package auth
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"strings"
|
||
|
||
jwt "github.com/golang-jwt/jwt/v5"
|
||
)
|
||
|
||
type contextKey string
|
||
|
||
const (
|
||
userContextKey contextKey = "gateway_user_id"
|
||
claimsContextKey contextKey = "gateway_claims"
|
||
)
|
||
|
||
// Claims 是 gateway 从 access token 中消费的最小用户状态快照。
|
||
// 它只用于入口鉴权和 profile gate,不替代 user-service 的用户主数据。
|
||
type Claims struct {
|
||
UserID int64
|
||
SessionID string
|
||
ProfileCompleted bool
|
||
OnboardingStatus string
|
||
}
|
||
|
||
// 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"].(float64)
|
||
if !ok {
|
||
return Claims{}, fmt.Errorf("missing user_id")
|
||
}
|
||
|
||
profileCompleted, _ := claims["profile_completed"].(bool)
|
||
onboardingStatus, _ := claims["onboarding_status"].(string)
|
||
sessionID, _ := claims["sid"].(string)
|
||
|
||
return Claims{
|
||
UserID: int64(rawUserID),
|
||
SessionID: strings.TrimSpace(sessionID),
|
||
ProfileCompleted: profileCompleted,
|
||
OnboardingStatus: strings.TrimSpace(onboardingStatus),
|
||
}, nil
|
||
}
|
||
|
||
// 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 = 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
|
||
}
|