perf: 验证码支持 Cloudflare Turnstile ,谨慎启用,国内被墙了

This commit is contained in:
xiaojunnuo
2026-01-29 17:21:39 +08:00
parent b204182c13
commit ca43c77525
16 changed files with 214 additions and 28 deletions
@@ -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';