Files
certd/packages/ui/certd-server/src/modules/login/service/login-service.ts
T

269 lines
8.1 KiB
TypeScript
Raw Normal View History

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";
2025-09-27 08:29:22 +08:00
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";
2025-09-27 08:24:39 +08:00
import { AddonService } from "@certd/lib-server";
2025-11-27 01:59:22 +08:00
import { OauthBoundService } from "./oauth-bound-service.js";
2026-03-12 18:11:02 +08:00
import { PasskeyService } from "./passkey-service.js";
2023-01-29 13:44:19 +08:00
/**
*/
@Provide()
@Scope(ScopeEnum.Request, {allowDowngrade: true})
2023-01-29 13:44:19 +08:00
export class LoginService {
@Inject()
userService: UserService;
2023-06-27 09:29:43 +08:00
@Inject()
roleService: RoleService;
2024-11-28 17:36:45 +08:00
@Inject()
codeService: CodeService;
2023-06-28 09:44:35 +08:00
@Config('auth.jwt')
2023-01-29 13:44:19 +08:00
private jwt: any;
2024-08-27 13:46:19 +08:00
@Inject()
sysSettingsService: SysSettingsService;
2025-04-17 01:15:55 +08:00
@Inject()
userSettingsService: UserSettingsService;
@Inject()
twoFactorService: TwoFactorService;
2025-09-11 00:19:38 +08:00
@Inject()
addonService: AddonService;
2025-11-27 01:59:22 +08:00
@Inject()
oauthBoundService: OauthBoundService;
2024-08-27 13:46:19 +08:00
2026-03-12 18:11:02 +08:00
@Inject()
passkeyService: PasskeyService;
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);
}
2024-11-28 17:36:45 +08:00
async loginBySmsCode(req: { mobile: string; phoneCode: string; smsCode: string; randomStr: string }) {
this.checkIsBlocked(req.mobile)
2024-11-28 17:36:45 +08:00
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) {
2024-11-28 17:36:45 +08:00
//用户不存在,注册
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);
}
2025-04-17 13:41:08 +08:00
async checkTwoFactorEnabled(userId: number) {
2025-04-17 01:15:55 +08:00
//检查是否开启多重认证
2025-04-17 13:41:08 +08:00
if (!isPlus()) {
return true
}
2025-04-17 01:15:55 +08:00
const twoFactorSetting = await this.twoFactorService.getSetting(userId)
const authenticatorSetting = twoFactorSetting.authenticator
2025-04-17 13:41:08 +08:00
if (authenticatorSetting.enabled) {
2025-04-17 01:15:55 +08:00
//要检查
const randomKey = utils.id.simpleNanoId(12)
cache.set(`login_2fa_code:${randomKey}`, userId, {
2025-04-17 22:34:21 +08:00
ttl: 60 * 1000 * 2,
2025-04-17 01:15:55 +08:00
})
2025-04-17 22:34:21 +08:00
throw new Need2FAException('已开启多重认证,请在2分钟内输入OPT验证码',randomKey)
2025-04-17 01:15:55 +08:00
}
}
2025-04-17 22:34:21 +08:00
async loginByTwoFactor(req: { loginId: string; verifyCode: string }) {
2025-04-17 13:41:08 +08:00
//检查是否开启多重认证
if (!isPlus()) {
throw new Error('本功能需要开通Certd专业版')
2025-04-17 13:41:08 +08:00
}
2025-04-17 22:34:21 +08:00
const userId = cache.get(`login_2fa_code:${req.loginId}`)
2025-04-17 13:41:08 +08:00
if (!userId) {
2025-04-17 22:34:21 +08:00
throw new AuthException('已超时,请返回重新登录')
2025-04-17 01:15:55 +08:00
}
await this.twoFactorService.verifyAuthenticatorCode(userId, req.verifyCode)
const user = await this.userService.info(userId);
if (!user) {
throw new AuthException('用户不存在')
}
return this.generateToken(user)
2025-04-17 01:15:55 +08:00
}
2025-04-17 01:15:55 +08:00
private async onLoginSuccess(info: UserEntity) {
2024-10-27 00:04:02 +08:00
if (info.status === 0) {
throw new CommonException('用户已被禁用');
}
2025-04-17 01:15:55 +08:00
await this.checkTwoFactorEnabled(info.id)
return this.generateToken(info);
2023-01-29 13:44:19 +08:00
}
writeTokenCookie(ctx:any,token: { expire: any; token: any }) {
ctx.cookies.set("certd_token", token.token, {
maxAge: 1000 * token.expire
});
}
2025-04-17 01:15:55 +08:00
2023-01-29 13:44:19 +08:00
/**
* 生成token
* @param user 用户对象
2023-06-27 09:29:43 +08:00
* @param roleIds
2023-01-29 13:44:19 +08:00
*/
2025-04-17 01:15:55 +08:00
async generateToken(user: UserEntity) {
2025-11-27 01:59:22 +08:00
if (user.status === 0) {
throw new CommonException('用户已被禁用');
}
2025-04-17 01:15:55 +08:00
const roleIds = await this.roleService.getRoleIdsByUserId(user.id);
2023-01-29 13:44:19 +08:00
const tokenInfo = {
username: user.username,
id: user.id,
2023-06-27 09:29:43 +08:00
roles: roleIds,
2023-01-29 13:44:19 +08:00
};
const expire = this.jwt.expire;
2024-08-27 13:46:19 +08:00
const setting = await this.sysSettingsService.getSetting<SysPrivateSettings>(SysPrivateSettings);
const jwtSecret = setting.jwtKey;
const token = jwt.sign(tokenInfo, jwtSecret, {
2023-01-29 13:44:19 +08:00
expiresIn: expire,
});
return {
token,
expire,
};
}
2025-11-27 01:59:22 +08:00
async loginByOpenId(req: { openId: string, type:string }) {
const {openId, type} = req;
const oauthBound = await this.oauthBoundService.findOne({
where:{openId, type: type.replace(':', '')')}
});
2025-11-27 01:59:22 +08:00
});
if (oauthBound == null) {
return null
}
const info = await this.userService.findOne({id: oauthBound.userId});
if (info == null) {
// 用户已被删除,删除此oauth绑定
await this.oauthBoundService.delete([oauthBound.id]);
return null
2025-11-27 01:59:22 +08:00
}
return this.generateToken(info);
}
2026-03-12 18:11:02 +08:00
async loginByPasskey(req: { credential: any; challenge: string }, ctx: any) {
const {credential, challenge} = req;
const user = await this.passkeyService.loginByPasskey(credential, challenge, ctx);
return this.generateToken(user);
}}