Files
lijiaoqiao/gateway/internal/middleware/remote_runtime.go
2026-04-11 09:25:31 +08:00

118 lines
3.1 KiB
Go

package middleware
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"strings"
"sync"
"time"
)
type RemoteTokenRuntime struct {
baseURL string
httpClient *http.Client
now func() time.Time
mu sync.RWMutex
records map[string]remoteResolvedToken
}
type remoteResolvedToken struct {
status TokenStatus
expiresAt time.Time
}
type remoteIntrospectResponse struct {
Data struct {
TokenID string `json:"token_id"`
SubjectID string `json:"subject_id"`
Role string `json:"role"`
Status string `json:"status"`
Scope []string `json:"scope"`
IssuedAt time.Time `json:"issued_at"`
ExpiresAt time.Time `json:"expires_at"`
} `json:"data"`
}
func NewRemoteTokenRuntime(baseURL string, httpClient *http.Client, now func() time.Time) *RemoteTokenRuntime {
if httpClient == nil {
httpClient = http.DefaultClient
}
if now == nil {
now = time.Now
}
return &RemoteTokenRuntime{
baseURL: strings.TrimRight(baseURL, "/"),
httpClient: httpClient,
now: now,
records: make(map[string]remoteResolvedToken),
}
}
func (r *RemoteTokenRuntime) Verify(ctx context.Context, rawToken string) (VerifiedToken, error) {
payload := map[string]string{"token": rawToken}
body, err := json.Marshal(payload)
if err != nil {
return VerifiedToken{}, err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, r.baseURL+"/api/v1/platform/tokens/introspect", bytes.NewReader(body))
if err != nil {
return VerifiedToken{}, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Request-Id", fmt.Sprintf("gateway-introspect-%d", r.now().UnixNano()))
resp, err := r.httpClient.Do(req)
if err != nil {
return VerifiedToken{}, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return VerifiedToken{}, fmt.Errorf("token introspection failed with status %d", resp.StatusCode)
}
var result remoteIntrospectResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return VerifiedToken{}, err
}
if strings.TrimSpace(result.Data.TokenID) == "" {
return VerifiedToken{}, errors.New("token introspection response missing token_id")
}
status := TokenStatus(strings.ToLower(strings.TrimSpace(result.Data.Status)))
r.mu.Lock()
r.records[result.Data.TokenID] = remoteResolvedToken{
status: status,
expiresAt: result.Data.ExpiresAt,
}
r.mu.Unlock()
return VerifiedToken{
TokenID: result.Data.TokenID,
SubjectID: result.Data.SubjectID,
Role: result.Data.Role,
Scope: append([]string(nil), result.Data.Scope...),
IssuedAt: result.Data.IssuedAt,
ExpiresAt: result.Data.ExpiresAt,
}, nil
}
func (r *RemoteTokenRuntime) Resolve(ctx context.Context, tokenID string) (TokenStatus, error) {
r.mu.RLock()
record, ok := r.records[tokenID]
r.mu.RUnlock()
if !ok {
return "", errors.New("token status not cached; verify must run before resolve")
}
if !record.expiresAt.IsZero() && !r.now().Before(record.expiresAt) && record.status == TokenStatusActive {
return TokenStatusExpired, nil
}
return record.status, nil
}