fix:修复Turnstile真人验证功能

This commit is contained in:
qian committed 2026-06-13 14:36:58 +08:00
1 parent e9b1b1827c
commit cea265a1dd
6 files changed
+108 -35

No files matched your search

+10 -1
View File
@@ -197,9 +197,18 @@ func (s *ConfigService) SetOption(ctx context.Context, key, value string) error
}
if count > 0 {
data := do.SysOptions{Value: value}
// Sync metadata from registry so that changes to IsPublic/Category/Description
// are reflected in the DB row on the next write (e.g. a setting newly marked
// as IsPublic will become visible to GetPublicOptions).
if def := GetSettingDef(key); def != nil {
data.Category = def.Category
data.Description = def.Label
data.IsPublic = def.IsPublic
}
_, err = dao.SysOptions.Ctx(ctx).
Where("key", key).
Data(do.SysOptions{Value: value}).
Data(data).
Update()
} else {
def := GetSettingDef(key)
+2 -2
View File
@@ -120,9 +120,9 @@ var Registry = []SettingDef{
{Key: "password_min_length", Type: SettingTypeInt, Default: "8", Category: "security",
Label: "密码最小长度", Validation: "min:6,max:32"},
{Key: "turnstile_enabled", Type: SettingTypeBool, Default: "false", Category: "security",
Label: "启用 Turnstile 人机验证"},
Label: "启用 Turnstile 人机验证", IsPublic: true},
{Key: "turnstile_site_key", Type: SettingTypeString, Default: "", Category: "security",
Label: "Turnstile Site Key", Sensitive: false},
Label: "Turnstile Site Key", Sensitive: false, IsPublic: true},
{Key: "turnstile_secret_key", Type: SettingTypeString, Default: "", Category: "security",
Label: "Turnstile Secret Key", Sensitive: true},
{Key: "captcha_expire_seconds", Type: SettingTypeInt, Default: "300", Category: "security",
+20 -15
View File
@@ -70,14 +70,16 @@ func (s *sTenant) Register(ctx context.Context, req *v1.TenantRegisterReq) (*v1.
return nil, err
}
} else {
// 人机验证:优先 Turnstile,否则滑块验证
if err := common.CheckTurnstileRequired(ctx, req.TurnstileToken); err != nil {
return nil, err
}
// Turnstile 未启用时,使用滑块验证
if err := common.CheckCaptchaRequired(ctx, "tenant_register", req.CaptchaKey, req.CaptchaX); err != nil {
return nil, err
// 人机验证:Turnstile 启用时只验 Turnstile,否则用滑块验证
turnstileEnabled := common.Config().GetBool(ctx, "turnstile_enabled")
if turnstileEnabled {
if err := common.CheckTurnstileRequired(ctx, req.TurnstileToken); err != nil {
return nil, err
}
} else {
if err := common.CheckCaptchaRequired(ctx, "tenant_register", req.CaptchaKey, req.CaptchaX); err != nil {
return nil, err
}
}
}
@@ -240,13 +242,16 @@ func (s *sTenant) Register(ctx context.Context, req *v1.TenantRegisterReq) (*v1.
func (s *sTenant) Login(ctx context.Context, req *v1.TenantLoginReq) (*v1.TenantLoginRes, error) {
account := strings.TrimSpace(req.Account)
// Check captcha if required
if err := common.CheckTurnstileRequired(ctx, req.TurnstileToken); err != nil {
return nil, err
}
if err := common.CheckCaptchaRequired(ctx, "tenant_login", req.CaptchaKey, req.CaptchaX); err != nil {
return nil, err
// Check captcha: Turnstile takes priority; slider captcha is fallback
turnstileEnabled := common.Config().GetBool(ctx, "turnstile_enabled")
if turnstileEnabled {
if err := common.CheckTurnstileRequired(ctx, req.TurnstileToken); err != nil {
return nil, err
}
} else {
if err := common.CheckCaptchaRequired(ctx, "tenant_login", req.CaptchaKey, req.CaptchaX); err != nil {
return nil, err
}
}
// Get client info early for login history recording
@@ -0,0 +1,27 @@
-- +goose Up
-- Sync is_public flag from code registry to DB rows.
-- Settings like turnstile_enabled/turnstile_site_key were marked IsPublic in the
-- registry but the DB rows still had is_public=false, causing GetPublicOptions
-- (WHERE is_public=true) to skip them and return stale defaults to the tenant frontend.
UPDATE sys_options SET is_public = true, updated_at = NOW()
WHERE key IN (
'site_name',
'site_description',
'register_enabled',
'register_email_verification',
'maintenance_message',
'maintenance_duration',
'api_maintenance_enabled',
'oauth_auto_register',
'oauth_github_enabled',
'oauth_google_enabled',
'turnstile_enabled',
'turnstile_site_key',
'agreement_enabled'
) AND is_public = false;
-- +goose Down
-- No-op: reverting is_public to false would break public settings again,
-- so we leave the corrected values in place.
SELECT 1;
+2 -1
View File
@@ -114,13 +114,14 @@ export const useTenantAuthStore = defineStore('tenant-auth', () => {
persist()
}
async function login(account: string, password: string, type: 'ram' | 'admin', captcha?: { captchaKey: string; captchaX: number }): Promise<any> {
async function login(account: string, password: string, type: 'ram' | 'admin', captcha?: { captchaKey: string; captchaX: number; turnstileToken?: string }): Promise<any> {
const { data } = await request.post('/tenant/auth/login', {
account,
password,
type,
captcha_key: captcha?.captchaKey,
captcha_x: captcha?.captchaX,
turnstile_token: captcha?.turnstileToken,
})
if (data.data?.totp_required) {
return data.data
+47 -16
View File
@@ -36,10 +36,13 @@ const provisionalToken = ref('')
const totpCode = ref('')
const totpLoading = ref(false)
// Captcha state — always required
// Captcha state
const captcha = reactive({ captchaKey: '', captchaX: 0 })
const turnstileToken = ref('')
const turnstileToken = ref('')
const captchaRef = ref<InstanceType<typeof SlideCaptcha> | null>(null)
const turnstileRef = ref<InstanceType<typeof Turnstile> | null>(null)
const useTurnstile = computed(() => settings.value['turnstile_enabled'] === true)
const emailInput = ref<HTMLInputElement | null>(null)
const accountInput = ref<HTMLInputElement | null>(null)
@@ -124,15 +127,24 @@ function validateRam(): boolean {
async function handleAdminLogin() {
if (!validateAdmin()) return
if (!captcha.captchaKey || !captcha.captchaX) {
errorMsg.value = '请先完成滑块验证'
return
if (useTurnstile.value) {
if (!turnstileToken.value) {
errorMsg.value = '请先完成人机验证'
return
}
} else {
if (!captcha.captchaKey || !captcha.captchaX) {
errorMsg.value = '请先完成滑块验证'
return
}
}
loading.value = true
errorMsg.value = ''
try {
const captchaPayload = { captchaKey: captcha.captchaKey, captchaX: captcha.captchaX }
const captchaPayload = useTurnstile.value
? { captchaKey: '', captchaX: 0, turnstileToken: turnstileToken.value }
: { captchaKey: captcha.captchaKey, captchaX: captcha.captchaX }
const res = await authStore.login(adminForm.email, adminForm.password, 'admin', captchaPayload)
if (res?.totp_required) {
provisionalToken.value = res.provisional_token
@@ -142,12 +154,16 @@ async function handleAdminLogin() {
proceedAfterLogin()
} catch (err) {
const apiErr = extractApiError(err)
if (apiErr?.code === 10058) {
if (useTurnstile.value) {
turnstileRef.value?.reset()
errorMsg.value = apiErr?.message || '登录失败,请检查邮箱和密码'
} else if (apiErr?.code === 10058) {
errorMsg.value = '滑块验证失败,请重新拖动'
captchaRef.value?.resetCaptcha()
} else {
errorMsg.value = apiErr?.message || '登录失败,请检查邮箱和密码'
captchaRef.value?.resetCaptcha()
}
captchaRef.value?.resetCaptcha()
} finally {
loading.value = false
}
@@ -155,15 +171,24 @@ async function handleAdminLogin() {
async function handleRamLogin() {
if (!validateRam()) return
if (!captcha.captchaKey || !captcha.captchaX) {
errorMsg.value = '请先完成滑块验证'
return
if (useTurnstile.value) {
if (!turnstileToken.value) {
errorMsg.value = '请先完成人机验证'
return
}
} else {
if (!captcha.captchaKey || !captcha.captchaX) {
errorMsg.value = '请先完成滑块验证'
return
}
}
loading.value = true
errorMsg.value = ''
try {
const captchaPayload = { captchaKey: captcha.captchaKey, captchaX: captcha.captchaX }
const captchaPayload = useTurnstile.value
? { captchaKey: '', captchaX: 0, turnstileToken: turnstileToken.value }
: { captchaKey: captcha.captchaKey, captchaX: captcha.captchaX }
const res = await authStore.login(ramForm.account, ramForm.password, 'ram', captchaPayload)
if (res?.totp_required) {
provisionalToken.value = res.provisional_token
@@ -173,12 +198,16 @@ async function handleRamLogin() {
proceedAfterLogin()
} catch (err) {
const apiErr = extractApiError(err)
if (apiErr?.code === 10058) {
if (useTurnstile.value) {
turnstileRef.value?.reset()
errorMsg.value = apiErr?.message || '登录失败,请检查用户名和密码'
} else if (apiErr?.code === 10058) {
errorMsg.value = '滑块验证失败,请重新拖动'
captchaRef.value?.resetCaptcha()
} else {
errorMsg.value = apiErr?.message || '登录失败,请检查用户名和密码'
captchaRef.value?.resetCaptcha()
}
captchaRef.value?.resetCaptcha()
} finally {
loading.value = false
}
@@ -335,7 +364,8 @@ async function handleOAuthLogin(provider: string) {
</div>
<!-- Captcha -->
<SlideCaptcha ref="captchaRef" v-model="captcha" />
<Turnstile v-if="useTurnstile" ref="turnstileRef" v-model="turnstileToken" :site-key="settings['turnstile_site_key']" />
<SlideCaptcha v-else ref="captchaRef" v-model="captcha" />
<!-- Error Message -->
<transition name="slide-fade">
@@ -432,7 +462,8 @@ async function handleOAuthLogin(provider: string) {
</div>
<!-- Captcha -->
<SlideCaptcha ref="captchaRef" v-model="captcha" />
<Turnstile v-if="useTurnstile" ref="turnstileRef" v-model="turnstileToken" :site-key="settings['turnstile_site_key']" />
<SlideCaptcha v-else ref="captchaRef" v-model="captcha" />
<!-- Error Message -->
<transition name="slide-fade">