feat(captcha): 打通人机验证 11 个场景的前后端完整链路
- 统一验证入口 aksrt_require_captcha(),返回 true|WP_Error,启用场景配置缺失时 fail-closed 拒绝 - 前端公共 API 重构为 window.AKSRTCaptcha(verify/collect/reset/run),单飞锁防并发重复验证,一次性 proof 绑定浏览器 cookie+场景+服务商+主机名 - challenge/exchange REST 端点:challenge 原子占用防票据放大厂商调用,增加频率限制 - 新增 OAuth bind pass:服务端签发 5 分钟一次性令牌,绑定用户+服务商+浏览器,微信扫码延迟消费 - 覆盖场景:评论、登录、注册、绑定第三方、绑定手机、找回密码、修改密码、更改邮箱、资料下载、签到、前台投稿 - 移除 Service 层重复验证与 _auth_code_verified/_defer_login 等客户端可伪造标志,删除下载共享 pass 死代码 - 修复腾讯 TC3 签名 scope、Cloudflare action/cdata/hostname 严格校验、阿里云验签错误码映射 - 后台 Schema 保存对 secret 字段做掩码/保留合并;导入导出过滤 aksrt_captcha_config - 重建 public bundle 与 admin SPA;新增 captcha 运行时/安全/业务契约测试与 harness
This commit is contained in:
1 parent
d2e95acd75
commit
ff4ec1eb43
45 files changed
+2694
-1435
No files matched your search
@@ -3,6 +3,12 @@ export const schemaApiMethods = {
|
||||
getSchemaPage(domain, section) {
|
||||
return this.request(`/settings/schema/${this.normalizeDomainKey(domain)}/${encodeURIComponent(section)}`)
|
||||
},
|
||||
testCaptcha(provider, token) {
|
||||
return this.request('/captcha/test', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ provider, token }),
|
||||
})
|
||||
},
|
||||
saveSchemaPage(domain, section, data, version = '1.0.0') {
|
||||
return this.request(`/settings/schema/${this.normalizeDomainKey(domain)}/${encodeURIComponent(section)}`, {
|
||||
method: 'POST',
|
||||
|
||||
@@ -40,9 +40,11 @@
|
||||
v-model:value="model.access_key_secret"
|
||||
type="password"
|
||||
show-password-on="click"
|
||||
autocomplete="current-password"
|
||||
autocomplete="new-password"
|
||||
placeholder="阿里云 RAM 用户 AccessKey Secret"
|
||||
@update:value="onSecretInput('access_key_secret', $event)"
|
||||
/>
|
||||
<n-button v-if="model.access_key_secret === '***'" size="tiny" type="error" ghost style="margin-left:8px" @click="clearSecret('access_key_secret')">清除</n-button>
|
||||
</n-form-item>
|
||||
<n-alert type="warning" style="margin-bottom: 8px;">
|
||||
请使用 RAM 子账号密钥并授权 <code>AliyunYundunAFSFullAccess</code> 权限,切勿使用主账号密钥。
|
||||
@@ -58,6 +60,16 @@
|
||||
import { computed } from 'vue'
|
||||
|
||||
const model = defineModel({ type: Object, default: () => ({}) })
|
||||
const emit = defineEmits(['clear-secret', 'restore-secret'])
|
||||
|
||||
function clearSecret(field) {
|
||||
emit('clear-secret', field)
|
||||
model.value = { ...model.value, [field]: '' }
|
||||
}
|
||||
|
||||
function onSecretInput(field, value) {
|
||||
if (value !== '') emit('restore-secret', field)
|
||||
}
|
||||
|
||||
const isConfigured = computed(() => {
|
||||
const c = model.value
|
||||
|
||||
@@ -5,10 +5,8 @@
|
||||
<n-input v-model:value="model.site_key" placeholder="Cloudflare Turnstile Site Key" autocomplete="off" />
|
||||
</n-form-item>
|
||||
<n-form-item label="Secret Key">
|
||||
<n-input v-model:value="model.secret_key" type="password" show-password-on="click" autocomplete="current-password" />
|
||||
</n-form-item>
|
||||
<n-form-item label="自定义域名">
|
||||
<n-input v-model:value="model.domain" placeholder="留空使用默认域名" autocomplete="url" />
|
||||
<n-input v-model:value="model.secret_key" type="password" show-password-on="click" autocomplete="new-password" @update:value="onSecretInput('secret_key', $event)" />
|
||||
<n-button v-if="model.secret_key === '***'" size="tiny" type="error" ghost style="margin-left:8px" @click="clearSecret('secret_key')">清除</n-button>
|
||||
</n-form-item>
|
||||
<n-form-item label="主题">
|
||||
<n-select v-model:value="model.theme" :options="themeOptions" clearable />
|
||||
@@ -26,6 +24,16 @@
|
||||
import { computed } from 'vue'
|
||||
|
||||
const model = defineModel({ type: Object, default: () => ({}) })
|
||||
const emit = defineEmits(['clear-secret', 'restore-secret'])
|
||||
|
||||
function clearSecret(field) {
|
||||
emit('clear-secret', field)
|
||||
model.value = { ...model.value, [field]: '' }
|
||||
}
|
||||
|
||||
function onSecretInput(field, value) {
|
||||
if (value !== '') emit('restore-secret', field)
|
||||
}
|
||||
|
||||
const isConfigured = computed(() => {
|
||||
const c = model.value
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
<n-input v-model:value="model.captcha_id" placeholder="极验 GeeTest CaptchaID" autocomplete="off" />
|
||||
</n-form-item>
|
||||
<n-form-item label="Captcha Key">
|
||||
<n-input v-model:value="model.captcha_key" type="password" show-password-on="click" autocomplete="current-password" />
|
||||
<n-input v-model:value="model.captcha_key" type="password" show-password-on="click" autocomplete="new-password" @update:value="onSecretInput('captcha_key', $event)" />
|
||||
<n-button v-if="model.captcha_key === '***'" size="tiny" type="error" ghost style="margin-left:8px" @click="clearSecret('captcha_key')">清除</n-button>
|
||||
</n-form-item>
|
||||
<n-tag v-if="isConfigured" type="success" size="small">✓ 已完整配置</n-tag>
|
||||
<n-tag v-else type="warning" size="small">未完成配置</n-tag>
|
||||
@@ -17,6 +18,16 @@
|
||||
import { computed } from 'vue'
|
||||
|
||||
const model = defineModel({ type: Object, default: () => ({ product: 'bind' }) })
|
||||
const emit = defineEmits(['clear-secret', 'restore-secret'])
|
||||
|
||||
function clearSecret(field) {
|
||||
emit('clear-secret', field)
|
||||
model.value = { ...model.value, [field]: '' }
|
||||
}
|
||||
|
||||
function onSecretInput(field, value) {
|
||||
if (value !== '') emit('restore-secret', field)
|
||||
}
|
||||
|
||||
const isConfigured = computed(() => {
|
||||
const c = model.value
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
<n-input v-model:value="model.secret_id" placeholder="腾讯云 API SecretId" autocomplete="off" />
|
||||
</n-form-item>
|
||||
<n-form-item label="SecretKey">
|
||||
<n-input v-model:value="model.secret_key" type="text" placeholder="腾讯云 API SecretKey" autocomplete="off" show-password-on="click" />
|
||||
<n-input v-model:value="model.secret_key" type="password" placeholder="腾讯云 API SecretKey" autocomplete="new-password" show-password-on="click" @update:value="onSecretInput('secret_key', $event)" />
|
||||
<n-button v-if="model.secret_key === '***'" size="tiny" type="error" ghost style="margin-left:8px" @click="clearSecret('secret_key')">清除</n-button>
|
||||
</n-form-item>
|
||||
|
||||
<n-divider title-placement="left">验证码密钥</n-divider>
|
||||
@@ -17,7 +18,8 @@
|
||||
<n-input v-model:value="model.app_id" placeholder="从验证码控制台获取 CaptchaAppId" autocomplete="off" />
|
||||
</n-form-item>
|
||||
<n-form-item label="AppSecretKey">
|
||||
<n-input v-model:value="model.app_secret_key" type="text" placeholder="从验证码控制台获取 AppSecretKey" autocomplete="off" show-password-on="click" />
|
||||
<n-input v-model:value="model.app_secret_key" type="password" placeholder="从验证码控制台获取 AppSecretKey" autocomplete="new-password" show-password-on="click" @update:value="onSecretInput('app_secret_key', $event)" />
|
||||
<n-button v-if="model.app_secret_key === '***'" size="tiny" type="error" ghost style="margin-left:8px" @click="clearSecret('app_secret_key')">清除</n-button>
|
||||
<template #feedback>请从「验证管理 → 基础配置」完整复制,注意区分 I/l/1/O/0</template>
|
||||
</n-form-item>
|
||||
|
||||
@@ -34,6 +36,16 @@
|
||||
import { computed } from 'vue'
|
||||
|
||||
const model = defineModel({ type: Object, default: () => ({}) })
|
||||
const emit = defineEmits(['clear-secret', 'restore-secret'])
|
||||
|
||||
function clearSecret(field) {
|
||||
emit('clear-secret', field)
|
||||
model.value = { ...model.value, [field]: '' }
|
||||
}
|
||||
|
||||
function onSecretInput(field, value) {
|
||||
if (value !== '') emit('restore-secret', field)
|
||||
}
|
||||
|
||||
const isConfigured = computed(() => {
|
||||
const c = model.value
|
||||
|
||||
@@ -19,28 +19,28 @@
|
||||
v-if="isPermissionsLogin"
|
||||
#field-tencent="{ value, setValue }"
|
||||
>
|
||||
<TencentConfig :model-value="value || {}" @update:model-value="setValue" />
|
||||
<TencentConfig :model-value="value || {}" @update:model-value="setValue" @clear-secret="markCaptchaSecretForClear('tencent', $event)" @restore-secret="unmarkCaptchaSecretForClear('tencent', $event)" />
|
||||
</template>
|
||||
|
||||
<template
|
||||
v-if="isPermissionsLogin"
|
||||
#field-aliyun="{ value, setValue }"
|
||||
>
|
||||
<AliyunConfig :model-value="value || {}" @update:model-value="setValue" />
|
||||
<AliyunConfig :model-value="value || {}" @update:model-value="setValue" @clear-secret="markCaptchaSecretForClear('aliyun', $event)" @restore-secret="unmarkCaptchaSecretForClear('aliyun', $event)" />
|
||||
</template>
|
||||
|
||||
<template
|
||||
v-if="isPermissionsLogin"
|
||||
#field-cloudflare="{ value, setValue }"
|
||||
>
|
||||
<CloudflareConfig :model-value="value || {}" @update:model-value="setValue" />
|
||||
<CloudflareConfig :model-value="value || {}" @update:model-value="setValue" @clear-secret="markCaptchaSecretForClear('cloudflare', $event)" @restore-secret="unmarkCaptchaSecretForClear('cloudflare', $event)" />
|
||||
</template>
|
||||
|
||||
<template
|
||||
v-if="isPermissionsLogin"
|
||||
#field-geetest="{ value, setValue }"
|
||||
>
|
||||
<GeetestConfig :model-value="value || {}" @update:model-value="setValue" />
|
||||
<GeetestConfig :model-value="value || {}" @update:model-value="setValue" @clear-secret="markCaptchaSecretForClear('geetest', $event)" @restore-secret="unmarkCaptchaSecretForClear('geetest', $event)" />
|
||||
</template>
|
||||
|
||||
<template
|
||||
@@ -131,6 +131,7 @@ const showTestModal = ref(false)
|
||||
const testLoading = ref(false)
|
||||
const testResult = ref(null)
|
||||
const serverSchema = ref(null)
|
||||
const captchaSecretsToClear = ref(new Set())
|
||||
|
||||
/** Resolve domain/section from props or route meta (canonicalDomain + section, with legacy fallback) */
|
||||
const resolvedDomain = computed(() => normalizeSchemaDomain(props.domain || route.meta?.canonicalDomain || route.meta?.schemaDomain || resolveDomainFromPath(route.path)))
|
||||
@@ -210,8 +211,12 @@ const {
|
||||
}
|
||||
})
|
||||
if (clearSecrets.length) payload.__clear_secrets = clearSecrets
|
||||
if (isPermissionsLogin.value && captchaSecretsToClear.value.size) {
|
||||
payload.__clear_secrets = [...(payload.__clear_secrets || []), ...captchaSecretsToClear.value]
|
||||
}
|
||||
const response = await api.saveSchemaPage(resolvedDomain.value, resolvedSection.value, payload)
|
||||
clearSecrets.forEach((key) => { data[key] = '' })
|
||||
if (isPermissionsLogin.value) captchaSecretsToClear.value = new Set()
|
||||
return response
|
||||
},
|
||||
defaults: defaults.value,
|
||||
@@ -230,10 +235,26 @@ async function handleSave() {
|
||||
|
||||
function handleReload() {
|
||||
closeTestModal()
|
||||
captchaSecretsToClear.value = new Set()
|
||||
reload()
|
||||
message.info('已放弃更改')
|
||||
}
|
||||
|
||||
function markCaptchaSecretForClear(provider, field) {
|
||||
const path = `${provider}.${field}`
|
||||
const next = new Set(captchaSecretsToClear.value)
|
||||
next.add(path)
|
||||
captchaSecretsToClear.value = next
|
||||
}
|
||||
|
||||
function unmarkCaptchaSecretForClear(provider, field) {
|
||||
const path = `${provider}.${field}`
|
||||
if (!captchaSecretsToClear.value.has(path)) return
|
||||
const next = new Set(captchaSecretsToClear.value)
|
||||
next.delete(path)
|
||||
captchaSecretsToClear.value = next
|
||||
}
|
||||
|
||||
function getCaptchaProviderConfig(provider) {
|
||||
if (!formData.value || !provider) return {}
|
||||
const cfg = formData.value[provider]
|
||||
@@ -256,9 +277,8 @@ function handleCaptchaTest() {
|
||||
message.warning(`请先填写必填字段: ${missing.join(', ')}`)
|
||||
return
|
||||
}
|
||||
|
||||
if (provider === 'tencent') {
|
||||
loadTencentTest(cfg)
|
||||
if (dirty.value) {
|
||||
message.warning('请先保存当前配置,再执行真实服务端验签。')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -267,52 +287,20 @@ function handleCaptchaTest() {
|
||||
testLoading.value = true
|
||||
|
||||
nextTick(() => {
|
||||
createTestIframe(provider, cfg)
|
||||
loadProviderSDK(provider, cfg)
|
||||
})
|
||||
}
|
||||
|
||||
function loadTencentTest(cfg) {
|
||||
const sdkSrc = 'https://turing.captcha.qcloud.com/TJCaptcha.js'
|
||||
|
||||
if (typeof window.TencentCaptcha !== 'undefined') {
|
||||
try {
|
||||
new window.TencentCaptcha(cfg.app_id, function(res) {
|
||||
if (res.ret === 0) message.success('腾讯云验证码测试成功!配置正确。')
|
||||
else message.warning('验证已取消。')
|
||||
}, { userLanguage: 'zh-cn' }).show()
|
||||
} catch (error) {
|
||||
message.error('初始化失败: ' + error.message)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const existingScript = document.querySelector('script[src="' + sdkSrc + '"]')
|
||||
if (existingScript) {
|
||||
existingScript.addEventListener('load', function() { loadTencentTest(cfg) }, { once: true })
|
||||
return
|
||||
}
|
||||
|
||||
const script = document.createElement('script')
|
||||
script.src = sdkSrc
|
||||
script.onload = function () {
|
||||
try {
|
||||
new window.TencentCaptcha(cfg.app_id, function(res) {
|
||||
if (res.ret === 0) message.success('腾讯云验证码测试成功!')
|
||||
else message.warning('验证已取消。')
|
||||
}, { userLanguage: 'zh-cn' }).show()
|
||||
} catch (error) {
|
||||
message.error('初始化失败: ' + error.message)
|
||||
}
|
||||
}
|
||||
script.onerror = function () { message.error('加载腾讯验证码SDK失败') }
|
||||
document.head.appendChild(script)
|
||||
}
|
||||
|
||||
function createTestIframe(provider, cfg) {
|
||||
setTimeout(() => {
|
||||
async function submitCaptchaTest(provider, token) {
|
||||
testLoading.value = true
|
||||
try {
|
||||
const response = await api.testCaptcha(provider, token)
|
||||
testResult.value = { success: true, message: response?.message || '服务端同源验签通过' }
|
||||
} catch (error) {
|
||||
testResult.value = { success: false, message: error?.message || '服务端验签失败' }
|
||||
} finally {
|
||||
testLoading.value = false
|
||||
loadProviderSDK(provider, cfg)
|
||||
}, 100)
|
||||
}
|
||||
}
|
||||
|
||||
function loadProviderSDK(provider, cfg) {
|
||||
@@ -331,28 +319,37 @@ function loadProviderSDK(provider, cfg) {
|
||||
case 'tencent':
|
||||
script.src = 'https://turing.captcha.qcloud.com/TJCaptcha.js'
|
||||
script.onload = function () {
|
||||
testLoading.value = false
|
||||
try {
|
||||
new window.TencentCaptcha(cfg.app_id, function(res) {
|
||||
testResult.value = { success: res.ret === 0, message: res.ret === 0 ? '验证成功!' : '验证已取消。' }
|
||||
if (res.ret === 0) {
|
||||
submitCaptchaTest('tencent', {
|
||||
tencent_ticket: res.ticket || '',
|
||||
tencent_randstr: res.randstr || '',
|
||||
})
|
||||
} else {
|
||||
testResult.value = { success: false, message: '验证已取消。' }
|
||||
}
|
||||
}, { userLanguage: 'zh-cn' }).show()
|
||||
} catch (error) {
|
||||
testResult.value = { success: false, message: '初始化失败: ' + error.message }
|
||||
}
|
||||
}
|
||||
script.onerror = function () { testResult.value = { success: false, message: '加载SDK失败' } }
|
||||
script.onerror = function () { testLoading.value = false; testResult.value = { success: false, message: '加载SDK失败' } }
|
||||
break
|
||||
|
||||
case 'cloudflare':
|
||||
window.onTurnstileTestLoad = function() {
|
||||
testLoading.value = false
|
||||
try {
|
||||
window.turnstile.render('#captcha-test-box', {
|
||||
sitekey: cfg.site_key,
|
||||
theme: 'auto',
|
||||
theme: cfg.theme || 'auto',
|
||||
size: cfg.size || 'normal',
|
||||
callback: function() {
|
||||
testResult.value = { success: true, message: '验证成功!' }
|
||||
message.success('Cloudflare Turnstile 测试成功!')
|
||||
setTimeout(closeTestModal, 1500)
|
||||
action: 'aksrt-admin-test',
|
||||
cdata: 'admin-captcha-test',
|
||||
callback: function(token) {
|
||||
submitCaptchaTest('cloudflare', { cf_turnstile_response: token })
|
||||
},
|
||||
'error-callback': function() {
|
||||
testResult.value = { success: false, message: '验证失败' }
|
||||
@@ -363,19 +360,28 @@ function loadProviderSDK(provider, cfg) {
|
||||
}
|
||||
}
|
||||
script.src = 'https://challenges.cloudflare.com/turnstile/v0/api.js?onload=onTurnstileTestLoad'
|
||||
script.onerror = function () { testResult.value = { success: false, message: '加载SDK失败' } }
|
||||
script.onerror = function () { testLoading.value = false; testResult.value = { success: false, message: '加载SDK失败' } }
|
||||
break
|
||||
|
||||
case 'geetest':
|
||||
script.src = 'https://static.geetest.com/v4/gt4.js'
|
||||
script.onload = function () {
|
||||
testLoading.value = false
|
||||
try {
|
||||
window.initGeetest4({ captchaId: cfg.captcha_id, product: cfg.product || 'float' }, function(captchaObj) {
|
||||
window.__gtTestCaptcha = captchaObj
|
||||
const onSuccess = function() {
|
||||
testResult.value = { success: true, message: '验证成功!' }
|
||||
message.success('极验 GeeTest 测试成功!')
|
||||
setTimeout(closeTestModal, 1500)
|
||||
const result = captchaObj.getValidate ? captchaObj.getValidate() : null
|
||||
if (!result) {
|
||||
testResult.value = { success: false, message: '未取得极验验证参数' }
|
||||
return
|
||||
}
|
||||
submitCaptchaTest('geetest', {
|
||||
geetest_lot_number: result.lot_number || '',
|
||||
geetest_captcha_output: result.captcha_output || '',
|
||||
geetest_pass_token: result.pass_token || '',
|
||||
geetest_gen_time: result.gen_time || '',
|
||||
})
|
||||
}
|
||||
const onError = function(error) {
|
||||
testResult.value = { success: false, message: '出错: ' + (error.msg || '?') }
|
||||
@@ -393,22 +399,21 @@ function loadProviderSDK(provider, cfg) {
|
||||
testResult.value = { success: false, message: '初始化失败: ' + error.message }
|
||||
}
|
||||
}
|
||||
script.onerror = function () { testResult.value = { success: false, message: '加载SDK失败' } }
|
||||
script.onerror = function () { testLoading.value = false; testResult.value = { success: false, message: '加载SDK失败' } }
|
||||
break
|
||||
|
||||
case 'aliyun':
|
||||
window.AliyunCaptchaConfig = { region: cfg.region || 'cn', prefix: cfg.prefix }
|
||||
script.src = 'https://o.alicdn.com/captcha-frontend/aliyunCaptcha/AliyunCaptcha.js'
|
||||
script.onload = function () {
|
||||
testLoading.value = false
|
||||
try {
|
||||
window.initAliyunCaptcha({
|
||||
SceneId: cfg.scene_id,
|
||||
mode: 'embed',
|
||||
element: '#captcha-test-box',
|
||||
success: function() {
|
||||
testResult.value = { success: true, message: '验证成功!' }
|
||||
message.success('阿里云测试成功!')
|
||||
setTimeout(closeTestModal, 1500)
|
||||
success: function(captchaVerifyParam) {
|
||||
submitCaptchaTest('aliyun', { aliyun_captcha_verify_param: String(captchaVerifyParam || '') })
|
||||
},
|
||||
fail: function(result) {
|
||||
testResult.value = { success: false, message: String(result) }
|
||||
@@ -421,7 +426,7 @@ function loadProviderSDK(provider, cfg) {
|
||||
testResult.value = { success: false, message: '初始化失败: ' + error.message }
|
||||
}
|
||||
}
|
||||
script.onerror = function () { testResult.value = { success: false, message: '加载SDK失败' } }
|
||||
script.onerror = function () { testLoading.value = false; testResult.value = { success: false, message: '加载SDK失败' } }
|
||||
break
|
||||
|
||||
default:
|
||||
|
||||
@@ -15,10 +15,7 @@ export const permissionsSchemas = {
|
||||
defaults: {
|
||||
enabled: false,
|
||||
active_provider: '',
|
||||
enabled_scenarios: [
|
||||
'comment', 'login', 'register', 'bind', 'bind_phone',
|
||||
'lostpassword', 'change_password', 'change_email', 'download', 'signin',
|
||||
],
|
||||
enabled_scenarios: [],
|
||||
tencent: {},
|
||||
aliyun: {},
|
||||
cloudflare: {},
|
||||
@@ -81,10 +78,10 @@ export const permissionsSchemas = {
|
||||
alertContent: '下方会渲染当前验证码服务商的专属配置面板。保存仍通过统一 schema contract 进行。',
|
||||
dependsOn: { key: 'enabled', value: true },
|
||||
},
|
||||
{ key: 'tencent', label: '腾讯云配置', type: 'custom', dependsOn: { key: 'active_provider', value: 'tencent' } },
|
||||
{ key: 'aliyun', label: '阿里云配置', type: 'custom', dependsOn: { key: 'active_provider', value: 'aliyun' } },
|
||||
{ key: 'cloudflare', label: 'Cloudflare 配置', type: 'custom', dependsOn: { key: 'active_provider', value: 'cloudflare' } },
|
||||
{ key: 'geetest', label: '极验配置', type: 'custom', dependsOn: { key: 'active_provider', value: 'geetest' } },
|
||||
{ key: 'tencent', label: '腾讯云配置', type: 'custom', default: { secret_id: '', secret_key: '', app_id: '', app_secret_key: '' }, secretFields: ['secret_key', 'app_secret_key'], dependsOn: { key: 'active_provider', value: 'tencent' } },
|
||||
{ key: 'aliyun', label: '阿里云配置', type: 'custom', default: { region: 'cn', prefix: '', scene_id: '', access_key_id: '', access_key_secret: '' }, secretFields: ['access_key_secret'], dependsOn: { key: 'active_provider', value: 'aliyun' } },
|
||||
{ key: 'cloudflare', label: 'Cloudflare 配置', type: 'custom', default: { site_key: '', secret_key: '', theme: 'auto', size: 'normal' }, secretFields: ['secret_key'], dependsOn: { key: 'active_provider', value: 'cloudflare' } },
|
||||
{ key: 'geetest', label: '极验配置', type: 'custom', default: { captcha_id: '', captcha_key: '', product: 'bind' }, secretFields: ['captcha_key'], dependsOn: { key: 'active_provider', value: 'geetest' } },
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
Reference in new issue
Block a user