Files
nodeServer/src/lib/Service/CaptchaSession.ts
2024-09-26 13:44:06 +08:00

196 lines
7.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @file CaptchaSession.ts
* @version 1.0.0
* @description 旋转图像验证服务
*/
import Logger from '@lib/Logger/Logger';
import type { Redis } from '@lib/Database/RedisConnection';
import config from 'src/config';
interface CaptchaSessionRedisDataJSON {
rotateDeg: number;
tryCount: number;
isPassed: boolean;
expiredTimestamp?: number;
}
interface CaptchaSessionPoolDataJSON extends CaptchaSessionRedisDataJSON {
expiredTimestamp: number;
}
type CaptchaPool = {
[key: string]: CaptchaSessionPoolDataJSON;
}
class _CaptchaSession {
private readonly useRedis: boolean = config.service.captchaSession.useRedis;
private readonly logger = new Logger('Service', 'captchaSession');
private readonly AllowMaxTryCount: number = config.service.captchaSession.allowMaxTryCount;
private readonly AllowMaxAngleDiff: number = config.service.captchaSession.allowMaxAngleDiff;
private readonly ExpriedTimeSec: number = config.service.captchaSession.expriedTimeSec;
private readonly RedisCommonKey: string = 'Service:captchaSession:';
private redisConnection?: Redis;
private captchaPool?: CaptchaPool;
constructor() {
new Promise(async (resolve) => {
if (this.useRedis) {
this.redisConnection = (await import('@lib/Database/RedisConnection')).default;
this.logger.info('Redis service has been enabled');
} else {
this.captchaPool = {};
this.logger.info('CaptchaPool init, Redis service is disabled');
// 需要手动进行过期项的处理
setInterval(() => {
this.logger.info('Starting automatic cleanup of expired items');
this.cleanupExpiredItems();
this.logger.info('Automatic cleanup completed');
}, 1000 * 60 * 30);
}
this.logger.info('Rotation image verification service has started');
resolve(undefined);
})
}
private cleanupExpiredItems(): void {
const now = Date.now();
for (const session in this.captchaPool) {
if (this.captchaPool.hasOwnProperty(session)) {
const item = this.captchaPool[session];
if (item && (now > item.expiredTimestamp)) {
delete this.captchaPool[session];
}
}
}
}
// get需要实现过期时间管理
private async get(session: string): Promise<CaptchaSessionRedisDataJSON | undefined> {
try {
if (this.useRedis) {
let redisRes = await this.redisConnection!.get(this.RedisCommonKey + session);
if (!redisRes)
return;
return JSON.parse(redisRes);
}
let poolRes = this.captchaPool![session];
if (!poolRes)
return;
// 如果不来自Redis则需要处理是否过期
if (!this.useRedis && Date.now() > poolRes.expiredTimestamp)
return;
return poolRes;
} catch (error) {
this.logger.error(`Error occurred while retrieving session [${session}]: ${error}`);
return;
}
}
private async remove(session: string): Promise<void> {
try {
if (this.useRedis)
await this.redisConnection!.del(this.RedisCommonKey + session);
else
delete this.captchaPool![session];
} catch (error) {
this.logger.error(`Failed to delete session [${session}]`);
}
}
/**
*
* @param session 验证会话标识符
* @param rotateDeg 图片旋转角度
* @returns true存储成功 false存储失败
*/
public async add(session: string, rotateDeg: number): Promise<boolean> {
const result: CaptchaSessionRedisDataJSON = {
rotateDeg: rotateDeg,
tryCount: 0,
isPassed: false,
}
try {
if (this.useRedis) {
const res = await this.redisConnection!.set(this.RedisCommonKey + session, JSON.stringify(result));
if (res && res === 'OK') {
this.redisConnection!.expire(this.RedisCommonKey + session, this.ExpriedTimeSec);
this.logger.info(`Session [${session}] and rotation angle [${rotateDeg}] have been stored`);
return true;
}
this.logger.error(`Failed to store session [${session}] and rotation angle [${rotateDeg}]`);
return false;
}
this.captchaPool![session] = {
...result,
expiredTimestamp: Date.now() + this.ExpriedTimeSec * 1000
}
this.logger.info(`Session [${session}] and rotation angle [${rotateDeg}] have been stored`);
return true;
} catch (error) {
this.logger.error(`Failed to store session [${session}] and rotation angle [${rotateDeg}]: ${error}`);
return false;
}
}
/**
*
* @param session 验证会话标识符
* @returns true已验证过期 false未通过
*/
public async isPassed(session: string): Promise<boolean> {
const result = await this.get(session);
if (!result)
return false;
if (result.isPassed)
this.remove(session);
return result.isPassed;
}
/**
*
* @param session 验证会话标识符
* @param rotateDeg 图片旋转角度
* @returns 0验证已过期或服务器错误 1通过验证 -1超过最大允许尝试次数 -2角度差异过大
*/
public async check(session: string, rotateDeg: number): Promise<number> {
try {
let result = await this.get(session);
if (!result) {
return 0;
}
if (result.isPassed) {
this.logger.info(`Session [${session}] has already been verified, no need to verify again`);
return 1;
}
if (Math.abs(result.rotateDeg - rotateDeg) <= this.AllowMaxAngleDiff) {
result.isPassed = true;
if (this.useRedis) {
await this.redisConnection!.del(this.RedisCommonKey + session);
await this.redisConnection!.set(this.RedisCommonKey + session, JSON.stringify(result));
this.redisConnection!.expire(this.RedisCommonKey + session, this.ExpriedTimeSec);
} else {
result.expiredTimestamp = Date.now() + this.ExpriedTimeSec * 1000;
}
return 1;
}
// 角度偏差过大,尝试次数记录+1
result.tryCount++;
if (result.tryCount >= this.AllowMaxTryCount) {
// 已达最大尝试次数
this.remove(session);
return -1;
}
// 允许下一次尝试
if (this.useRedis)
this.redisConnection!.set(this.RedisCommonKey + session, JSON.stringify(result));
return -2;
} catch (error) {
this.logger.error(`Error occurred while checking session [${session}]: ${error}`);
return 0;
}
}
}
const CaptchaSession = new _CaptchaSession();
export default CaptchaSession;