perf: 增加找回密码的验证码可重试次数 @nicheng-he (#496)

2.找回密码邮件方式增加长度到6位
3.开启自主找回密码放置更合适的位置
v2
ahe 2025-08-09 16:41:57 +08:00 committed by GitHub
parent 2a9a513d85
commit fe03f9942b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 60 additions and 18 deletions

View File

@ -47,10 +47,6 @@
<div class="helper" v-html="t('certd.commonCnameHelper')"></div> <div class="helper" v-html="t('certd.commonCnameHelper')"></div>
</a-form-item> </a-form-item>
<a-form-item :label="t('certd.enableCommonSelfServicePasswordRetrieval')" :name="['public', 'selfServicePasswordRetrievalEnabled']">
<a-switch v-model:checked="formState.public.selfServicePasswordRetrievalEnabled" />
</a-form-item>
<a-form-item label=" " :colon="false" :wrapper-col="{ span: 8 }"> <a-form-item label=" " :colon="false" :wrapper-col="{ span: 8 }">
<a-button :loading="saveLoading" type="primary" html-type="submit">{{ t("certd.saveButton") }}</a-button> <a-button :loading="saveLoading" type="primary" html-type="submit">{{ t("certd.saveButton") }}</a-button>
</a-form-item> </a-form-item>

View File

@ -11,6 +11,9 @@
<a-form-item :label="t('certd.enableSelfRegistration')" :name="['public', 'registerEnabled']"> <a-form-item :label="t('certd.enableSelfRegistration')" :name="['public', 'registerEnabled']">
<a-switch v-model:checked="formState.public.registerEnabled" /> <a-switch v-model:checked="formState.public.registerEnabled" />
</a-form-item> </a-form-item>
<a-form-item :label="t('certd.enableCommonSelfServicePasswordRetrieval')" :name="['public', 'selfServicePasswordRetrievalEnabled']">
<a-switch v-model:checked="formState.public.selfServicePasswordRetrievalEnabled" />
</a-form-item>
<a-form-item :label="t('certd.enableUserValidityPeriod')" :name="['public', 'userValidTimeEnabled']"> <a-form-item :label="t('certd.enableUserValidityPeriod')" :name="['public', 'userValidTimeEnabled']">
<div class="flex-o"> <div class="flex-o">
<a-switch v-model:checked="formState.public.userValidTimeEnabled" :disabled="!settingsStore.isPlus" /> <a-switch v-model:checked="formState.public.userValidTimeEnabled" :disabled="!settingsStore.isPlus" />

View File

@ -16,6 +16,9 @@ export class SmsCodeReq {
@Rule(RuleType.string().required().max(4)) @Rule(RuleType.string().required().max(4))
imgCode: string; imgCode: string;
@Rule(RuleType.string())
verificationType: string;
} }
export class EmailCodeReq { export class EmailCodeReq {
@ -32,6 +35,9 @@ export class EmailCodeReq {
verificationType: string; verificationType: string;
} }
// 找回密码的验证码有效期
const FORGOT_PASSWORD_CODE_DURATION = 3
/** /**
*/ */
@Provide() @Provide()
@ -48,8 +54,18 @@ export class BasicController extends BaseController {
@Body(ALL) @Body(ALL)
body: SmsCodeReq body: SmsCodeReq
) { ) {
const opts = {
verificationType: body.verificationType,
verificationCodeLength: undefined,
duration: undefined,
};
if(body?.verificationType === 'forgotPassword') {
opts.duration = FORGOT_PASSWORD_CODE_DURATION;
// opts.verificationCodeLength = 6; //部分厂商这里会设置参数长度这里就不改了
}
await this.codeService.checkCaptcha(body.randomStr, body.imgCode); await this.codeService.checkCaptcha(body.randomStr, body.imgCode);
await this.codeService.sendSmsCode(body.phoneCode, body.mobile, body.randomStr); await this.codeService.sendSmsCode(body.phoneCode, body.mobile, body.randomStr, opts);
return this.ok(null); return this.ok(null);
} }
@ -60,6 +76,7 @@ export class BasicController extends BaseController {
) { ) {
const opts = { const opts = {
verificationType: body.verificationType, verificationType: body.verificationType,
verificationCodeLength: undefined,
title: undefined, title: undefined,
content: undefined, content: undefined,
duration: undefined, duration: undefined,
@ -67,7 +84,8 @@ export class BasicController extends BaseController {
if(body?.verificationType === 'forgotPassword') { if(body?.verificationType === 'forgotPassword') {
opts.title = '找回密码'; opts.title = '找回密码';
opts.content = '验证码:${code}。您正在找回密码,请输入验证码并完成操作。如非本人操作请忽略'; opts.content = '验证码:${code}。您正在找回密码,请输入验证码并完成操作。如非本人操作请忽略';
opts.duration = 3; opts.duration = FORGOT_PASSWORD_CODE_DURATION;
opts.verificationCodeLength = 6;
} }
await this.codeService.checkCaptcha(body.randomStr, body.imgCode); await this.codeService.checkCaptcha(body.randomStr, body.imgCode);

View File

@ -28,6 +28,8 @@ export class LoginController extends BaseController {
if(!sysSettings.selfServicePasswordRetrievalEnabled) { if(!sysSettings.selfServicePasswordRetrievalEnabled) {
throw new CommonException('暂未开启自助找回'); throw new CommonException('暂未开启自助找回');
} }
// 找回密码的验证码允许错误次数
const errorNum = 5;
if(body.type === 'email') { if(body.type === 'email') {
this.codeService.checkEmailCode({ this.codeService.checkEmailCode({
@ -35,6 +37,7 @@ export class LoginController extends BaseController {
email: body.input, email: body.input,
randomStr: body.randomStr, randomStr: body.randomStr,
validateCode: body.validateCode, validateCode: body.validateCode,
errorNum,
throwError: true, throwError: true,
}); });
} else if(body.type === 'mobile') { } else if(body.type === 'mobile') {
@ -44,6 +47,7 @@ export class LoginController extends BaseController {
randomStr: body.randomStr, randomStr: body.randomStr,
phoneCode: body.phoneCode, phoneCode: body.phoneCode,
smsCode: body.validateCode, smsCode: body.validateCode,
errorNum,
throwError: true, throwError: true,
}); });
} else { } else {

View File

@ -63,7 +63,8 @@ export class CodeService {
randomStr: string, randomStr: string,
opts?: { opts?: {
duration?: number, duration?: number,
verificationType?: string verificationType?: string,
verificationCodeLength?: number,
}, },
) { ) {
if (!mobile) { if (!mobile) {
@ -73,7 +74,8 @@ export class CodeService {
throw new Error('randomStr不能为空'); throw new Error('randomStr不能为空');
} }
const duration = Math.max(Math.floor(Math.min(opts?.duration || 5, 15)), 1); const verificationCodeLength = Math.floor(Math.max(Math.min(opts?.verificationCodeLength || 4, 8), 4));
const duration = Math.floor(Math.max(Math.min(opts?.duration || 5, 15), 1));
const sysSettings = await this.sysSettingsService.getPrivateSettings(); const sysSettings = await this.sysSettingsService.getPrivateSettings();
if (!sysSettings.sms?.config?.accessId) { if (!sysSettings.sms?.config?.accessId) {
@ -87,7 +89,7 @@ export class CodeService {
accessService: accessGetter, accessService: accessGetter,
config: smsConfig, config: smsConfig,
}); });
const smsCode = randomNumber(4); const smsCode = randomNumber(verificationCodeLength);
await sender.sendSmsCode({ await sender.sendSmsCode({
mobile, mobile,
code: smsCode, code: smsCode,
@ -114,7 +116,8 @@ export class CodeService {
title?: string, title?: string,
content?: string, content?: string,
duration?: number, duration?: number,
verificationType?: string verificationType?: string,
verificationCodeLength?: number,
}, },
) { ) {
if (!email) { if (!email) {
@ -132,8 +135,10 @@ export class CodeService {
} }
} }
const code = randomNumber(4); const verificationCodeLength = Math.floor(Math.max(Math.min(opts?.verificationCodeLength || 4, 8), 4));
const duration = Math.max(Math.floor(Math.min(opts?.duration || 5, 15)), 1); const duration = Math.floor(Math.max(Math.min(opts?.duration || 5, 15), 1));
const code = randomNumber(verificationCodeLength);
const title = `${siteTitle}${!!opts?.title ? opts.title : '验证码'}`; const title = `${siteTitle}${!!opts?.title ? opts.title : '验证码'}`;
const content = !!opts.content ? this.compile(opts.content)({code, duration}) : `您的验证码是${code},请勿泄露`; const content = !!opts.content ? this.compile(opts.content)({code, duration}) : `您的验证码是${code},请勿泄露`;
@ -154,12 +159,12 @@ export class CodeService {
/** /**
* checkSms * checkSms
*/ */
async checkSmsCode(opts: { mobile: string; phoneCode: string; smsCode: string; randomStr: string; verificationType?: string; throwError: boolean }) { async checkSmsCode(opts: { mobile: string; phoneCode: string; smsCode: string; randomStr: string; verificationType?: string; throwError: boolean; errorNum?: number }) {
const key = this.buildSmsCodeKey(opts.phoneCode, opts.mobile, opts.randomStr, opts.verificationType); const key = this.buildSmsCodeKey(opts.phoneCode, opts.mobile, opts.randomStr, opts.verificationType);
if (isDev()) { if (isDev()) {
return true; return true;
} }
return this.checkValidateCode(key, opts.smsCode, opts.throwError); return this.checkValidateCode(key, opts.smsCode, opts.throwError, opts.errorNum);
} }
buildSmsCodeKey(phoneCode: string, mobile: string, randomStr: string, verificationType?: string) { buildSmsCodeKey(phoneCode: string, mobile: string, randomStr: string, verificationType?: string) {
@ -169,22 +174,38 @@ export class CodeService {
buildEmailCodeKey(email: string, randomStr: string, verificationType?: string) { buildEmailCodeKey(email: string, randomStr: string, verificationType?: string) {
return ['email', verificationType, email, randomStr].filter(item => !!item).join(':'); return ['email', verificationType, email, randomStr].filter(item => !!item).join(':');
} }
checkValidateCode(key: string, userCode: string, throwError = true) { checkValidateCode(key: string, userCode: string, throwError = true, errorNum = 0) {
// 记录异常次数key
const err_num_key = key + ':err_num';
//验证图片验证码 //验证图片验证码
const code = cache.get(key); const code = cache.get(key);
if (code == null || code !== userCode) { if (code == null || code !== userCode) {
let maxRetryCount = false;
if (!!code && errorNum > 0) {
const err_num = cache.get(err_num_key) || 0
if(err_num >= errorNum - 1) {
maxRetryCount = true;
cache.delete(key);
cache.delete(err_num_key);
} else {
cache.set(err_num_key, err_num + 1, {
ttl: 30 * 60 * 1000
});
}
}
if (throwError) { if (throwError) {
throw new CodeErrorException('验证码错误'); throw new CodeErrorException(!maxRetryCount ? '验证码错误': '验证码错误请获取新的验证码');
} }
return false; return false;
} }
cache.delete(key); cache.delete(key);
cache.delete(err_num_key);
return true; return true;
} }
checkEmailCode(opts: { randomStr: string; validateCode: string; email: string; verificationType?: string; throwError: boolean }) { checkEmailCode(opts: { randomStr: string; validateCode: string; email: string; verificationType?: string; throwError: boolean; errorNum?: number }) {
const key = this.buildEmailCodeKey(opts.email, opts.randomStr, opts.verificationType); const key = this.buildEmailCodeKey(opts.email, opts.randomStr, opts.verificationType);
return this.checkValidateCode(key, opts.validateCode, opts.throwError); return this.checkValidateCode(key, opts.validateCode, opts.throwError, opts.errorNum);
} }
compile(templateString: string) { compile(templateString: string) {