mirror of
https://github.com/certd/certd.git
synced 2026-04-14 20:40:53 +08:00
perf: 验证码支持 Cloudflare Turnstile ,谨慎启用,国内被墙了
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
<template>
|
||||
<div class="cf-turnstile">
|
||||
<div id="turnstile-container" class="cf-turnstile-container" :data-sitekey="siteKeyRef"></div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { nextTick, onMounted, onUnmounted, ref, watch } from "vue";
|
||||
|
||||
import { loadScript } from "vue-plugin-load-script";
|
||||
const loaded = ref(false);
|
||||
async function loadCaptchaScript() {
|
||||
await loadScript("https://challenges.cloudflare.com/turnstile/v0/api.js");
|
||||
loaded.value = true;
|
||||
}
|
||||
|
||||
defineOptions({
|
||||
name: "CfTurnstileCaptcha",
|
||||
});
|
||||
const emit = defineEmits(["update:modelValue", "change"]);
|
||||
const props = defineProps<{
|
||||
modelValue: any;
|
||||
captchaGet: () => Promise<any>;
|
||||
}>();
|
||||
const captchaRef = ref(null);
|
||||
const siteKeyRef = ref("");
|
||||
const widgetIdRef = ref(null);
|
||||
|
||||
onMounted(async () => {
|
||||
await loadCaptchaScript();
|
||||
await nextTick();
|
||||
const { siteKey } = await props.captchaGet();
|
||||
siteKeyRef.value = siteKey; //这里确定是string类型
|
||||
//@ts-ignore
|
||||
const widgetId = turnstile.render("#turnstile-container", {
|
||||
sitekey: siteKey,
|
||||
size: "flexible",
|
||||
callback: function (token: string) {
|
||||
console.log("turnstile success:", token);
|
||||
emitChange({
|
||||
token: token,
|
||||
});
|
||||
},
|
||||
});
|
||||
widgetIdRef.value = widgetId;
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
//@ts-ignore
|
||||
if (turnstile && widgetIdRef.value) {
|
||||
//@ts-ignore
|
||||
turnstile.remove(widgetIdRef.value);
|
||||
}
|
||||
});
|
||||
|
||||
function checkExpired() {
|
||||
//@ts-ignore
|
||||
if (turnstile && widgetIdRef.value) {
|
||||
//@ts-ignore
|
||||
return turnstile.isExpired(widgetIdRef.value);
|
||||
}
|
||||
}
|
||||
|
||||
function emitChange(value: any) {
|
||||
emit("update:modelValue", value);
|
||||
emit("change", value);
|
||||
}
|
||||
function reset() {
|
||||
// 重置验证码
|
||||
//@ts-ignore
|
||||
if (turnstile && widgetIdRef.value) {
|
||||
//@ts-ignore
|
||||
turnstile.reset(widgetIdRef.value);
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => {
|
||||
return props.modelValue;
|
||||
},
|
||||
value => {
|
||||
if (value == null) {
|
||||
reset();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
defineExpose({
|
||||
reset,
|
||||
});
|
||||
</script>
|
||||
<style lang="less">
|
||||
.cf-turnstile-container {
|
||||
iframe {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -11,8 +11,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { defineEmits, defineExpose, defineProps, ref, watch } from "vue";
|
||||
import { nanoid } from "nanoid";
|
||||
import { ref, watch } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: any;
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { onMounted, defineProps, defineEmits, ref, onUnmounted, Ref, watch } from "vue";
|
||||
import { notification } from "ant-design-vue";
|
||||
import { ref, Ref, watch } from "vue";
|
||||
|
||||
import { loadScript } from "vue-plugin-load-script";
|
||||
const loaded = ref(false);
|
||||
|
||||
@@ -455,4 +455,10 @@ h6 {
|
||||
background: #ffecb3;
|
||||
color: #f57c00;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-tag{
|
||||
.fs-icon{
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
@@ -9,9 +9,11 @@
|
||||
<addon-selector v-model:model-value="formState.public.captchaAddonId" addon-type="captcha" from="sys" @selected-change="onAddonChanged" />
|
||||
</a-form-item>
|
||||
<a-form-item v-if="formState.public.captchaType === settingsStore.sysPublic.captchaType" :label="t('certd.sys.setting.captchaTest')">
|
||||
<div class="flex">
|
||||
<CaptchaInput v-model:model-value="captchaTestForm.captcha" class="w-50%"></CaptchaInput>
|
||||
<a-button class="ml-2" type="primary" @click="doCaptchaValidate">后端验证</a-button>
|
||||
<div class="flex items-center">
|
||||
<CaptchaInput v-model:model-value="captchaTestForm.captcha" class="w-60%"></CaptchaInput>
|
||||
<a-button class="ml-2 mr-2" type="primary" @click="doCaptchaValidate">后端验证</a-button>
|
||||
<a-tag v-if="captchaTestForm.pass" color="green" class="flex items-center"> <fs-icon icon="material-symbols:check-circle-rounded"></fs-icon> 校验通过</a-tag>
|
||||
<a-tag v-else class="flex items-center"> <fs-icon icon="material-symbols:info-rounded"></fs-icon> 请先点击验证</a-tag>
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
|
||||
@@ -87,7 +87,7 @@ export default ({ command, mode }) => {
|
||||
host: "0.0.0.0",
|
||||
port: 3008,
|
||||
fs: devServerFs,
|
||||
allowedHosts: ["localhost", "127.0.0.1", "yfy.docmirror.cn"],
|
||||
allowedHosts: ["localhost", "127.0.0.1", "yfy.docmirror.cn", "docmirror.top", "*"],
|
||||
proxy: {
|
||||
// with options
|
||||
"/api": {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { BaseController, Constants, SysSettingsService } from "@certd/lib-server";
|
||||
import { ALL, Body, Controller, Inject, Post, Provide, Query } from "@midwayjs/core";
|
||||
import { ALL, Body, Controller, Inject, Post, Provide, Query, RequestIP } from "@midwayjs/core";
|
||||
import { Rule, RuleType } from "@midwayjs/validate";
|
||||
import { CaptchaService } from "../../modules/basic/service/captcha-service.js";
|
||||
import { CodeService } from "../../modules/basic/service/code-service.js";
|
||||
@@ -62,7 +62,8 @@ export class BasicController extends BaseController {
|
||||
@Post('/sendSmsCode', { summary: Constants.per.guest })
|
||||
public async sendSmsCode(
|
||||
@Body(ALL)
|
||||
body: SmsCodeReq
|
||||
body: SmsCodeReq,
|
||||
@RequestIP() remoteIp: string
|
||||
) {
|
||||
const opts = {
|
||||
verificationType: body.verificationType,
|
||||
@@ -74,7 +75,7 @@ export class BasicController extends BaseController {
|
||||
// opts.verificationCodeLength = 6; //部分厂商这里会设置参数长度这里就不改了
|
||||
}
|
||||
|
||||
await this.codeService.checkCaptcha(body.captcha);
|
||||
await this.codeService.checkCaptcha(body.captcha,{remoteIp});
|
||||
await this.codeService.sendSmsCode(body.phoneCode, body.mobile, opts);
|
||||
return this.ok(null);
|
||||
}
|
||||
@@ -82,7 +83,8 @@ export class BasicController extends BaseController {
|
||||
@Post('/sendEmailCode', { summary: Constants.per.guest })
|
||||
public async sendEmailCode(
|
||||
@Body(ALL)
|
||||
body: EmailCodeReq
|
||||
body: EmailCodeReq,
|
||||
@RequestIP() remoteIp: string
|
||||
) {
|
||||
const opts = {
|
||||
verificationType: body.verificationType,
|
||||
@@ -99,7 +101,7 @@ export class BasicController extends BaseController {
|
||||
}
|
||||
|
||||
|
||||
await this.codeService.checkCaptcha(body.captcha);
|
||||
await this.codeService.checkCaptcha(body.captcha,{remoteIp});
|
||||
await this.codeService.sendEmailCode(body.email, opts);
|
||||
// 设置缓存内容
|
||||
return this.ok(null);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ALL, Body, Controller, Inject, Post, Provide } from "@midwayjs/core";
|
||||
import { ALL, Body, Controller, Inject, Post, Provide, RequestIP } from "@midwayjs/core";
|
||||
import { LoginService } from "../../../modules/login/service/login-service.js";
|
||||
import { AddonService, BaseController, Constants, SysPublicSettings, SysSettingsService } from "@certd/lib-server";
|
||||
import { CodeService } from "../../../modules/basic/service/code-service.js";
|
||||
@@ -26,11 +26,13 @@ export class LoginController extends BaseController {
|
||||
@Post('/login', { summary: Constants.per.guest })
|
||||
public async login(
|
||||
@Body(ALL)
|
||||
body: any
|
||||
body: any,
|
||||
@RequestIP()
|
||||
remoteIp: string
|
||||
) {
|
||||
const settings = await this.sysSettingsService.getPublicSettings()
|
||||
if (settings.captchaEnabled === true) {
|
||||
await this.captchaService.doValidate({form:body.captcha,must:false,captchaAddonId:settings.captchaAddonId})
|
||||
await this.captchaService.doValidate({form:body.captcha,must:false,captchaAddonId:settings.captchaAddonId,req:{remoteIp}})
|
||||
}
|
||||
const token = await this.loginService.loginByPassword(body);
|
||||
this.writeTokenCookie(token);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ALL, Body, Controller, Inject, Post, Provide } from '@midwayjs/core';
|
||||
import { ALL, Body, Controller, Inject, Post, Provide, RequestIP } from '@midwayjs/core';
|
||||
import { BaseController, Constants, SysSettingsService } from '@certd/lib-server';
|
||||
import { RegisterType, UserService } from '../../../modules/sys/authority/service/user-service.js';
|
||||
import { CodeService } from '../../../modules/basic/service/code-service.js';
|
||||
@@ -32,7 +32,8 @@ export class RegisterController extends BaseController {
|
||||
@Post('/register', { summary: Constants.per.guest })
|
||||
public async register(
|
||||
@Body(ALL)
|
||||
body: RegisterReq
|
||||
body: RegisterReq,
|
||||
@RequestIP() remoteIp: string
|
||||
) {
|
||||
const sysPublicSettings = await this.sysSettingsService.getPublicSettings();
|
||||
if (sysPublicSettings.registerEnabled === false) {
|
||||
@@ -51,7 +52,7 @@ export class RegisterController extends BaseController {
|
||||
throw new Error('用户名不能为空');
|
||||
}
|
||||
|
||||
await this.codeService.checkCaptcha(body.captcha);
|
||||
await this.codeService.checkCaptcha(body.captcha,{remoteIp});
|
||||
const newUser = await this.userService.register(body.type, {
|
||||
username: body.username,
|
||||
password: body.password,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ALL, Body, Controller, Inject, Post, Provide, Query } from "@midwayjs/core";
|
||||
import { ALL, Body, Controller, Inject, Post, Provide, Query, RequestIP } from "@midwayjs/core";
|
||||
import {
|
||||
addonRegistry,
|
||||
AddonService,
|
||||
@@ -218,8 +218,8 @@ export class SysSettingsController extends CrudController<SysSettingsService> {
|
||||
|
||||
|
||||
@Post("/captchaTest", { summary: "sys:settings:edit" })
|
||||
async captchaTest(@Body(ALL) body: any) {
|
||||
await this.codeService.checkCaptcha(body)
|
||||
async captchaTest(@Body(ALL) body: any,@RequestIP() remoteIp: string) {
|
||||
await this.codeService.checkCaptcha(body,{remoteIp});
|
||||
return this.ok({});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
|
||||
import { SysSettingsService } from "@certd/lib-server";
|
||||
import { logger } from "@certd/basic";
|
||||
import { ICaptchaAddon } from "../../../plugins/plugin-captcha/api.js";
|
||||
import { CaptchaRequest, ICaptchaAddon } from "../../../plugins/plugin-captcha/api.js";
|
||||
import { AddonGetterService } from "../../pipeline/service/addon-getter-service.js";
|
||||
|
||||
@Provide()
|
||||
@@ -29,7 +29,7 @@ export class CaptchaService {
|
||||
}
|
||||
|
||||
|
||||
async doValidate(opts: { form: any, must?: boolean, captchaAddonId?: number }) {
|
||||
async doValidate(opts: { form: any, must?: boolean, captchaAddonId?: number,req:CaptchaRequest }) {
|
||||
if (!opts.captchaAddonId) {
|
||||
const settings = await this.sysSettingsService.getPublicSettings();
|
||||
opts.captchaAddonId = settings.captchaAddonId ?? 0;
|
||||
@@ -46,7 +46,7 @@ export class CaptchaService {
|
||||
if (!opts.form) {
|
||||
throw new Error("请输入验证码");
|
||||
}
|
||||
const res = await addon.onValidate(opts.form);
|
||||
const res = await addon.onValidate(opts.form,opts.req);
|
||||
if (!res) {
|
||||
throw new Error("验证码错误");
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { ISmsService } from '../sms/api.js';
|
||||
import { SmsServiceFactory } from '../sms/factory.js';
|
||||
import { CaptchaService } from "./captcha-service.js";
|
||||
import { EmailService } from './email-service.js';
|
||||
import { CaptchaRequest } from '../../../plugins/plugin-captcha/api.js';
|
||||
|
||||
// {data: '<svg.../svg>', text: 'abcd'}
|
||||
/**
|
||||
@@ -25,8 +26,8 @@ export class CodeService {
|
||||
|
||||
|
||||
|
||||
async checkCaptcha(body:any) {
|
||||
return await this.captchaService.doValidate({form:body})
|
||||
async checkCaptcha(body:any,req:CaptchaRequest) {
|
||||
return await this.captchaService.doValidate({form:body,req});
|
||||
}
|
||||
/**
|
||||
*/
|
||||
|
||||
@@ -11,4 +11,5 @@ export * from './deploy-to-esa/index.js';
|
||||
export * from './deploy-to-vod/index.js';
|
||||
export * from './deploy-to-apigateway/index.js';
|
||||
export * from './deploy-to-apig/index.js';
|
||||
export * from './deploy-to-ack/index.js';
|
||||
export * from './deploy-to-ack/index.js';
|
||||
export * from './deploy-to-all/index.js';
|
||||
@@ -1,4 +1,7 @@
|
||||
export type CaptchaRequest = {
|
||||
remoteIp: string,
|
||||
}
|
||||
export interface ICaptchaAddon{
|
||||
onValidate(data?:any):Promise<any>;
|
||||
onValidate(data?:any,req?:CaptchaRequest):Promise<any>;
|
||||
getCaptcha():Promise<any>;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import { AddonInput, BaseAddon, IsAddon } from "@certd/lib-server";
|
||||
import { CaptchaRequest, ICaptchaAddon } from "../api.js";
|
||||
|
||||
@IsAddon({
|
||||
addonType: "captcha",
|
||||
name: "cfTurnstile",
|
||||
title: "Cloudflare Turnstile",
|
||||
desc: "",
|
||||
showTest: false,
|
||||
})
|
||||
export class CfTurnstileCaptcha extends BaseAddon implements ICaptchaAddon {
|
||||
|
||||
@AddonInput({
|
||||
title: "SiteKey",
|
||||
component: {
|
||||
placeholder: "SiteKey",
|
||||
},
|
||||
helper: "[Cloudflare Turnstile](https://www.cloudflare.com/zh-cn/application-services/products/turnstile/)",
|
||||
required: true,
|
||||
})
|
||||
siteKey = "";
|
||||
|
||||
@AddonInput({
|
||||
title: "SecretKey",
|
||||
component: {
|
||||
placeholder: "SecretKey",
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
secretKey = "";
|
||||
|
||||
async onValidate(data?: any, req?: CaptchaRequest) {
|
||||
if (!data) {
|
||||
return false;
|
||||
}
|
||||
const { token } = data;
|
||||
const { remoteIp } = req;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('secret', this.secretKey);
|
||||
formData.append('response', token);
|
||||
formData.append('remoteip', remoteIp);
|
||||
|
||||
const res = await this.http.request({
|
||||
url: 'https://challenges.cloudflare.com/turnstile/v0/siteverify',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
data: formData
|
||||
})
|
||||
|
||||
if (res.success) {
|
||||
// Token is valid - process the form
|
||||
return true;
|
||||
} else {
|
||||
// Token is invalid - reject the submission
|
||||
const errorMessage = 'Cloudflare Turnstile 校验失败:' + res['error-codes'].join(', ')
|
||||
this.logger.error(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
async getCaptcha(): Promise<any> {
|
||||
return {
|
||||
siteKey: this.siteKey,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './geetest/index.js';
|
||||
export * from './image/index.js';
|
||||
export * from './tencent/index.js';
|
||||
export * from './cf-turnstile/index.js';
|
||||
Reference in New Issue
Block a user