/** * @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 { 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 { 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 { 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 { 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 { 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;