package server import ( "context" "encoding/json" "errors" "fmt" "net/http" "path" "strings" "time" "git.apinb.com/bsm-sdk/core/errcode" "github.com/golang-jwt/jwt/v5" "google.golang.org/grpc" "google.golang.org/grpc/metadata" ) type authorization struct { key []byte expire time.Duration anonymous map[string]struct{} } func newAuthorization(key string, expireSeconds int64, anonymous []string) (*authorization, error) { key = strings.TrimSpace(key) if key == "" { return nil, errcode.ErrTokenSecretKeyNotFound } if expireSeconds <= 0 { return nil, errcode.ErrTokenAuthExpire } allowed := make(map[string]struct{}, len(anonymous)) for _, item := range anonymous { if item = normalizePath(item); item != "" { allowed[item] = struct{}{} } } return &authorization{key: []byte(key), expire: time.Duration(expireSeconds) * time.Second, anonymous: allowed}, nil } func (a *authorization) unaryInterceptor(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { if !a.isAnonymous(info.FullMethod) { values := metadata.ValueFromIncomingContext(ctx, "authorization") if len(values) == 0 { return nil, errcode.ErrHeaderAuthorization } if err := a.validate(values[0]); err != nil { return nil, err } } return handler(ctx, req) } func (a *authorization) httpMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { requestPath := canonicalHTTPPath(r.URL.Path) if !a.isAnonymous(requestPath) { if err := a.validate(r.Header.Get("Authorization")); err != nil { writeHTTPAuthorizationError(w, err) return } } next.ServeHTTP(w, r) }) } func (a *authorization) validate(raw string) error { raw = strings.TrimSpace(raw) if raw == "" { return errcode.ErrHeaderAuthorization } claims := &jwt.RegisteredClaims{} tokenValue, err := jwt.ParseWithClaims(raw, claims, func(tokenValue *jwt.Token) (any, error) { if tokenValue.Method != jwt.SigningMethodHS256 { return nil, fmt.Errorf("unexpected signing method: %s", tokenValue.Method.Alg()) } return a.key, nil }, jwt.WithExpirationRequired(), jwt.WithIssuedAt(), jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Alg()})) if err != nil || !tokenValue.Valid { if errors.Is(err, jwt.ErrTokenExpired) { return errcode.ErrTokenAuthExpire } return errcode.ErrTokenAuthParseFail } if claims.IssuedAt == nil { return errcode.ErrTokenDataInvalid } now := time.Now() if claims.IssuedAt.Time.After(now) || now.Sub(claims.IssuedAt.Time) > a.expire { return errcode.ErrTokenAuthExpire } return nil } func (a *authorization) isAnonymous(requestPath string) bool { requestPath = normalizePath(requestPath) if strings.HasPrefix(requestPath, "/grpc.reflection.") || strings.HasPrefix(requestPath, "/grpc.health.") { return true } _, ok := a.anonymous[requestPath] return ok } func canonicalHTTPPath(requestPath string) string { normalized := normalizePath(requestPath) parts := strings.Split(strings.TrimPrefix(normalized, "/"), "/") if len(parts) == 4 && parts[0] == "rpc" { return "/" + parts[1] + "." + parts[2] + "/" + parts[3] } return normalized } func normalizePath(value string) string { value = strings.TrimSpace(value) if value == "" { return "" } return path.Clean("/" + strings.TrimPrefix(value, "/")) } func writeHTTPAuthorizationError(w http.ResponseWriter, err error) { w.Header().Set("Content-Type", "application/json; charset=utf-8") w.WriteHeader(http.StatusOK) _ = json.NewEncoder(w).Encode(newErrorResponse(err)) }