后端加入Redis、旋转图片验证接口
This commit is contained in:
41
Server/src/APIs/CheckCaptcha.ts
Normal file
41
Server/src/APIs/CheckCaptcha.ts
Normal 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;
|
||||
36
Server/src/APIs/GetCaptcha.ts
Normal file
36
Server/src/APIs/GetCaptcha.ts
Normal 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;
|
||||
43
Server/src/Plugs/RedisConnection.ts
Normal file
43
Server/src/Plugs/RedisConnection.ts
Normal 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();
|
||||
114
Server/src/Plugs/Service/captchaSession.ts
Normal file
114
Server/src/Plugs/Service/captchaSession.ts
Normal 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;
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
BIN
Server/src/assets/captchaImgs/1.jpg
Normal file
BIN
Server/src/assets/captchaImgs/1.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 141 KiB |
BIN
Server/src/assets/captchaImgs/2.jpg
Normal file
BIN
Server/src/assets/captchaImgs/2.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 116 KiB |
BIN
Server/src/assets/captchaImgs/3.jpg
Normal file
BIN
Server/src/assets/captchaImgs/3.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 120 KiB |
BIN
Server/src/assets/captchaImgs/4.jpg
Normal file
BIN
Server/src/assets/captchaImgs/4.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 134 KiB |
BIN
Server/src/assets/captchaImgs/5.jpg
Normal file
BIN
Server/src/assets/captchaImgs/5.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 142 KiB |
BIN
Server/src/assets/captchaImgs/6.jpg
Normal file
BIN
Server/src/assets/captchaImgs/6.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 110 KiB |
Reference in New Issue
Block a user