Files
pure-note/internal/middleware/middleware.go
T
wangairnan 247e88c4fb 后端基础层:模块定义、配置解析、SQLite 存储层、Argon2id 认证与 Markdown 渲染
- internal/config:serve/init/backup/gc 子命令参数解析(--dev 强制 loopback 守卫)
- internal/store:user_version 版本化迁移(仅追加式 + 越界拒启 + --allow-newer)、
  笔记/图片/引用/会话/设置 DAO、并集可见性查询、VACUUM INTO 在线备份、
  回收站 30 天 + 孤儿图 7 天宽限 gc
- internal/auth:Argon2id PHC 串(m=19456,t=2,p=1)、256bit token 与 SHA-256 摘要
- internal/markdown:goldmark(默认转义)+ bluemonday 双保险,摘要纯文本提取
- internal/middleware:安全头(CSP/HSTS/nosniff 等)、错误日志、
  有界令牌桶限流(per-IP 桶上限 + TTL 逐出)、Origin/Referer 同源校验
2026-09-08 08:14:12 +08:00

137 lines
4.1 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Package middleware 自写中间件链(§7.2):
// SecurityHeaders → 日志 → 全局限流 → Origin 校验 →(路由级)MaxBytes → Auth → CSRF。
package middleware
import (
"context"
"log/slog"
"net"
"net/http"
"strings"
"time"
)
// statusWriter 捕获响应状态码供日志使用。
type statusWriter struct {
http.ResponseWriter
status int
}
func (w *statusWriter) WriteHeader(code int) {
if w.status == 0 {
w.status = code
}
w.ResponseWriter.WriteHeader(code)
}
func (w *statusWriter) Write(b []byte) (int, error) {
if w.status == 0 {
w.status = http.StatusOK
}
return w.ResponseWriter.Write(b)
}
// SecurityHeaders 下发安全 HTTP 头(§9.2)。
func SecurityHeaders(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
h := w.Header()
h.Set("Content-Security-Policy",
"default-src 'self'; "+
"script-src 'self'; "+
// 'unsafe-inline' 仅因 CodeMirror 经 style-mod 运行时注入 <style>(§9.2)。
"style-src 'self' 'unsafe-inline'; "+
"img-src 'self' data:; "+
"font-src 'self'; "+
"connect-src 'self'; "+
"object-src 'none'; base-uri 'none'; "+
"frame-ancestors 'none'; form-action 'self'")
// 站点仅经 HTTPS 反代对外服务(§9.1-T11)
h.Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
h.Set("Referrer-Policy", "strict-origin-when-cross-origin")
h.Set("X-Content-Type-Options", "nosniff")
h.Set("X-Frame-Options", "DENY")
h.Set("Permissions-Policy", "camera=(), microphone=(), geolocation=()")
next.ServeHTTP(w, r)
})
}
// RequestLogger 请求日志:仅记录错误(status ≥ 400)。
func RequestLogger(log *slog.Logger) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
sw := &statusWriter{ResponseWriter: w}
start := time.Now()
next.ServeHTTP(sw, r)
if sw.status >= 400 {
log.Warn("http",
"method", r.Method,
"path", r.URL.Path,
"status", sw.status,
"ip", ClientIP(r, trueBehindProxy(r)),
"duration_ms", time.Since(start).Milliseconds(),
)
}
})
}
}
// behindProxyKey 由 Server 注入到请求上下文,供日志取 IP 用。
type ctxKey string
const behindProxyKey ctxKey = "behind_proxy"
// BehindProxy 中间件:把「位于可信反代之后」标记注入请求上下文,
// 供日志与限流取客户端 IP 使用。
func BehindProxy(v bool) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
next.ServeHTTP(w, WithBehindProxy(r, v))
})
}
}
// WithBehindProxy 标记请求位于可信反代之后。
func WithBehindProxy(r *http.Request, v bool) *http.Request {
return r.WithContext(context.WithValue(r.Context(), behindProxyKey, v))
}
func trueBehindProxy(r *http.Request) bool {
v, _ := r.Context().Value(behindProxyKey).(bool)
return v
}
// ClientIP 提取客户端 IP:behindProxy 时取 X-Forwarded-For 最右条目
// (Caddy 追加语义,§14),否则取 RemoteAddr。
func ClientIP(r *http.Request, behindProxy bool) string {
if behindProxy {
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
parts := strings.Split(xff, ",")
return strings.TrimSpace(parts[len(parts)-1])
}
}
host, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
return r.RemoteAddr
}
return host
}
// MaxBytes 路由级请求体上限中间件(§7.2)。
func MaxBytes(n int64) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, n)
next.ServeHTTP(w, r)
})
}
}
// NoStore 为 /api/admin/* 与 /api/auth/* 响应统一附加
// Cache-Control: no-store(防登出后 bfcache 回看,§9.2)。
func NoStore(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", "no-store")
next.ServeHTTP(w, r)
})
}