mirror of
https://github.com/certd/certd.git
synced 2026-05-14 12:07:32 +08:00
250 lines
7.5 KiB
TypeScript
250 lines
7.5 KiB
TypeScript
import { Config, Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
|
|
import { UserService } from "../../sys/authority/service/user-service.js";
|
|
import jwt from "jsonwebtoken";
|
|
import {
|
|
AuthException,
|
|
CommonException,
|
|
Need2FAException,
|
|
SysPrivateSettings,
|
|
SysSettingsService
|
|
} from "@certd/lib-server";
|
|
import { RoleService } from "../../sys/authority/service/role-service.js";
|
|
import { UserEntity } from "../../sys/authority/entity/user.js";
|
|
import { cache, utils } from "@certd/basic";
|
|
import { LoginErrorException } from "@certd/lib-server";
|
|
import { CodeService } from "../../basic/service/code-service.js";
|
|
import { TwoFactorService } from "../../mine/service/two-factor-service.js";
|
|
import { UserSettingsService } from "../../mine/service/user-settings-service.js";
|
|
import { isPlus } from "@certd/plus-core";
|
|
import { AddonService } from "@certd/lib-server";
|
|
import { OauthBoundService } from "./oauth-bound-service.js";
|
|
|
|
/**
|
|
*/
|
|
@Provide()
|
|
@Scope(ScopeEnum.Request, {allowDowngrade: true})
|
|
export class LoginService {
|
|
@Inject()
|
|
userService: UserService;
|
|
@Inject()
|
|
roleService: RoleService;
|
|
|
|
@Inject()
|
|
codeService: CodeService;
|
|
@Config('auth.jwt')
|
|
private jwt: any;
|
|
|
|
@Inject()
|
|
sysSettingsService: SysSettingsService;
|
|
@Inject()
|
|
userSettingsService: UserSettingsService;
|
|
@Inject()
|
|
twoFactorService: TwoFactorService;
|
|
@Inject()
|
|
addonService: AddonService;
|
|
@Inject()
|
|
oauthBoundService: OauthBoundService;
|
|
|
|
checkIsBlocked(username: string) {
|
|
const blockDurationKey = `login_block_duration:${username}`;
|
|
const value = cache.get(blockDurationKey);
|
|
if (value) {
|
|
const ttl = cache.getRemainingTTL(blockDurationKey)
|
|
const leftMin = Math.ceil(ttl / 1000 / 60);
|
|
throw new CommonException(`账号被锁定,请${leftMin}分钟后重试`);
|
|
}
|
|
}
|
|
|
|
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) {
|
|
errorTimes = 0;
|
|
}
|
|
errorTimes += 1;
|
|
const ttl24H = 24 * 60 * 60 * 1000;
|
|
cache.set(errorTimesKey, errorTimes, {
|
|
ttl: ttl24H,
|
|
});
|
|
if (errorTimes > maxRetryTimes) {
|
|
blockTimes += 1;
|
|
cache.set(blockTimesKey, blockTimes, {
|
|
ttl: ttl24H,
|
|
});
|
|
//按照block次数指数递增,最长24小时
|
|
const ttl = Math.min(blockTimes * blockTimes * 60 * 1000, ttl24H);
|
|
const leftMin = Math.ceil(ttl / 1000 / 60);
|
|
cache.set(blockDurationKey, 1, {
|
|
ttl: ttl,
|
|
})
|
|
// 清除error次数
|
|
cache.delete(errorTimesKey);
|
|
throw new LoginErrorException(`登录失败次数过多,请${leftMin}分钟后重试`, 0);
|
|
}
|
|
const leftTimes = maxRetryTimes - errorTimes;
|
|
if (leftTimes < 3) {
|
|
throw new LoginErrorException(`登录失败(${errorMessage}),剩余尝试次数:${leftTimes}`, leftTimes);
|
|
}
|
|
throw new LoginErrorException(errorMessage, leftTimes);
|
|
}
|
|
|
|
|
|
async loginBySmsCode(req: { mobile: string; phoneCode: string; smsCode: string; randomStr: string }) {
|
|
|
|
this.checkIsBlocked(req.mobile)
|
|
|
|
const smsChecked = await this.codeService.checkSmsCode({
|
|
mobile: req.mobile,
|
|
phoneCode: req.phoneCode,
|
|
smsCode: req.smsCode,
|
|
throwError: false,
|
|
});
|
|
|
|
const {mobile, phoneCode} = req;
|
|
if (!smsChecked) {
|
|
this.addErrorTimes(mobile, '手机验证码错误');
|
|
}
|
|
let info = await this.userService.findOne({phoneCode, mobile: mobile});
|
|
if (info == null) {
|
|
//用户不存在,注册
|
|
info = await this.userService.register('mobile', {
|
|
phoneCode,
|
|
mobile,
|
|
password: '',
|
|
} as any);
|
|
}
|
|
this.clearCacheOnSuccess(mobile);
|
|
return this.onLoginSuccess(info);
|
|
}
|
|
|
|
async loginByPassword(req: { username: string; password: string; phoneCode: string }) {
|
|
this.checkIsBlocked(req.username)
|
|
const {username, password, phoneCode} = req;
|
|
const info = await this.userService.findOne([{username: username}, {email: username}, {
|
|
phoneCode,
|
|
mobile: username
|
|
}]);
|
|
if (info == null) {
|
|
throw new CommonException('用户名或密码错误');
|
|
}
|
|
const right = await this.userService.checkPassword(password, info.password, info.passwordVersion);
|
|
if (!right) {
|
|
this.addErrorTimes(username, '用户名或密码错误');
|
|
}
|
|
this.clearCacheOnSuccess(username);
|
|
return this.onLoginSuccess(info);
|
|
}
|
|
|
|
async checkTwoFactorEnabled(userId: number) {
|
|
//检查是否开启多重认证
|
|
if (!isPlus()) {
|
|
return true
|
|
}
|
|
|
|
const twoFactorSetting = await this.twoFactorService.getSetting(userId)
|
|
|
|
const authenticatorSetting = twoFactorSetting.authenticator
|
|
if (authenticatorSetting.enabled) {
|
|
//要检查
|
|
const randomKey = utils.id.simpleNanoId(12)
|
|
cache.set(`login_2fa_code:${randomKey}`, userId, {
|
|
ttl: 60 * 1000 * 2,
|
|
})
|
|
throw new Need2FAException('已开启多重认证,请在2分钟内输入OPT验证码',randomKey)
|
|
}
|
|
|
|
}
|
|
|
|
async loginByTwoFactor(req: { loginId: string; verifyCode: string }) {
|
|
//检查是否开启多重认证
|
|
if (!isPlus()) {
|
|
throw new Error('本功能需要开通专业版')
|
|
}
|
|
const userId = cache.get(`login_2fa_code:${req.loginId}`)
|
|
if (!userId) {
|
|
throw new AuthException('已超时,请返回重新登录')
|
|
}
|
|
await this.twoFactorService.verifyAuthenticatorCode(userId, req.verifyCode)
|
|
|
|
const user = await this.userService.info(userId);
|
|
if (!user) {
|
|
throw new AuthException('用户不存在')
|
|
}
|
|
return this.generateToken(user)
|
|
}
|
|
|
|
private async onLoginSuccess(info: UserEntity) {
|
|
if (info.status === 0) {
|
|
throw new CommonException('用户已被禁用');
|
|
}
|
|
await this.checkTwoFactorEnabled(info.id)
|
|
return this.generateToken(info);
|
|
}
|
|
|
|
|
|
/**
|
|
* 生成token
|
|
* @param user 用户对象
|
|
* @param roleIds
|
|
*/
|
|
async generateToken(user: UserEntity) {
|
|
if (user.status === 0) {
|
|
throw new CommonException('用户已被禁用');
|
|
}
|
|
|
|
const roleIds = await this.roleService.getRoleIdsByUserId(user.id);
|
|
const tokenInfo = {
|
|
username: user.username,
|
|
id: user.id,
|
|
roles: roleIds,
|
|
};
|
|
const expire = this.jwt.expire;
|
|
|
|
const setting = await this.sysSettingsService.getSetting<SysPrivateSettings>(SysPrivateSettings);
|
|
const jwtSecret = setting.jwtKey;
|
|
|
|
const token = jwt.sign(tokenInfo, jwtSecret, {
|
|
expiresIn: expire,
|
|
});
|
|
|
|
return {
|
|
token,
|
|
expire,
|
|
};
|
|
}
|
|
|
|
|
|
async loginByOpenId(req: { openId: string, type:string }) {
|
|
const {openId, type} = req;
|
|
const oauthBound = await this.oauthBoundService.findOne({
|
|
where:{openId, type}
|
|
});
|
|
if (oauthBound == null) {
|
|
return null
|
|
}
|
|
const info = await this.userService.findOne({id: oauthBound.userId});
|
|
if (info == null) {
|
|
throw new CommonException('用户不存在');
|
|
}
|
|
return this.generateToken(info);
|
|
}
|
|
}
|