后端加入Redis、旋转图片验证接口

This commit is contained in:
2024-08-30 21:34:24 +08:00
parent 41b7a38669
commit e9a8ad6717
14 changed files with 545 additions and 2 deletions

View File

@@ -0,0 +1,41 @@
import { API } from "../Plugs/API/API";
import ServerStdResponse from "../ServerStdResponse";
import captchaSession from "../Plugs/Service/captchaSession";
// 检查人机验证
class CheckCaptcha extends API {
constructor() {
super('POST', '/checkCaptcha');
}
public async onRequset(data: any, res: any) {
let { session, rotateDeg } = data;
if (!session || !rotateDeg) {
return res.json(ServerStdResponse.PARAMS_MISSING);
}
switch (await captchaSession.check(session, rotateDeg)) {
case 0:
// 验证码已过期或服务器错误
res.json(ServerStdResponse.CAPTCHA.NOTFOUND);
break;
case 1:
// 验证通过
res.json(ServerStdResponse.OK);
break;
case -1:
// 超过最大尝试次数
res.json(ServerStdResponse.CAPTCHA.MAX_TRY_COUNT);
break;
case -2:
// 角度不正确
res.json(ServerStdResponse.CAPTCHA.NOTRIGHT);
break;
default:
// 未知错误
res.json(ServerStdResponse.SERVER_ERROR);
break;
}
}
}
export default CheckCaptcha;

View File

@@ -0,0 +1,36 @@
import fs from "fs";
import { API } from "../Plugs/API/API";
import ServerStdResponse from "../ServerStdResponse";
import captchaSession from "../Plugs/Service/captchaSession";
import path from "path";
import sharp from "sharp";
import crypto from 'crypto'
// 获取人机验证图片及标识符
class GetCaptcha extends API {
constructor() {
super('GET', '/captcha');
}
public async onRequset(data: any, res: any) {
const imgsPath = path.join(__dirname, '../assets/captchaImgs');
const fileList = fs.readdirSync(imgsPath)
const imgPath = path.join(imgsPath, fileList[Math.floor(Math.random() * fileList.length)]);
const rotateDeg = Math.floor(Math.random() * 240) + 60;
const img = Buffer.from(await sharp(imgPath).rotate(-rotateDeg).toBuffer()).toString('base64')
const session = crypto.createHash('md5').update(`${Math.random()} ${Date.now()}`).digest('hex');
if (await captchaSession.add(session, rotateDeg)) {
return res.json({
...ServerStdResponse.OK, data: {
img: img,
session: session,
imgPreStr: 'data:image/jpeg;base64,'
}
});
} else {
return res.json(ServerStdResponse.SERVER_ERROR)
}
}
}
export default GetCaptcha;

View File

@@ -0,0 +1,43 @@
import Redis from 'ioredis';
import config from '../config';
import Logger from './Logger';
class RedisConnection {
private pool?: Redis
private logger = new Logger('Redis')
constructor() {
try {
this.pool = new Redis({
port: config.redis.port,
host: config.redis.host,
password: config.redis.password,
maxRetriesPerRequest: 10,
});
this.logger.info('数据库连接池已创建')
} catch (error) {
this.logger.error('数据库连接池创建失败:' + error)
}
setTimeout(async () => {
if(this.pool == undefined)
return;
try {
let res = await this.pool.set('redis_test', '1');
if (res)
this.logger.info('数据库测试成功')
else
throw new Error('返回值错误')
} catch (error) {
this.logger.error('数据库测试失败:' + error)
}
}, 10);
}
public getPool(): Redis {
return <Redis>this.pool;
}
}
const redisConnection = new RedisConnection();
export default redisConnection.getPool();

View File

@@ -0,0 +1,114 @@
import Logger from '../Logger';
import RedisConnection from '../RedisConnection';
class _captchaSession {
private logger = new Logger('Service][captchaSession');
private AllowMaxTryCount: number = 5;
private AllowMaxAngleDiff: number = 8;
private ExpriedTimeSec: number = 60;
private RedisCommonKey: string = 'Service:captchaSession:';
constructor() {
this.logger.info('旋转图像验证服务已启动');
}
private async get(session: string) {
try {
const result = await RedisConnection.get(this.RedisCommonKey + session);
if(result === null)
return;
return JSON.parse(result);
} catch (error) {
this.logger.error(`获取session[${session}]时发生错误:${error}`);
return;
}
}
private async remove(session: string) {
try {
await RedisConnection.del(this.RedisCommonKey + session);
} catch (error) {
this.logger.error(`删除session[${session}]失败`);
}
}
/**
*
* @param session 验证会话标识符
* @param rotateDeg 图片旋转角度
* @returns true存储成功 false存储失败
*/
public async add(session: string, rotateDeg: number): Promise<boolean> {
const result = {
rotateDeg: rotateDeg,
tryCount: 0,
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}]及角度[${rotateDeg}]已存储`);
return true;
}
this.logger.error(`session[${session}]及角度[${rotateDeg}]存储失败`);
return false;
} catch (error) {
this.logger.error(`session[${session}]及角度[${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}]已通过验证,无需重复验证`);
return 1;
}
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);
return 1;
}
result.tryCount++;
if(result.tryCount >= this.AllowMaxTryCount) {
this.remove(session);
return -1;
}
RedisConnection.set(this.RedisCommonKey + session, JSON.stringify(result));
return -2;
} catch (error) {
this.logger.error(`检查session[${session}]时发生错误:${error}`);
return 0;
}
}
}
const captchaSession = new _captchaSession();
export default captchaSession;

View File

@@ -2,11 +2,17 @@ import Logger from "../Plugs/Logger";
import { APILoader } from "../Plugs/API/APILoader";
import config from "../config";
// 加载Plugs
import '../Plugs/Service/captchaSession'
// 加载API
import GetTest from "../APIs/GetTest";
import GetResourceList from "../APIs/GetResourceList";
import GetBlogList from "../APIs/GetBlogList";
import GetBlogContent from "../APIs/GetBlogContent";
import BlogLike from "../APIs/BlogLike";
import GetCaptcha from "../APIs/GetCaptcha";
import CheckCaptcha from "../APIs/CheckCaptcha";
class Server {
private logger = new Logger('Server');
@@ -18,12 +24,14 @@ class Server {
}
public async start() {
// 加载前台API
// 加载API
this.apiLoader.add(GetTest);
this.apiLoader.add(GetResourceList);
this.apiLoader.add(GetBlogList);
this.apiLoader.add(GetBlogContent);
this.apiLoader.add(BlogLike);
this.apiLoader.add(GetCaptcha);
this.apiLoader.add(CheckCaptcha);
this.apiLoader.start(config.apiPort);
}

View File

@@ -29,6 +29,20 @@ const ServerStdResponse = {
message: 'Blog not found'
}
},
CAPTCHA: {
NOTFOUND: {
code: -5000,
message: 'captcha session not found'
},
MAX_TRY_COUNT: {
code: -5001,
message: 'captcha session try max count'
},
NOTRIGHT: {
code: -5002,
message: 'captcha is not right, please try again'
}
}
} as const;
export default ServerStdResponse;

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB