diff --git a/src/api/isPassedCaptcha.ts b/src/api/isPassedCaptcha.ts new file mode 100644 index 0000000..e10da76 --- /dev/null +++ b/src/api/isPassedCaptcha.ts @@ -0,0 +1,25 @@ +import { API, RequestData } from "@lib/API/API"; +import ServerStdResponse from "@lib/ServerResponse/ServerStdResponse"; +import { Response } from "express"; +import CaptchaSession from "@lib/Service/CaptchaSession"; +// 检查人机验证是否通过 +class isPassedCaptcha extends API { + constructor() { + super('GET', '/isPassedCaptcha'); + } + + public async onRequset(data: RequestData, res: Response): Promise { + // 获取session + const { session } = data; + if (!session) { + res.json({ ...ServerStdResponse.ERROR }); + return; + } + if (await CaptchaSession.isPassed(session)) + res.json(ServerStdResponse.OK); + else + res.json({ ...ServerStdResponse.ERROR, message: 'not passed' }) + } +} + +export default isPassedCaptcha; \ No newline at end of file diff --git a/src/config.ts b/src/config.ts index 106f4c1..7140989 100644 --- a/src/config.ts +++ b/src/config.ts @@ -18,6 +18,7 @@ const config = { }, service: { captchaSession: { + useRedis: false, allowMaxTryCount: 5, allowMaxAngleDiff: 8, expriedTimeSec: 60, diff --git a/src/index.ts b/src/index.ts index 41c9bf8..9c6fdca 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,6 +9,7 @@ RedisConnection import GetTest from "./api/GetTest"; import GetCaptcha from "./api/GetCaptcha"; import CheckCaptcha from "./api/CheckCaptcha"; +import isPassedCaptcha from "./api/isPassedCaptcha"; const logger = new Logger('Server') async function main(): Promise { @@ -18,6 +19,7 @@ async function main(): Promise { apiLoader.add(GetTest); apiLoader.add(GetCaptcha); apiLoader.add(CheckCaptcha); + apiLoader.add(isPassedCaptcha); await apiLoader.start(config.API_Port); logger.info('Server started successfully') diff --git a/src/lib/Service/CaptchaSession.ts b/src/lib/Service/CaptchaSession.ts index 9c416fd..4417d40 100644 --- a/src/lib/Service/CaptchaSession.ts +++ b/src/lib/Service/CaptchaSession.ts @@ -4,31 +4,82 @@ * @description 旋转图像验证服务 */ import Logger from '@lib/Logger/Logger'; -import RedisConnection from '@lib/Database/RedisConnection'; +import type { Redis } from '@lib/Database/RedisConnection'; import config from 'src/config'; -type CaptchaSessionDataJSON = { - rotateDeg: number, - tryCount: number, - isPassed: boolean +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() { - this.logger.info('Rotation image verification service has started'); + 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 async get(session: string): Promise { + 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 { - const result = await RedisConnection.get(this.RedisCommonKey + session); - if (result === null) + 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; - return JSON.parse(result); + // 如果不来自Redis,则需要处理是否过期 + if (!this.useRedis && Date.now() > poolRes.expiredTimestamp) + return; + return poolRes; } catch (error) { this.logger.error(`Error occurred while retrieving session [${session}]: ${error}`); return; @@ -37,7 +88,10 @@ class _CaptchaSession { private async remove(session: string): Promise { try { - await RedisConnection.del(this.RedisCommonKey + session); + 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}]`); } @@ -50,20 +104,29 @@ class _CaptchaSession { * @returns true存储成功 false存储失败 */ public async add(session: string, rotateDeg: number): Promise { - const result: CaptchaSessionDataJSON = { + const result: CaptchaSessionRedisDataJSON = { rotateDeg: rotateDeg, tryCount: 0, - isPassed: false + isPassed: false, } + try { - const res = await RedisConnection.set(this.RedisCommonKey + session, JSON.stringify(result)); - if (res && res === 'OK') { - RedisConnection.expire(this.RedisCommonKey + session, this.ExpriedTimeSec); - this.logger.info(`Session [${session}] and rotation angle [${rotateDeg}] have been stored`); - return true; + 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.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; @@ -102,17 +165,25 @@ class _CaptchaSession { } if (Math.abs(result.rotateDeg - rotateDeg) <= this.AllowMaxAngleDiff) { result.isPassed = true; - await RedisConnection.del(this.RedisCommonKey + session); - await RedisConnection.set(this.RedisCommonKey + session, JSON.stringify(result)); - RedisConnection.expire(this.RedisCommonKey + session, this.ExpriedTimeSec); + 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; } - RedisConnection.set(this.RedisCommonKey + session, JSON.stringify(result)); + // 允许下一次尝试 + 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}`);