feat: 添加安全中间件、优化限流器与API密钥验证
1. 新增安全响应头中间件强化请求安全性 2. 使用恒定时间比较修复API密钥验证时序漏洞 3. 新增限流器条目上限配置与自动清理逻辑 4. 优化CORS日志与客户端IP获取逻辑 5. 升级Go构建环境与基础镜像版本 6. 新增非root用户运行容器增强安全性
This commit is contained in:
+8
-3
@@ -1,4 +1,4 @@
|
|||||||
FROM golang:1.19-alpine AS builder
|
FROM golang:1.23-alpine AS builder
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
@@ -9,15 +9,20 @@ COPY . .
|
|||||||
|
|
||||||
RUN CGO_ENABLED=0 GOOS=linux go build -o tts-api .
|
RUN CGO_ENABLED=0 GOOS=linux go build -o tts-api .
|
||||||
|
|
||||||
FROM alpine:3.18
|
FROM alpine:3.21
|
||||||
|
|
||||||
RUN apk --no-cache add ca-certificates tzdata
|
RUN apk --no-cache add ca-certificates tzdata \
|
||||||
|
&& addgroup -S appgroup && adduser -S appuser -G appgroup
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY --from=builder /app/tts-api .
|
COPY --from=builder /app/tts-api .
|
||||||
COPY --from=builder /app/health.html .
|
COPY --from=builder /app/health.html .
|
||||||
|
|
||||||
|
RUN chown -R appuser:appgroup /app
|
||||||
|
|
||||||
|
USER appuser
|
||||||
|
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
|
|
||||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
|
||||||
|
|||||||
+2
-1
@@ -1,4 +1,4 @@
|
|||||||
package common
|
package common
|
||||||
|
|
||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
@@ -17,4 +17,5 @@ const (
|
|||||||
MaxConcurrentRequests = 10
|
MaxConcurrentRequests = 10
|
||||||
CleanupInterval = time.Hour
|
CleanupInterval = time.Hour
|
||||||
MaxModelNameLength = 64
|
MaxModelNameLength = 64
|
||||||
|
MaxRateLimiterEntries = 100000
|
||||||
)
|
)
|
||||||
|
|||||||
+2
-1
@@ -1,6 +1,7 @@
|
|||||||
package middleware
|
package middleware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/subtle"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -39,7 +40,7 @@ func ValidateAPIKey(r *http.Request) bool {
|
|||||||
|
|
||||||
token := strings.TrimPrefix(authHeader, "Bearer ")
|
token := strings.TrimPrefix(authHeader, "Bearer ")
|
||||||
for _, validKey := range validAPIKeys {
|
for _, validKey := range validAPIKeys {
|
||||||
if token == validKey {
|
if subtle.ConstantTimeCompare([]byte(token), []byte(validKey)) == 1 {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-6
@@ -96,16 +96,13 @@ func CORS(next http.Handler) http.Handler {
|
|||||||
} else if !strings.Contains(vary, "Origin") {
|
} else if !strings.Contains(vary, "Origin") {
|
||||||
w.Header().Set("Vary", vary+", Origin")
|
w.Header().Set("Vary", vary+", Origin")
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
log.Printf("CORS拦截: 来源=%q 路径=%s 方法=%s 客户端=%s",
|
||||||
|
origin, r.URL.Path, r.Method, GetClientIP(r))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if r.Method == http.MethodOptions {
|
if r.Method == http.MethodOptions {
|
||||||
if origin != "" {
|
|
||||||
if _, matched := matchOrigin(origin); !matched {
|
|
||||||
w.WriteHeader(http.StatusNoContent)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
w.WriteHeader(http.StatusNoContent)
|
w.WriteHeader(http.StatusNoContent)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-2
@@ -1,6 +1,7 @@
|
|||||||
package middleware
|
package middleware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"log"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -77,6 +78,16 @@ func (rl *RateLimiter) cleanup() {
|
|||||||
rl.requests[k] = valid
|
rl.requests[k] = valid
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(rl.requests) > common.MaxRateLimiterEntries {
|
||||||
|
log.Printf("警告: 限流器条目数 %d 超过上限 %d,触发强制清理", len(rl.requests), common.MaxRateLimiterEntries)
|
||||||
|
for k := range rl.requests {
|
||||||
|
if len(rl.requests) <= common.MaxRateLimiterEntries/2 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
delete(rl.requests, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetClientIP(r *http.Request) string {
|
func GetClientIP(r *http.Request) string {
|
||||||
@@ -84,11 +95,14 @@ func GetClientIP(r *http.Request) string {
|
|||||||
if xForwardedFor != "" {
|
if xForwardedFor != "" {
|
||||||
ips := strings.Split(xForwardedFor, ",")
|
ips := strings.Split(xForwardedFor, ",")
|
||||||
if len(ips) > 0 {
|
if len(ips) > 0 {
|
||||||
return strings.TrimSpace(ips[0])
|
ip := strings.TrimSpace(ips[0])
|
||||||
|
if ip != "" {
|
||||||
|
return ip
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
xRealIP := r.Header.Get("X-Real-IP")
|
xRealIP := strings.TrimSpace(r.Header.Get("X-Real-IP"))
|
||||||
if xRealIP != "" {
|
if xRealIP != "" {
|
||||||
return xRealIP
|
return xRealIP
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SecurityHeaders(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||||
|
w.Header().Set("X-Frame-Options", "DENY")
|
||||||
|
w.Header().Set("X-XSS-Protection", "1; mode=block")
|
||||||
|
w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
|
||||||
|
|
||||||
|
if strings.HasPrefix(r.URL.Path, "/v1/") || r.URL.Path == "/health" {
|
||||||
|
w.Header().Set("Cache-Control", "no-store")
|
||||||
|
}
|
||||||
|
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ func Setup() *mux.Router {
|
|||||||
r := mux.NewRouter()
|
r := mux.NewRouter()
|
||||||
|
|
||||||
r.Use(middleware.CORS)
|
r.Use(middleware.CORS)
|
||||||
|
r.Use(middleware.SecurityHeaders)
|
||||||
r.Use(middleware.Logger)
|
r.Use(middleware.Logger)
|
||||||
|
|
||||||
r.HandleFunc("/v1/audio/speech", controller.OpenaiTTSHandler).Methods("POST", "OPTIONS")
|
r.HandleFunc("/v1/audio/speech", controller.OpenaiTTSHandler).Methods("POST", "OPTIONS")
|
||||||
|
|||||||
BIN
Binary file not shown.
Reference in New Issue
Block a user