package middleware import ( "bufio" "context" "crypto/rand" "encoding/hex" "encoding/json" "log/slog" "net" "net/http" "runtime/debug" "time" "git.yeij.top/AskaEth/Scoreboard/server/internal/auth" ) // HeaderRequestID 是客户端可复用的幂等/追踪请求头。 const HeaderRequestID = "X-Request-Id" // RequestID 复用客户端传入的 request_id,没有则生成一个并写回响应头。 func RequestID(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { id := r.Header.Get(HeaderRequestID) if id == "" { buf := make([]byte, 8) if _, err := rand.Read(buf); err == nil { id = hex.EncodeToString(buf) } else { id = time.Now().UTC().Format("20060102150405.000000") } } w.Header().Set(HeaderRequestID, id) next.ServeHTTP(w, r.WithContext(contextWithRequestID(r.Context(), id))) }) } func contextWithRequestID(ctx context.Context, id string) context.Context { return context.WithValue(ctx, ctxKeyRequestID, id) } // Recover 捕获 handler panic,返回统一 500,避免进程被单个请求带崩。 func Recover(logger *slog.Logger) func(http.Handler) http.Handler { if logger == nil { logger = slog.Default() } return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { defer func() { if rec := recover(); rec != nil { logger.Error("请求处理 panic", "err", rec, "path", r.URL.Path, "requestId", GetRequestID(r.Context()), "stack", string(debug.Stack()), ) writeError(w, http.StatusInternalServerError, "INTERNAL", "服务端异常", nil) } }() next.ServeHTTP(w, r) }) } } // Log 记录访问日志(方法、路径、状态、耗时、request_id)。 func Log(logger *slog.Logger) func(http.Handler) http.Handler { if logger == nil { logger = slog.Default() } return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() sw := &statusWriter{ResponseWriter: w, status: http.StatusOK} next.ServeHTTP(sw, r) logger.Debug("http", "method", r.Method, "path", r.URL.Path, "status", sw.status, "dur", time.Since(start), "requestId", GetRequestID(r.Context()), "ip", ClientIP(r), ) }) } } // Authenticate 解析会话 Cookie;没有或无效时按 guest 放行(只读默认身份)。 func Authenticate(svc *auth.Service) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { u := &auth.User{Role: auth.RoleGuest} if c, err := r.Cookie(auth.CookieSession); err == nil && c.Value != "" { if found, _, err := svc.Lookup(r.Context(), c.Value); err == nil { u = found } } next.ServeHTTP(w, r.WithContext(WithUser(r.Context(), u))) }) } } // RequireRole 要求至少 need 权限;未登录或权限不足一律 403。 // 用于「guest 也不该看」的只读接口(系统配置),以便与未登录写操作区分。 func RequireRole(need auth.Role) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if !User(r.Context()).Role.Allows(need) { writeError(w, http.StatusForbidden, "FORBIDDEN", "无权访问该资源", map[string]any{"required": need.String()}) return } next.ServeHTTP(w, r) }) } } // RequireWrite 要求至少 need 权限的写操作;guest 返回 401(需登录),角色不足返回 403。 func RequireWrite(need auth.Role) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { u := User(r.Context()) if u.Role == auth.RoleGuest { writeError(w, http.StatusUnauthorized, "AUTH_REQUIRED", "请先登录", nil) return } if !u.Role.Allows(need) { writeError(w, http.StatusForbidden, "FORBIDDEN", "无权执行该操作", map[string]any{"required": need.String()}) return } next.ServeHTTP(w, r) }) } } // CSRF 校验双提交 Cookie:已登录的写请求必须带 X-CSRF-Token 且与 csrf_token Cookie 一致。 func CSRF(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet, http.MethodHead, http.MethodOptions: next.ServeHTTP(w, r) return } if User(r.Context()).Role == auth.RoleGuest { // 未登录请求由 RequireWrite 处理,无需 CSRF。 next.ServeHTTP(w, r) return } var cookie string if c, err := r.Cookie(auth.CookieCSRF); err == nil { cookie = c.Value } if !auth.ValidCSRF(r.Header.Get("X-CSRF-Token"), cookie) { writeError(w, http.StatusForbidden, "FORBIDDEN", "CSRF 校验失败", nil) return } next.ServeHTTP(w, r) }) } // RateLimitWrite 对已登录用户的写操作限流(默认 30 次/分钟/用户)。 func RateLimitWrite(svc *auth.Service) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet, http.MethodHead, http.MethodOptions: next.ServeHTTP(w, r) return } u := User(r.Context()) if u.Role != auth.RoleGuest && !svc.AllowWrite(u.ID) { writeError(w, http.StatusTooManyRequests, "RATE_LIMITED", "操作过于频繁,请稍后再试", nil) return } next.ServeHTTP(w, r) }) } } // statusWriter 记录响应状态码,供日志与审计判断是否成功。 type statusWriter struct { http.ResponseWriter status int written bool } func (w *statusWriter) WriteHeader(code int) { if !w.written { w.status = code w.written = true } w.ResponseWriter.WriteHeader(code) } func (w *statusWriter) Write(b []byte) (int, error) { if !w.written { w.written = true } return w.ResponseWriter.Write(b) } // Hijack 透传底层连接:WebSocket 升级依赖 http.Hijacker, // 包装 ResponseWriter 时必须显式转发,否则握手会退化成 501。 func (w *statusWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { hj, ok := w.ResponseWriter.(http.Hijacker) if !ok { return nil, nil, http.ErrNotSupported } return hj.Hijack() } // Flush 透传,保证流式响应不被缓冲。 func (w *statusWriter) Flush() { if f, ok := w.ResponseWriter.(http.Flusher); ok { f.Flush() } } // writeError 输出统一错误结构;中间件不依赖 httpapi,避免包循环。 func writeError(w http.ResponseWriter, status int, code, message string, detail any) { w.Header().Set("Content-Type", "application/json; charset=utf-8") w.WriteHeader(status) body := map[string]any{ "ok": false, "error": map[string]any{ "code": code, "message": message, }, } if detail != nil { body["error"].(map[string]any)["detail"] = detail } _ = json.NewEncoder(w).Encode(body) }