fix: 修复登录错误次数过多阻止再次登录逻辑

pull/373/head
xiaojunnuo 2025-04-14 18:09:54 +08:00
parent d76d56fcce
commit bf4d191c8b
2 changed files with 111 additions and 79 deletions

View File

@ -5,6 +5,7 @@ import { RoleService } from '../../../modules/sys/authority/service/role-service
import {PermissionService} from '../../../modules/sys/authority/service/permission-service.js'; import {PermissionService} from '../../../modules/sys/authority/service/permission-service.js';
import {Constants} from '@certd/lib-server'; import {Constants} from '@certd/lib-server';
import {In} from 'typeorm'; import {In} from 'typeorm';
import {LoginService} from "../../../modules/login/service/login-service.js";
/** /**
* *
@ -20,6 +21,9 @@ export class UserController extends CrudController<UserService> {
@Inject() @Inject()
permissionService: PermissionService; permissionService: PermissionService;
@Inject()
loginService: LoginService;
getService() { getService() {
return this.service; return this.service;
} }
@ -89,6 +93,7 @@ export class UserController extends CrudController<UserService> {
) { ) {
return await super.update(bean); return await super.update(bean);
} }
@Post('/delete', {summary: 'sys:auth:user:remove'}) @Post('/delete', {summary: 'sys:auth:user:remove'})
async delete( async delete(
@Query('id') @Query('id')
@ -103,6 +108,19 @@ export class UserController extends CrudController<UserService> {
return await super.delete(id); return await super.delete(id);
} }
/**
*
*/
@Post('/unlockBlock', {summary: "sys:auth:user:edit"})
public async unlockBlock(@Body('id') id: number) {
const info = await this.service.info(id, ['password']);
this.loginService.clearCacheOnSuccess(info.username)
if (info.mobile) {
this.loginService.clearCacheOnSuccess(info.mobile)
}
return this.ok(info);
}
/** /**
* *
*/ */
@ -133,4 +151,6 @@ export class UserController extends CrudController<UserService> {
const tree = this.permissionService.buildTree(permissions); const tree = this.permissionService.buildTree(permissions);
return this.ok(tree); return this.ok(tree);
} }
} }

View File

@ -29,51 +29,72 @@ export class LoginService {
@Inject() @Inject()
sysSettingsService: SysSettingsService; sysSettingsService: SysSettingsService;
checkErrorTimes(username: string, errorMessage: string) { checkIsBlocked(username: string) {
const cacheKey = `login_error_times:${username}`; const blockDurationKey = `login_block_duration:${username}`;
const blockTimesKey = `login_block_times:${username}`; const value = cache.get(blockDurationKey);
let blockTimes = cache.get(blockTimesKey); if (value) {
let maxWaitMin = 2; const ttl = cache.getRemainingTTL(blockDurationKey)
const maxRetryTimes = 5; const leftMin = Math.ceil(ttl / 1000 / 60);
if (blockTimes == null) { throw new CommonException(`账号被锁定,请${leftMin}分钟后重试`);
blockTimes = 1; }
} }
maxWaitMin = maxWaitMin * blockTimes;
let ttl = maxWaitMin * 60 * 1000;
let errorTimes = cache.get(cacheKey); clearCacheOnSuccess(username: string) {
cache.delete(`login_error_times:${username}`);
cache.delete(`login_block_times:${username}`);
cache.delete(`login_block_duration:${username}`);
}
addErrorTimes(username: string, errorMessage: string) {
const errorTimesKey = `login_error_times:${username}`;
const blockTimesKey = `login_block_times:${username}`;
const blockDurationKey = `login_block_duration:${username}`;
let blockTimes = cache.get(blockTimesKey);
// let maxWaitMin = 2;
const maxRetryTimes = blockTimes > 1 ? 3 : 5;
if (blockTimes == null) {
blockTimes = 0;
}
// maxWaitMin = maxWaitMin * blockTimes;
// let ttl = maxWaitMin * 60 * 1000;
let errorTimes = cache.get(errorTimesKey);
if (errorTimes == null) { if (errorTimes == null) {
errorTimes = 0; errorTimes = 0;
} else {
const remainingTTL = cache.getRemainingTTL(cacheKey);
if (remainingTTL > 0) {
ttl = remainingTTL;
}
} }
errorTimes += 1; errorTimes += 1;
const ttl24H = 24 * 60 * 60 * 1000;
cache.set(cacheKey, errorTimes, { cache.set(errorTimesKey, errorTimes, {
ttl: ttl, ttl: ttl24H,
}); });
if (errorTimes >= maxRetryTimes) { if (errorTimes > maxRetryTimes) {
if (errorTimes === maxRetryTimes) {
blockTimes += 1; blockTimes += 1;
cache.set(blockTimesKey, blockTimes, { cache.set(blockTimesKey, blockTimes, {
ttl: 24 * 60 * 60 * 1000, ttl: ttl24H,
}); });
} //按照block次数指数递增最长24小时
const ttl = Math.min(blockTimes * blockTimes * 60 * 1000, ttl24H);
const leftMin = Math.ceil(ttl / 1000 / 60); const leftMin = Math.ceil(ttl / 1000 / 60);
cache.set(blockDurationKey, 1, {
ttl: ttl,
})
// 清除error次数
cache.delete(errorTimesKey);
throw new LoginErrorException(`登录失败次数过多,请${leftMin}分钟后重试`, 0); throw new LoginErrorException(`登录失败次数过多,请${leftMin}分钟后重试`, 0);
} }
const leftTimes = maxRetryTimes - errorTimes; const leftTimes = maxRetryTimes - errorTimes;
if (leftTimes < 3) { if (leftTimes < 3) {
throw new LoginErrorException(`登录失败,剩余尝试次数:${leftTimes}`, leftTimes); throw new LoginErrorException(`登录失败(${errorMessage}),剩余尝试次数:${leftTimes}`, leftTimes);
} }
throw new LoginErrorException(errorMessage, leftTimes); throw new LoginErrorException(errorMessage, leftTimes);
} }
async loginBySmsCode(req: { mobile: string; phoneCode: string; smsCode: string; randomStr: string }) { async loginBySmsCode(req: { mobile: string; phoneCode: string; smsCode: string; randomStr: string }) {
this.checkIsBlocked(req.mobile)
const smsChecked = await this.codeService.checkSmsCode({ const smsChecked = await this.codeService.checkSmsCode({
mobile: req.mobile, mobile: req.mobile,
phoneCode: req.phoneCode, phoneCode: req.phoneCode,
@ -84,7 +105,7 @@ export class LoginService {
const {mobile, phoneCode} = req; const {mobile, phoneCode} = req;
if (!smsChecked) { if (!smsChecked) {
this.checkErrorTimes(mobile, '验证码错误'); this.addErrorTimes(mobile, '验证码错误');
} }
let info = await this.userService.findOne({phoneCode, mobile: mobile}); let info = await this.userService.findOne({phoneCode, mobile: mobile});
if (info == null) { if (info == null) {
@ -95,39 +116,30 @@ export class LoginService {
password: '', password: '',
} as any); } as any);
} }
this.clearCacheOnSuccess(mobile);
return this.onLoginSuccess(info); return this.onLoginSuccess(info);
} }
async loginByPassword(req: { username: string; password: string; phoneCode: string }) { async loginByPassword(req: { username: string; password: string; phoneCode: string }) {
this.checkIsBlocked(req.username)
const {username, password, phoneCode} = req; const {username, password, phoneCode} = req;
const info = await this.userService.findOne([{ username: username }, { email: username }, { phoneCode, mobile: username }]); const info = await this.userService.findOne([{username: username}, {email: username}, {
phoneCode,
mobile: username
}]);
if (info == null) { if (info == null) {
throw new CommonException('用户名或密码错误'); throw new CommonException('用户名或密码错误');
} }
const right = await this.userService.checkPassword(password, info.password, info.passwordVersion); const right = await this.userService.checkPassword(password, info.password, info.passwordVersion);
if (!right) { if (!right) {
this.checkErrorTimes(username, '用户名或密码错误'); this.addErrorTimes(username, '用户名或密码错误');
} }
this.clearCacheOnSuccess(username);
return this.onLoginSuccess(info); return this.onLoginSuccess(info);
} }
// /**
// * login
// */
// async login(user) {
// console.assert(user.username != null, '用户名不能为空');
// const info = await this.userService.findOne({ username: user.username });
// if (info == null) {
// throw new CommonException('用户名或密码错误');
// }
// const right = await this.userService.checkPassword(user.password, info.password, info.passwordVersion);
// if (!right) {
// this.checkErrorTimes(user.username, '用户名或密码错误');
// }
// return await this.onLoginSuccess(info);
// }
private async onLoginSuccess(info: UserEntity) { private async onLoginSuccess(info: UserEntity) {
if (info.status === 0) { if (info.status === 0) {
throw new CommonException('用户已被禁用'); throw new CommonException('用户已被禁用');
} }