fb2c3a6cae
- Tool 接口 + Registry 注册中心,工具模块化插拔 - system 工具:health / info - database 工具:db_query / db_tables / db_table_info / db_explain(四层 SQL 安全) - docker 工具:docker_ps / docker_logs / docker_inspect(白名单 + 大小限制) - middleware:auth(JWT+APIKey)/ ratelimit(令牌桶)/ audit(slog 结构化) - stdio / SSE 双传输模式,Viper 多环境配置 - Dockerfile 多阶段构建(golang:alpine → scratch),~15MB 镜像 - docker-compose.yml 一键部署 + 安全加固(read_only / no-new-privileges / cap_drop) - go build ./... / go vet ./... / go test ./... 全部通过 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
95 lines
2.1 KiB
Go
95 lines
2.1 KiB
Go
package middleware
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"net/http"
|
||
"strings"
|
||
|
||
"github.com/golang-jwt/jwt/v5"
|
||
)
|
||
|
||
type contextKey string
|
||
|
||
const (
|
||
KeySubject contextKey = "subject"
|
||
KeyScopes contextKey = "scopes"
|
||
)
|
||
|
||
// Auth 验证 Bearer Token(API Key 或 JWT)。
|
||
type Auth struct {
|
||
apiKeys map[string]string // key → description
|
||
jwtSecret []byte
|
||
}
|
||
|
||
func NewAuth(apiKeys map[string]string, jwtSecret string) *Auth {
|
||
return &Auth{
|
||
apiKeys: apiKeys,
|
||
jwtSecret: []byte(jwtSecret),
|
||
}
|
||
}
|
||
|
||
// Middleware 从 HTTP Header 中提取并验证 Token。
|
||
func (a *Auth) Middleware(next http.Handler) http.Handler {
|
||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
token := extractBearer(r)
|
||
if token == "" {
|
||
http.Error(w, "missing Bearer token", http.StatusUnauthorized)
|
||
return
|
||
}
|
||
|
||
// 尝试 API Key 验证
|
||
if desc, ok := a.apiKeys[token]; ok {
|
||
ctx := context.WithValue(r.Context(), KeySubject, fmt.Sprintf("apikey:%s", desc))
|
||
ctx = context.WithValue(ctx, KeyScopes, []string{"*"})
|
||
next.ServeHTTP(w, r.WithContext(ctx))
|
||
return
|
||
}
|
||
|
||
// 尝试 JWT 验证
|
||
if a.jwtSecret != nil {
|
||
claims, err := a.parseJWT(token)
|
||
if err == nil {
|
||
ctx := context.WithValue(r.Context(), KeySubject, claims.Subject)
|
||
ctx = context.WithValue(ctx, KeyScopes, claims.Scopes)
|
||
next.ServeHTTP(w, r.WithContext(ctx))
|
||
return
|
||
}
|
||
}
|
||
|
||
http.Error(w, "invalid token", http.StatusUnauthorized)
|
||
})
|
||
}
|
||
|
||
type customClaims struct {
|
||
jwt.RegisteredClaims
|
||
Scopes []string `json:"scopes"`
|
||
}
|
||
|
||
func (a *Auth) parseJWT(tokenStr string) (*customClaims, error) {
|
||
token, err := jwt.ParseWithClaims(tokenStr, &customClaims{},
|
||
func(t *jwt.Token) (any, error) {
|
||
return a.jwtSecret, nil
|
||
},
|
||
)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
claims, ok := token.Claims.(*customClaims)
|
||
if !ok || !token.Valid {
|
||
return nil, fmt.Errorf("invalid claims")
|
||
}
|
||
return claims, nil
|
||
}
|
||
|
||
func extractBearer(r *http.Request) string {
|
||
auth := r.Header.Get("Authorization")
|
||
if auth == "" {
|
||
return ""
|
||
}
|
||
if !strings.HasPrefix(auth, "Bearer ") {
|
||
return ""
|
||
}
|
||
return strings.TrimPrefix(auth, "Bearer ")
|
||
}
|