Go MCP Server 项目初始化:工具注册架构 + 编译运行通过
- 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>
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// responseWriter 捕获状态码,实现 http.ResponseWriter。
|
||||
type responseWriter struct {
|
||||
http.ResponseWriter
|
||||
status int
|
||||
}
|
||||
|
||||
func (rw *responseWriter) WriteHeader(code int) {
|
||||
rw.status = code
|
||||
rw.ResponseWriter.WriteHeader(code)
|
||||
}
|
||||
|
||||
// Audit 记录每个 HTTP 请求的结构化审计日志。
|
||||
func Audit(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
start := time.Now()
|
||||
rw := &responseWriter{ResponseWriter: w, status: 200}
|
||||
|
||||
next.ServeHTTP(rw, r)
|
||||
|
||||
subject := "anonymous"
|
||||
if sub, ok := r.Context().Value(KeySubject).(string); ok {
|
||||
subject = sub
|
||||
}
|
||||
|
||||
slog.Info("request",
|
||||
"method", r.Method,
|
||||
"path", r.URL.Path,
|
||||
"status", rw.status,
|
||||
"duration_ms", time.Since(start).Milliseconds(),
|
||||
"subject", subject,
|
||||
"remote_addr", r.RemoteAddr,
|
||||
)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
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 ")
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"golang.org/x/time/rate"
|
||||
)
|
||||
|
||||
// RateLimiter 基于调用方标识的令牌桶限流。
|
||||
type RateLimiter struct {
|
||||
limiters map[string]*rate.Limiter
|
||||
rate rate.Limit
|
||||
burst int
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func NewRateLimiter(r rate.Limit, burst int) *RateLimiter {
|
||||
return &RateLimiter{
|
||||
limiters: make(map[string]*rate.Limiter),
|
||||
rate: r,
|
||||
burst: burst,
|
||||
}
|
||||
}
|
||||
|
||||
func (rl *RateLimiter) Middleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
subject := "anonymous"
|
||||
if sub, ok := r.Context().Value(KeySubject).(string); ok {
|
||||
subject = sub
|
||||
}
|
||||
|
||||
limiter := rl.getLimiter(subject)
|
||||
if !limiter.Allow() {
|
||||
http.Error(w, "rate limit exceeded", http.StatusTooManyRequests)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func (rl *RateLimiter) getLimiter(key string) *rate.Limiter {
|
||||
rl.mu.Lock()
|
||||
defer rl.mu.Unlock()
|
||||
|
||||
limiter, ok := rl.limiters[key]
|
||||
if !ok {
|
||||
limiter = rate.NewLimiter(rl.rate, rl.burst)
|
||||
rl.limiters[key] = limiter
|
||||
}
|
||||
return limiter
|
||||
}
|
||||
Reference in New Issue
Block a user