AI Gateway Go 0.10.0 源码快照 + 旗舰版需求规划报告

M0-M7 已完成:核心网关(身份/RBAC/TOTP/OIDC/SAML/Provider/配额/路由/内容策略/审计/定价)+ 资源市场(MCP/Skills/数字员工)。
含 22 个 PostgreSQL 迁移、管理端/门户端前端源码、OpenAPI 契约、部署 compose。

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
ben
2026-08-12 11:45:54 +08:00
commit 5759c1862e
807 changed files with 114727 additions and 0 deletions
+211
View File
@@ -0,0 +1,211 @@
package identity
import (
"context"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"strings"
"time"
"github.com/redis/go-redis/v9"
)
var ErrInvalidSession = errors.New("invalid or expired session")
type Principal struct {
Kind Kind `json:"kind"`
SubjectID string `json:"subject_id"`
Login string `json:"login"`
DisplayName string `json:"display_name"`
Role string `json:"role,omitempty"`
Purpose string `json:"purpose"`
IssuedAt int64 `json:"issued_at"`
}
type SessionStore struct {
client *redis.Client
ttl time.Duration
}
func NewSessionStore(client *redis.Client, ttl time.Duration) *SessionStore {
return &SessionStore{client: client, ttl: ttl}
}
func (s *SessionStore) Create(ctx context.Context, principal Principal) (string, error) {
principal.Purpose = "session"
return s.create(ctx, principal, s.ttl)
}
func (s *SessionStore) CreatePending(ctx context.Context, principal Principal, ttl time.Duration) (string, error) {
principal.Purpose = "totp_pending"
return s.create(ctx, principal, ttl)
}
func (s *SessionStore) create(ctx context.Context, principal Principal, ttl time.Duration) (string, error) {
if s.client == nil {
return "", ErrUnavailable
}
random := make([]byte, 32)
if _, err := rand.Read(random); err != nil {
return "", err
}
token := base64.RawURLEncoding.EncodeToString(random)
principal.IssuedAt = time.Now().Unix()
payload, err := json.Marshal(principal)
if err != nil {
return "", err
}
if err := s.client.Set(ctx, sessionKey(token), payload, ttl).Err(); err != nil {
return "", fmt.Errorf("%w: %v", ErrUnavailable, err)
}
return token, nil
}
func (s *SessionStore) Authenticate(ctx context.Context, authorization string, expected Kind) (Principal, error) {
if s.client == nil {
return Principal{}, ErrUnavailable
}
token, ok := bearerToken(authorization)
if !ok {
return Principal{}, ErrInvalidSession
}
payload, err := s.client.Get(ctx, sessionKey(token)).Bytes()
if errors.Is(err, redis.Nil) {
return Principal{}, ErrInvalidSession
}
if err != nil {
return Principal{}, fmt.Errorf("%w: %v", ErrUnavailable, err)
}
var principal Principal
if err := json.Unmarshal(payload, &principal); err != nil || principal.Kind != expected || principal.SubjectID == "" || principal.Purpose != "session" {
return Principal{}, ErrInvalidSession
}
return principal, nil
}
func (s *SessionStore) AuthenticatePending(ctx context.Context, token string, expected Kind) (Principal, error) {
principal, err := s.authenticateToken(ctx, token)
if err != nil || principal.Kind != expected || principal.SubjectID == "" || principal.Purpose != "totp_pending" {
return Principal{}, ErrInvalidSession
}
return principal, nil
}
func (s *SessionStore) authenticateToken(ctx context.Context, token string) (Principal, error) {
if s.client == nil {
return Principal{}, ErrUnavailable
}
payload, err := s.client.Get(ctx, sessionKey(strings.TrimSpace(token))).Bytes()
if errors.Is(err, redis.Nil) {
return Principal{}, ErrInvalidSession
}
if err != nil {
return Principal{}, fmt.Errorf("%w: %v", ErrUnavailable, err)
}
var principal Principal
if err := json.Unmarshal(payload, &principal); err != nil {
return Principal{}, ErrInvalidSession
}
return principal, nil
}
func (s *SessionStore) DeleteToken(ctx context.Context, token string) error {
if s.client == nil {
return ErrUnavailable
}
if err := s.client.Del(ctx, sessionKey(strings.TrimSpace(token))).Err(); err != nil {
return fmt.Errorf("%w: %v", ErrUnavailable, err)
}
return nil
}
func (s *SessionStore) Delete(ctx context.Context, authorization string) error {
if s.client == nil {
return ErrUnavailable
}
token, ok := bearerToken(authorization)
if !ok {
return nil
}
if err := s.client.Del(ctx, sessionKey(token)).Err(); err != nil {
return fmt.Errorf("%w: %v", ErrUnavailable, err)
}
return nil
}
func (s *SessionStore) StoreOneTime(ctx context.Context, namespace string, value any, ttl time.Duration) (string, error) {
if s.client == nil {
return "", ErrUnavailable
}
random := make([]byte, 32)
if _, err := rand.Read(random); err != nil {
return "", err
}
token := base64.RawURLEncoding.EncodeToString(random)
payload, err := json.Marshal(value)
if err != nil {
return "", err
}
if err := s.client.Set(ctx, oneTimeKey(namespace, token), payload, ttl).Err(); err != nil {
return "", fmt.Errorf("%w: %v", ErrUnavailable, err)
}
return token, nil
}
func (s *SessionStore) ConsumeOneTime(ctx context.Context, namespace, token string, target any) error {
if s.client == nil {
return ErrUnavailable
}
payload, err := s.client.GetDel(ctx, oneTimeKey(namespace, strings.TrimSpace(token))).Bytes()
if errors.Is(err, redis.Nil) {
return ErrInvalidSession
}
if err != nil {
return fmt.Errorf("%w: %v", ErrUnavailable, err)
}
if err := json.Unmarshal(payload, target); err != nil {
return ErrInvalidSession
}
return nil
}
// ClaimIdentifier atomically claims a caller-provided replay identifier for
// the TTL. It is used for signed protocol message IDs, not bearer secrets.
func (s *SessionStore) ClaimIdentifier(ctx context.Context, namespace, identifier string, ttl time.Duration) (bool, error) {
if s.client == nil {
return false, ErrUnavailable
}
identifier = strings.TrimSpace(identifier)
if identifier == "" || len(identifier) > 512 {
return false, ErrInvalidSession
}
claimed, err := s.client.SetNX(ctx, oneTimeKey(namespace, identifier), "1", ttl).Result()
if err != nil {
return false, fmt.Errorf("%w: %v", ErrUnavailable, err)
}
return claimed, nil
}
func bearerToken(authorization string) (string, bool) {
authorization = strings.TrimSpace(authorization)
if len(authorization) <= len("Bearer ") || !strings.EqualFold(authorization[:len("Bearer ")], "Bearer ") {
return "", false
}
token := strings.TrimSpace(authorization[len("Bearer "):])
return token, token != ""
}
func sessionKey(token string) string {
digest := sha256.Sum256([]byte(token))
return "gateway:session:v1:" + hex.EncodeToString(digest[:])
}
func oneTimeKey(namespace, token string) string {
digest := sha256.Sum256([]byte(token))
return "gateway:one-time:" + namespace + ":" + hex.EncodeToString(digest[:])
}