aliyun sensor server

This commit is contained in:
tamakyi
2026-09-10 18:04:19 +08:00
parent e3e59e7c69
commit 8e2325eda5
38 changed files with 3147 additions and 344 deletions
+42
View File
@@ -42,6 +42,11 @@ func Init() error {
return errors.Wrap(err, "map 'security'")
}
if err := validateTextCensorSource(Security.EnableTextCensor, Security.TextCensorSource,
App.QiniuAccessKey, App.QiniuAccessSecret, App.AliyunAccessKey, App.AliyunAccessKeySecret); err != nil {
return err
}
if err := File.Section("server").MapTo(&Server); err != nil {
return errors.Wrap(err, "map 'server'")
}
@@ -103,3 +108,40 @@ func Init() error {
return nil
}
// validateTextCensorSource 校验文本审核通道([security] text_censor_source)与凭据的搭配。
//
// 单独抽成纯函数(只依赖入参、不读全局)是为了能直接被单测覆盖,也让 Init 保持轻薄。
//
// 规则:
// - 审核整体关闭(enabled=false)→ 不校验,此时不需要任何凭据。
// - 留空 / auto → 通过。auto 是历史默认(七牛优先、阿里云兜底),两者都没配凭据时
// 维持既有行为(运行期 fail-closed),不阻断启动 —— 否则 app.sample.ini 原样拷贝
// (enable_text_censor = true 且凭据留空)将无法启动,破坏开箱流程。
// - 显式选 qiniu / aliyun → 必须配齐该通道的凭据,否则启动即失败。
// 理由:缺凭据时运行期审核必然失败,而审核失败是 fail-closed(拒绝提交),
// 会让发布/私信整站不可用,且现象隐蔽(只在日志可见)——不如启动就报错。
// - 其它取值 → 直接报错,不做静默回退:拼写错误若被悄悄当成 auto,
// 运维会以为「已经只用七牛了」,实际仍在走双通道。
func validateTextCensorSource(enabled bool, source, qiniuKey, qiniuSecret, aliyunKey, aliyunSecret string) error {
if !enabled {
return nil
}
switch strings.ToLower(strings.TrimSpace(source)) {
case "", CensorSourceAuto:
return nil
case CensorSourceQiniu:
if qiniuKey == "" || qiniuSecret == "" {
return errors.New("security.text_censor_source = qiniu requires app.qiniu_access_key and app.qiniu_access_secret")
}
return nil
case CensorSourceAliyun:
if aliyunKey == "" || aliyunSecret == "" {
return errors.New("security.text_censor_source = aliyun requires app.aliyun_access_key and app.aliyun_access_key_secret")
}
return nil
default:
return errors.Errorf("security.text_censor_source must be one of qiniu / aliyun / auto, got %q", source)
}
}
+35 -6
View File
@@ -12,6 +12,19 @@ var (
BuildCommit = "dev"
)
// 文本审核服务通道取值([security] text_censor_source)。
//
// 定义在 conf 侧是为了让「配置校验」(conf.Init)与「通道装配」
// (internal/security/censor)引用同一份字面量,避免两边各写一份字符串而漂移。
const (
// CensorSourceAuto 七牛优先、阿里云兜底(历史行为,留空同义)。
CensorSourceAuto = "auto"
// CensorSourceQiniu 只用七牛。
CensorSourceQiniu = "qiniu"
// CensorSourceAliyun 只用阿里云。
CensorSourceAliyun = "aliyun"
)
var (
App struct {
Production bool `ini:"production"`
@@ -51,12 +64,28 @@ var (
// (域名/题目不存在等均走 302 业务跳转,不产生计数)一律放行,仅命中后台扫描特征
// 路径(KindSensitive)才参与统计封禁。避免共享出口(CDN 边缘 / CGNAT NAT444)下
// 正常用户被 404 探测流量连坐误伤。如需恢复对纯 404 字典扫描的封禁再置 true。
BanTrackNotFound bool `ini:"ban_track_notfound"`
BanThresholdSensitive int `ini:"ban_threshold_sensitive"` // 窗口内敏感路径命中次数
BanThreshold404 int `ini:"ban_threshold_404"` // 窗口内 404 次数
BanWindowSeconds int `ini:"ban_window_seconds"` // 统计窗口(秒)
BanDurationMinutes int `ini:"ban_duration_minutes"` // 封禁时长(分钟)
SecurityAlertEmail string `ini:"security_alert_email"` // 触发封禁时告警收件人(留空不发送)
BanTrackNotFound bool `ini:"ban_track_notfound"`
BanThresholdSensitive int `ini:"ban_threshold_sensitive"` // 窗口内敏感路径命中次数
BanThreshold404 int `ini:"ban_threshold_404"` // 窗口内 404 次数
BanWindowSeconds int `ini:"ban_window_seconds"` // 统计窗口(秒)
BanDurationMinutes int `ini:"ban_duration_minutes"` // 封禁时长(分钟)
// DefaultModerationStrictness 站点默认的「内容审核严格度」档位
// (lax / standard / strict)。仅对未单独设置档位的账号生效:
// 账号一旦显式选择了档位就以账号设置为准(见 moderation.StrictnessFor)。
// 留空或非法值回退 strict(最严档)—— 保证升级不会放宽任何拦截;
// 严格档拦截面不小于升级前,部署后默认拦截面可能略有扩大;
// 若希望默认更少误伤,在本段显式写 standard 即可,无需改代码。
DefaultModerationStrictness string `ini:"default_moderation_strictness"`
// TextCensorSource 文本审核服务通道,取值:
// qiniu —— 只用七牛(凭据见 [app] qiniu_access_key / qiniu_access_secret)
// aliyun —— 只用阿里云(走内容安全 2.0 / 文本审核增强版 TextModeration;
// 开通只需实名认证,个人账号可用。凭据见 [app] aliyun_access_key / aliyun_access_key_secret)
// auto(默认,留空同义)—— 保持历史行为:七牛优先,未通过时用阿里云二次复核
// 仅显式选定的通道会被调用;选定通道缺少凭据时启动即报错(见 conf.Init),
// 避免运行时审核必然失败 —— 审核失败是 fail-closed(拒绝提交),
// 会让发布/私信整站不可用,且现象隐蔽(只在日志里)。
TextCensorSource string `ini:"text_censor_source"`
SecurityAlertEmail string `ini:"security_alert_email"` // 触发封禁时告警收件人(留空不发送)
}
Server struct {
+55
View File
@@ -0,0 +1,55 @@
// Copyright 2022 E99p1ant. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package conf
import (
"testing"
"github.com/stretchr/testify/assert"
)
// TestValidateTextCensorSource 覆盖 [security] text_censor_source 与 [app] 凭据的搭配校验。
//
// 关注两类真实故障:
// 1. 显式选定通道却缺凭据 —— 运行期审核必然失败,而审核失败是 fail-closed,
// 会让整站发布/私信不可用且现象隐蔽,必须在启动阶段拦住;
// 2. 取值拼写错误 —— 若被静默回退成 auto,运维会误以为「已经只用七牛了」。
func TestValidateTextCensorSource(t *testing.T) {
for _, tc := range []struct {
name string
enabled bool
source string
qk, qs string
ak, as string
wantErr bool
}{
{name: "审核关闭:不校验任何通道配置", enabled: false, source: "aliyun"},
{name: "审核关闭:非法取值也不报错", enabled: false, source: "qinui"},
{name: "留空:通过(历史默认行为)", enabled: true, source: ""},
{name: "auto:通过", enabled: true, source: "auto"},
{name: "auto:无任何凭据也通过(不阻断开箱启动)", enabled: true, source: "auto"},
{name: "qiniu:凭据齐备通过", enabled: true, source: "qiniu", qk: "k", qs: "s"},
{name: "qiniu:缺 access key 报错", enabled: true, source: "qiniu", qs: "s", wantErr: true},
{name: "qiniu:缺 access secret 报错", enabled: true, source: "qiniu", qk: "k", wantErr: true},
{name: "aliyun:凭据齐备通过", enabled: true, source: "aliyun", ak: "k", as: "s"},
{name: "aliyun:缺凭据报错", enabled: true, source: "aliyun", wantErr: true},
{name: "大小写与空白不敏感", enabled: true, source: " QINIU ", qk: "k", qs: "s"},
{name: "非法取值:报错而非静默回退为 auto", enabled: true, source: "qinui", qk: "k", qs: "s", wantErr: true},
} {
t.Run(tc.name, func(t *testing.T) {
err := validateTextCensorSource(tc.enabled, tc.source, tc.qk, tc.qs, tc.ak, tc.as)
if tc.wantErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
})
}
}