Files
lijiaoqiao/gateway/internal/middleware/chain.go
Your Name 89104bd0db feat(P1/P2): 完成TDD开发及P1/P2设计文档
## 设计文档
- multi_role_permission_design: 多角色权限设计 (CONDITIONAL GO)
- audit_log_enhancement_design: 审计日志增强 (CONDITIONAL GO)
- routing_strategy_template_design: 路由策略模板 (CONDITIONAL GO)
- sso_saml_technical_research: SSO/SAML调研 (CONDITIONAL GO)
- compliance_capability_package_design: 合规能力包设计 (CONDITIONAL GO)

## TDD开发成果
- IAM模块: supply-api/internal/iam/ (111个测试)
- 审计日志模块: supply-api/internal/audit/ (40+测试)
- 路由策略模块: gateway/internal/router/ (33+测试)
- 合规能力包: gateway/internal/compliance/ + scripts/ci/compliance/

## 规范文档
- parallel_agent_output_quality_standards: 并行Agent产出质量规范
- project_experience_summary: 项目经验总结 (v2)
- 2026-04-02-p1-p2-tdd-execution-plan: TDD执行计划

## 评审报告
- 5个CONDITIONAL GO设计文档评审报告
- fix_verification_report: 修复验证报告
- full_verification_report: 全面质量验证报告
- tdd_module_quality_verification: TDD模块质量验证
- tdd_execution_summary: TDD执行总结

依据: Superpowers执行框架 + TDD规范
2026-04-02 23:35:53 +08:00

311 lines
8.5 KiB
Go

package middleware
import (
"context"
"encoding/json"
"fmt"
"net"
"net/http"
"strings"
"time"
)
const requestIDHeader = "X-Request-Id"
var defaultNowFunc = time.Now
type contextKey string
const (
requestIDKey contextKey = "request_id"
principalKey contextKey = "principal"
)
// Principal 认证成功后的主体信息
type Principal struct {
RequestID string
TokenID string
SubjectID string
Role string
Scope []string
}
// BuildTokenAuthChain 构建认证中间件链
func BuildTokenAuthChain(cfg AuthMiddlewareConfig, next http.Handler) http.Handler {
handler := tokenAuthMiddleware(cfg)(next)
handler = queryKeyRejectMiddleware(handler, cfg.Auditor, cfg.Now)
handler = requestIDMiddleware(handler, cfg.Now)
return handler
}
// RequestIDMiddleware 请求ID中间件
func requestIDMiddleware(next http.Handler, now func() time.Time) http.Handler {
if next == nil {
return http.HandlerFunc(func(http.ResponseWriter, *http.Request) {})
}
if now == nil {
now = defaultNowFunc
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requestID := ensureRequestID(r, now)
w.Header().Set(requestIDHeader, requestID)
next.ServeHTTP(w, r)
})
}
// queryKeyRejectMiddleware 拒绝query key入站
func queryKeyRejectMiddleware(next http.Handler, auditor AuditEmitter, now func() time.Time) http.Handler {
if next == nil {
return http.HandlerFunc(func(http.ResponseWriter, *http.Request) {})
}
if now == nil {
now = defaultNowFunc
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if hasExternalQueryKey(r) {
requestID, _ := RequestIDFromContext(r.Context())
emitAudit(r.Context(), auditor, AuditEvent{
EventName: EventTokenQueryKeyRejected,
RequestID: requestID,
Route: r.URL.Path,
ResultCode: CodeQueryKeyNotAllowed,
ClientIP: extractClientIP(r),
CreatedAt: now(),
})
writeError(w, http.StatusUnauthorized, requestID, CodeQueryKeyNotAllowed, "query key not allowed")
return
}
next.ServeHTTP(w, r)
})
}
// tokenAuthMiddleware Token认证中间件
func tokenAuthMiddleware(cfg AuthMiddlewareConfig) func(http.Handler) http.Handler {
cfg = cfg.withDefaults()
return func(next http.Handler) http.Handler {
if next == nil {
next = http.HandlerFunc(func(http.ResponseWriter, *http.Request) {})
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !cfg.shouldProtect(r.URL.Path) {
next.ServeHTTP(w, r)
return
}
requestID := ensureRequestID(r, cfg.Now)
if cfg.Verifier == nil || cfg.StatusResolver == nil || cfg.Authorizer == nil {
writeError(w, http.StatusServiceUnavailable, requestID, CodeAuthNotReady, "auth middleware dependencies are not ready")
return
}
rawToken, ok := extractBearerToken(r.Header.Get("Authorization"))
if !ok {
emitAudit(r.Context(), cfg.Auditor, AuditEvent{
EventName: EventTokenAuthnFail,
RequestID: requestID,
Route: r.URL.Path,
ResultCode: CodeAuthMissingBearer,
ClientIP: extractClientIP(r),
CreatedAt: cfg.Now(),
})
writeError(w, http.StatusUnauthorized, requestID, CodeAuthMissingBearer, "missing bearer token")
return
}
claims, err := cfg.Verifier.Verify(r.Context(), rawToken)
if err != nil {
emitAudit(r.Context(), cfg.Auditor, AuditEvent{
EventName: EventTokenAuthnFail,
RequestID: requestID,
Route: r.URL.Path,
ResultCode: CodeAuthInvalidToken,
ClientIP: extractClientIP(r),
CreatedAt: cfg.Now(),
})
writeError(w, http.StatusUnauthorized, requestID, CodeAuthInvalidToken, "invalid bearer token")
return
}
tokenStatus, err := cfg.StatusResolver.Resolve(r.Context(), claims.TokenID)
if err != nil || tokenStatus != TokenStatusActive {
emitAudit(r.Context(), cfg.Auditor, AuditEvent{
EventName: EventTokenAuthnFail,
RequestID: requestID,
TokenID: claims.TokenID,
SubjectID: claims.SubjectID,
Route: r.URL.Path,
ResultCode: CodeAuthTokenInactive,
ClientIP: extractClientIP(r),
CreatedAt: cfg.Now(),
})
writeError(w, http.StatusUnauthorized, requestID, CodeAuthTokenInactive, "token is inactive")
return
}
if !cfg.Authorizer.Authorize(r.URL.Path, r.Method, claims.Scope, claims.Role) {
emitAudit(r.Context(), cfg.Auditor, AuditEvent{
EventName: EventTokenAuthzDenied,
RequestID: requestID,
TokenID: claims.TokenID,
SubjectID: claims.SubjectID,
Route: r.URL.Path,
ResultCode: CodeAuthScopeDenied,
ClientIP: extractClientIP(r),
CreatedAt: cfg.Now(),
})
writeError(w, http.StatusForbidden, requestID, CodeAuthScopeDenied, "scope denied")
return
}
principal := Principal{
RequestID: requestID,
TokenID: claims.TokenID,
SubjectID: claims.SubjectID,
Role: claims.Role,
Scope: append([]string(nil), claims.Scope...),
}
ctx := context.WithValue(r.Context(), principalKey, principal)
ctx = context.WithValue(ctx, requestIDKey, requestID)
emitAudit(ctx, cfg.Auditor, AuditEvent{
EventName: EventTokenAuthnSuccess,
RequestID: requestID,
TokenID: claims.TokenID,
SubjectID: claims.SubjectID,
Route: r.URL.Path,
ResultCode: "OK",
ClientIP: extractClientIP(r),
CreatedAt: cfg.Now(),
})
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
// RequestIDFromContext 从Context获取请求ID
func RequestIDFromContext(ctx context.Context) (string, bool) {
if ctx == nil {
return "", false
}
value, ok := ctx.Value(requestIDKey).(string)
return value, ok
}
// PrincipalFromContext 从Context获取认证主体
func PrincipalFromContext(ctx context.Context) (Principal, bool) {
if ctx == nil {
return Principal{}, false
}
value, ok := ctx.Value(principalKey).(Principal)
return value, ok
}
func (cfg AuthMiddlewareConfig) withDefaults() AuthMiddlewareConfig {
if cfg.Now == nil {
cfg.Now = defaultNowFunc
}
if len(cfg.ProtectedPrefixes) == 0 {
cfg.ProtectedPrefixes = []string{"/api/v1/supply", "/api/v1/platform"}
}
if len(cfg.ExcludedPrefixes) == 0 {
cfg.ExcludedPrefixes = []string{"/health", "/healthz", "/metrics", "/readyz"}
}
return cfg
}
func (cfg AuthMiddlewareConfig) shouldProtect(path string) bool {
for _, prefix := range cfg.ExcludedPrefixes {
if strings.HasPrefix(path, prefix) {
return false
}
}
for _, prefix := range cfg.ProtectedPrefixes {
if strings.HasPrefix(path, prefix) {
return true
}
}
return false
}
func ensureRequestID(r *http.Request, now func() time.Time) string {
if now == nil {
now = defaultNowFunc
}
if requestID, ok := RequestIDFromContext(r.Context()); ok && requestID != "" {
return requestID
}
requestID := strings.TrimSpace(r.Header.Get(requestIDHeader))
if requestID == "" {
requestID = fmt.Sprintf("req-%d", now().UnixNano())
}
ctx := context.WithValue(r.Context(), requestIDKey, requestID)
*r = *r.WithContext(ctx)
return requestID
}
func extractBearerToken(authHeader string) (string, bool) {
const bearerPrefix = "Bearer "
if !strings.HasPrefix(authHeader, bearerPrefix) {
return "", false
}
token := strings.TrimSpace(strings.TrimPrefix(authHeader, bearerPrefix))
return token, token != ""
}
func hasExternalQueryKey(r *http.Request) bool {
if r.URL == nil {
return false
}
query := r.URL.Query()
for key := range query {
lowerKey := strings.ToLower(key)
if lowerKey == "key" || lowerKey == "api_key" || lowerKey == "token" || lowerKey == "access_token" {
return true
}
}
return false
}
func emitAudit(ctx context.Context, auditor AuditEmitter, event AuditEvent) {
if auditor == nil {
return
}
_ = auditor.Emit(ctx, event)
}
type errorResponse struct {
RequestID string `json:"request_id"`
Error errorPayload `json:"error"`
}
type errorPayload struct {
Code string `json:"code"`
Message string `json:"message"`
Details map[string]any `json:"details,omitempty"`
}
func writeError(w http.ResponseWriter, status int, requestID, code, message string) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
payload := errorResponse{
RequestID: requestID,
Error: errorPayload{
Code: code,
Message: message,
},
}
_ = json.NewEncoder(w).Encode(payload)
}
func extractClientIP(r *http.Request) string {
xForwardedFor := strings.TrimSpace(r.Header.Get("X-Forwarded-For"))
if xForwardedFor != "" {
parts := strings.Split(xForwardedFor, ",")
return strings.TrimSpace(parts[0])
}
host, _, err := net.SplitHostPort(r.RemoteAddr)
if err == nil {
return host
}
return r.RemoteAddr
}