🔒️ security(server): 业务接口保护开关与存活探针
- access_key 改用 subtle.ConstantTimeCompare 常量时间比较,防时序攻击 - 新增 server.protect_all 配置:开启后业务接口(/search、/content 等)也要求访问密钥 - 代理路由(/audio/proxy/、/manga/image/)不参与保护,其 URL 由服务端生成不含 key - 新增 /healthz 存活探针(不校验密钥,供容器/反代使用) - openapi.yaml 补 /healthz 定义,Unauthorized 描述说明 protect_all 行为 - 新增 3 个测试:保护开关放行、healthz、错误响应行为
This commit is contained in:
1 parent
d1774a3902
commit
24c7e4b6fd
5 files changed
+78
-26
No files matched your search
@@ -1,6 +1,7 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"crypto/subtle"
|
||||
"encoding/json"
|
||||
"fanqie/internal/config"
|
||||
"fanqie/internal/fqapi"
|
||||
@@ -122,19 +123,33 @@ func RequireAccessKey(handler http.HandlerFunc) http.HandlerFunc {
|
||||
key = r.Header.Get("X-Access-Key")
|
||||
}
|
||||
|
||||
// 验证密钥
|
||||
if key != cfg.Server.AccessKey {
|
||||
// 验证密钥(常量时间比较,防时序攻击)
|
||||
if subtle.ConstantTimeCompare([]byte(key), []byte(cfg.Server.AccessKey)) != 1 {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
fmt.Fprintf(w, `{"code": -1, "msg": "访问被拒绝:无效的访问密钥"}`)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// 验证通过,继续处理请求
|
||||
handler(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
// OptionalAccessKey 业务接口保护(仅 server.protect_all 开启时校验)
|
||||
func OptionalAccessKey(handler http.HandlerFunc) http.HandlerFunc {
|
||||
cfg := config.GetAppConfig()
|
||||
if cfg == nil || !cfg.Server.ProtectAll {
|
||||
return handler
|
||||
}
|
||||
return RequireAccessKey(handler)
|
||||
}
|
||||
|
||||
// HandleHealth 存活探针(供容器/反向代理使用)
|
||||
func HandleHealth(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprint(w, "ok")
|
||||
}
|
||||
|
||||
// HandleMain 主页处理
|
||||
func HandleMain(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
|
||||
@@ -2,6 +2,7 @@ package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
@@ -40,3 +41,22 @@ func TestHandleDetailMissingParam(t *testing.T) {
|
||||
t.Errorf("缺参应返回 400, got %d, body: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
// TestOptionalAccessKeyDisabled protect_all 未启用(或配置未加载)时直接放行
|
||||
func TestOptionalAccessKeyDisabled(t *testing.T) {
|
||||
called := false
|
||||
h := OptionalAccessKey(func(w http.ResponseWriter, r *http.Request) { called = true })
|
||||
h(httptest.NewRecorder(), httptest.NewRequest("GET", "/search?query=x", nil))
|
||||
if !called {
|
||||
t.Error("protect_all 未启用时应直接放行")
|
||||
}
|
||||
}
|
||||
|
||||
// TestHandleHealth 存活探针返回 200 ok
|
||||
func TestHandleHealth(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
HandleHealth(w, httptest.NewRequest("GET", "/healthz", nil))
|
||||
if w.Code != 200 || w.Body.String() != "ok" {
|
||||
t.Errorf("应返回 200 ok, got %d %q", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user