196 lines
7.3 KiB
TypeScript
196 lines
7.3 KiB
TypeScript
/**
|
||
* @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; |