feat(auth): 添加 Turnstile 注册防护

This commit is contained in:
Entropy.Xu
2026-05-15 01:54:20 +08:00
parent bf511f9f8c
commit 85f48123b6
25 changed files with 1705 additions and 22 deletions

View File

@@ -61,6 +61,11 @@
:rate-limit-per-minute="systemConfig.rate_limit_per_minute"
:enable-registration="systemConfig.enable_registration"
:password-policy-level="systemConfig.password_policy_level"
:turnstile-enabled="systemConfig.turnstile_enabled"
:turnstile-site-key="systemConfig.turnstile_site_key"
:turnstile-secret-key="systemConfig.turnstile_secret_key"
:turnstile-secret-configured="systemConfig.turnstile_secret_key_is_set"
:turnstile-allowed-hostnames-str="turnstileAllowedHostnamesStr"
:auto-delete-expired-keys="systemConfig.auto_delete_expired_keys"
:enable-format-conversion="systemConfig.enable_format_conversion"
:enable-openai-image-sync-heartbeat="systemConfig.enable_openai_image_sync_heartbeat"
@@ -71,6 +76,11 @@
@update:rate-limit-per-minute="systemConfig.rate_limit_per_minute = $event"
@update:enable-registration="systemConfig.enable_registration = $event"
@update:password-policy-level="systemConfig.password_policy_level = $event"
@update:turnstile-enabled="systemConfig.turnstile_enabled = $event"
@update:turnstile-site-key="systemConfig.turnstile_site_key = $event"
@update:turnstile-secret-key="systemConfig.turnstile_secret_key = $event"
@update:turnstile-allowed-hostnames-str="turnstileAllowedHostnamesStr = $event"
@clear-turnstile-secret="clearTurnstileSecret"
@update:auto-delete-expired-keys="systemConfig.auto_delete_expired_keys = $event"
@update:enable-format-conversion="systemConfig.enable_format_conversion = $event"
@update:enable-openai-image-sync-heartbeat="systemConfig.enable_openai_image_sync_heartbeat = $event"
@@ -309,11 +319,13 @@ const {
maxRequestBodySizeKB,
maxResponseBodySizeKB,
sensitiveHeadersStr,
turnstileAllowedHostnamesStr,
loadSystemConfig,
loadSystemVersion,
saveSiteInfo,
saveProxyConfig,
saveBasicConfig,
clearTurnstileSecret,
saveLogConfig,
saveCleanupConfig,
handleAutoCleanupToggle,

View File

@@ -171,6 +171,97 @@
</div>
</div>
</div>
<div class="md:col-span-2 grid grid-cols-1 md:grid-cols-2 gap-4 border-t pt-5">
<div class="flex items-center h-full">
<div class="flex items-center space-x-2">
<Checkbox
id="turnstile-enabled"
:checked="turnstileEnabled"
@update:checked="$emit('update:turnstileEnabled', $event)"
/>
<div>
<Label
for="turnstile-enabled"
class="cursor-pointer"
>
注册人机验证
</Label>
<p class="text-xs text-muted-foreground">
开启后注册与发送邮箱验证码前需要通过 Cloudflare Turnstile
</p>
</div>
</div>
</div>
<div>
<Label
for="turnstile-site-key"
class="block text-sm font-medium"
>
Turnstile Site Key
</Label>
<Input
id="turnstile-site-key"
:model-value="turnstileSiteKey || ''"
type="text"
placeholder="0x4AAAA..."
class="mt-1"
@update:model-value="$emit('update:turnstileSiteKey', String($event || '').trim() || null)"
/>
</div>
<div>
<div class="flex items-center justify-between">
<Label
for="turnstile-secret-key"
class="block text-sm font-medium"
>
Turnstile Secret Key
</Label>
<Button
v-if="turnstileSecretConfigured"
type="button"
variant="link"
size="sm"
class="h-auto p-0 text-xs"
:disabled="loading"
@click="$emit('clearTurnstileSecret')"
>
清空
</Button>
</div>
<Input
id="turnstile-secret-key"
:model-value="turnstileSecretKey"
type="password"
:placeholder="turnstileSecretConfigured ? '已配置,留空不修改' : '输入 Secret Key'"
class="mt-1"
autocomplete="new-password"
@update:model-value="$emit('update:turnstileSecretKey', String($event || ''))"
/>
</div>
<div>
<Label
for="turnstile-hostnames"
class="block text-sm font-medium"
>
允许的 Hostname
</Label>
<Input
id="turnstile-hostnames"
:model-value="turnstileAllowedHostnamesStr"
type="text"
placeholder="example.com, app.example.com"
class="mt-1"
@update:model-value="$emit('update:turnstileAllowedHostnamesStr', String($event || ''))"
/>
<p class="mt-1 text-xs text-muted-foreground">
留空则不额外校验 Cloudflare 返回的 hostname
</p>
</div>
</div>
</div>
</CardSection>
</template>
@@ -192,6 +283,11 @@ defineProps<{
rateLimitPerMinute: number
enableRegistration: boolean
passwordPolicyLevel: string
turnstileEnabled: boolean
turnstileSiteKey: string | null
turnstileSecretKey: string
turnstileSecretConfigured: boolean
turnstileAllowedHostnamesStr: string
autoDeleteExpiredKeys: boolean
enableFormatConversion: boolean
enableOpenaiImageSyncHeartbeat: boolean
@@ -205,6 +301,11 @@ defineEmits<{
'update:rateLimitPerMinute': [value: number]
'update:enableRegistration': [value: boolean]
'update:passwordPolicyLevel': [value: string]
'update:turnstileEnabled': [value: boolean]
'update:turnstileSiteKey': [value: string | null]
'update:turnstileSecretKey': [value: string]
'update:turnstileAllowedHostnamesStr': [value: string]
clearTurnstileSecret: []
'update:autoDeleteExpiredKeys': [value: boolean]
'update:enableFormatConversion': [value: boolean]
'update:enableOpenaiImageSyncHeartbeat': [value: boolean]

View File

@@ -15,6 +15,11 @@ export interface SystemConfig {
rate_limit_per_minute: number
enable_registration: boolean
password_policy_level: string
turnstile_enabled: boolean
turnstile_site_key: string | null
turnstile_secret_key: string
turnstile_secret_key_is_set: boolean
turnstile_allowed_hostnames: string[]
// 独立余额 Key 过期管理
auto_delete_expired_keys: boolean
// 格式转换
@@ -56,6 +61,10 @@ const CONFIG_KEYS = [
'rate_limit_per_minute',
'enable_registration',
'password_policy_level',
'turnstile_enabled',
'turnstile_site_key',
'turnstile_secret_key',
'turnstile_allowed_hostnames',
// 独立余额 Key 过期管理
'auto_delete_expired_keys',
// 格式转换
@@ -98,6 +107,11 @@ function createDefaultConfig(): SystemConfig {
rate_limit_per_minute: 0,
enable_registration: false,
password_policy_level: 'weak',
turnstile_enabled: false,
turnstile_site_key: null,
turnstile_secret_key: '',
turnstile_secret_key_is_set: false,
turnstile_allowed_hostnames: [],
// 独立余额 Key 过期管理
auto_delete_expired_keys: false,
// 格式转换
@@ -165,6 +179,11 @@ export function useSystemConfig() {
systemConfig.value.rate_limit_per_minute !== originalConfig.value.rate_limit_per_minute ||
systemConfig.value.enable_registration !== originalConfig.value.enable_registration ||
systemConfig.value.password_policy_level !== originalConfig.value.password_policy_level ||
systemConfig.value.turnstile_enabled !== originalConfig.value.turnstile_enabled ||
systemConfig.value.turnstile_site_key !== originalConfig.value.turnstile_site_key ||
systemConfig.value.turnstile_secret_key.trim() !== '' ||
JSON.stringify(systemConfig.value.turnstile_allowed_hostnames) !==
JSON.stringify(originalConfig.value.turnstile_allowed_hostnames) ||
systemConfig.value.auto_delete_expired_keys !== originalConfig.value.auto_delete_expired_keys ||
systemConfig.value.enable_format_conversion !== originalConfig.value.enable_format_conversion ||
systemConfig.value.enable_openai_image_sync_heartbeat !== originalConfig.value.enable_openai_image_sync_heartbeat
@@ -233,12 +252,27 @@ export function useSystemConfig() {
},
})
const turnstileAllowedHostnamesStr = computed({
get: () => systemConfig.value.turnstile_allowed_hostnames.join(', '),
set: (val: string) => {
systemConfig.value.turnstile_allowed_hostnames = val
.split(',')
.map((s) => s.trim().toLowerCase())
.filter((s) => s.length > 0)
},
})
// 加载配置
async function loadSystemConfig() {
try {
for (const key of CONFIG_KEYS) {
try {
const response = await adminApi.getSystemConfig(key)
if (key === 'turnstile_secret_key') {
systemConfig.value.turnstile_secret_key = ''
systemConfig.value.turnstile_secret_key_is_set = !!response.is_set
continue
}
if (response.value !== null && response.value !== undefined) {
; (systemConfig.value as Record<string, unknown>)[key] = response.value
}
@@ -337,6 +371,21 @@ export function useSystemConfig() {
value: systemConfig.value.password_policy_level,
description: '密码策略等级',
},
{
key: 'turnstile_enabled',
value: systemConfig.value.turnstile_enabled,
description: 'Cloudflare Turnstile 注册人机验证开关',
},
{
key: 'turnstile_site_key',
value: systemConfig.value.turnstile_site_key?.trim() || null,
description: 'Cloudflare Turnstile 站点 Key',
},
{
key: 'turnstile_allowed_hostnames',
value: systemConfig.value.turnstile_allowed_hostnames,
description: 'Cloudflare Turnstile 允许的 hostname 列表',
},
{
key: 'auto_delete_expired_keys',
value: systemConfig.value.auto_delete_expired_keys,
@@ -353,6 +402,14 @@ export function useSystemConfig() {
description: '同步生图心跳开关:开启后外层 HTTP 状态固定为 200上游失败写入响应体',
},
]
const turnstileSecret = systemConfig.value.turnstile_secret_key.trim()
if (turnstileSecret) {
configItems.push({
key: 'turnstile_secret_key',
value: turnstileSecret,
description: 'Cloudflare Turnstile Secret Key',
})
}
await Promise.all(
configItems.map((item) =>
@@ -364,6 +421,17 @@ export function useSystemConfig() {
originalConfig.value.rate_limit_per_minute = systemConfig.value.rate_limit_per_minute
originalConfig.value.enable_registration = systemConfig.value.enable_registration
originalConfig.value.password_policy_level = systemConfig.value.password_policy_level
originalConfig.value.turnstile_enabled = systemConfig.value.turnstile_enabled
originalConfig.value.turnstile_site_key = systemConfig.value.turnstile_site_key?.trim() || null
originalConfig.value.turnstile_allowed_hostnames = [
...systemConfig.value.turnstile_allowed_hostnames,
]
if (turnstileSecret) {
systemConfig.value.turnstile_secret_key = ''
systemConfig.value.turnstile_secret_key_is_set = true
originalConfig.value.turnstile_secret_key = ''
originalConfig.value.turnstile_secret_key_is_set = true
}
originalConfig.value.auto_delete_expired_keys =
systemConfig.value.auto_delete_expired_keys
originalConfig.value.enable_format_conversion =
@@ -380,6 +448,29 @@ export function useSystemConfig() {
}
}
async function clearTurnstileSecret() {
basicConfigLoading.value = true
try {
await adminApi.updateSystemConfig(
'turnstile_secret_key',
'',
'Cloudflare Turnstile Secret Key'
)
systemConfig.value.turnstile_secret_key = ''
systemConfig.value.turnstile_secret_key_is_set = false
if (originalConfig.value) {
originalConfig.value.turnstile_secret_key = ''
originalConfig.value.turnstile_secret_key_is_set = false
}
success('Turnstile 密钥已清空')
} catch (err) {
error('清空 Turnstile 密钥失败')
log.error('清空 Turnstile 密钥失败:', err)
} finally {
basicConfigLoading.value = false
}
}
async function saveLogConfig() {
logConfigLoading.value = true
try {
@@ -559,6 +650,7 @@ export function useSystemConfig() {
maxRequestBodySizeKB,
maxResponseBodySizeKB,
sensitiveHeadersStr,
turnstileAllowedHostnamesStr,
// 加载函数
loadSystemConfig,
loadSystemVersion,
@@ -566,6 +658,7 @@ export function useSystemConfig() {
saveSiteInfo,
saveProxyConfig,
saveBasicConfig,
clearTurnstileSecret,
saveLogConfig,
saveCleanupConfig,
handleAutoCleanupToggle,