From 14f5711e8735ac15334e27f552b6377139c52970 Mon Sep 17 00:00:00 2001 From: expressgy Date: Mon, 27 Mar 2023 19:31:17 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E6=88=90=E4=B8=A4=E4=B8=AA=E7=99=BB?= =?UTF-8?q?=E5=BD=95=E6=8E=A5=E5=8F=A3=E5=92=8C=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + config/development.ts | 11 +- config/production.ts | 7 + src/Gservice/GEMAIL/gemail.service.ts | 86 ++++++++ src/Gservice/GREDIS/gredis.service.ts | 238 ++++++++++++++++++++- src/Gservice/GTOOLS/gtools.service.ts | 4 +- src/starlight/dto/signIn.dto.ts | 16 +- src/starlight/starlight.controller.ts | 26 ++- src/starlight/starlight.service.ts | 288 ++++++++++++++++++++++++-- 9 files changed, 651 insertions(+), 26 deletions(-) diff --git a/.gitignore b/.gitignore index 07f2afc..e74865e 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ pnpm-debug.log* yarn-debug.log* yarn-error.log* lerna-debug.log* +pnpm-lock.yaml # OS .DS_Store diff --git a/config/development.ts b/config/development.ts index 4ff06ad..9187605 100644 --- a/config/development.ts +++ b/config/development.ts @@ -20,7 +20,7 @@ export default { host: 'localhost', port: 3306, username: 'root', - password: 'Hxl1314521', + password: 'root', database: 'Starlight', }, }, @@ -47,4 +47,13 @@ export default { timeout: 1000 * 60 * 60 * 24 * 7, secretKey: 'Missing You!(John waite)', }, + // 登录 + signIn: { + // @ 密码验证次数超限后的冷却时长 + signInErrorTimeout: 60 * 10, + // @ 允许密码输错几次 + signInErrorNumber: 5, + // @ 允许同时在线数 + onLineNumber: 6, + }, }; diff --git a/config/production.ts b/config/production.ts index 622db12..63b74e3 100644 --- a/config/production.ts +++ b/config/production.ts @@ -47,4 +47,11 @@ export default { timeout: 1000 * 60 * 60 * 24 * 7, secretKey: 'Missing You!(John waite)', }, + // 登录 + signIn: { + // @ 密码验证次数超限后的冷却时长 + signInErrorTimeout: 60 * 10, + // @ 允许密码输错几次 + signInErrorNumber: 5, + }, }; diff --git a/src/Gservice/GEMAIL/gemail.service.ts b/src/Gservice/GEMAIL/gemail.service.ts index 2084043..5171a8e 100644 --- a/src/Gservice/GEMAIL/gemail.service.ts +++ b/src/Gservice/GEMAIL/gemail.service.ts @@ -228,4 +228,90 @@ export class GemailService { } }); } + + // ? ? + // ? 函数名称: sendSignInCodeMail(邮箱, 验证码) + // ? 函数描述: 给制定邮箱发送注册验证码 + // ? ? + public sendSignInCodeMail(email, code) { + return new Promise(async (res, rej) => { + const test = ` + +
+
+
我们欢迎您使用${this.sysName}
+
您在某些地方请求了邮箱的验证码,如果不是自己操作请修改账户的密码。
+
+
${code}
+
+
此邮件作用于账户登录
截止邮件发送时间5分钟内使用有效。
+
+
+ `; + try { + console.time('EMAIL'); + const resd = await this.senMail(test, email); + console.timeEnd('EMAIL'); + res(resd); + } catch (e) { + rej({ + data: e, + message: '发送邮件失败!', + }); + } + }); + } } diff --git a/src/Gservice/GREDIS/gredis.service.ts b/src/Gservice/GREDIS/gredis.service.ts index b0b6d70..09d4fc2 100644 --- a/src/Gservice/GREDIS/gredis.service.ts +++ b/src/Gservice/GREDIS/gredis.service.ts @@ -1,13 +1,18 @@ import { Injectable } from '@nestjs/common'; import { createClient } from 'redis'; import config from '../../../config'; -import { GtoolsService } from '@/Gservice/GTOOLS/gtools.service'; +import { GtoolsService, HASHT } from '@/Gservice/GTOOLS/gtools.service'; export declare interface ISetRegisterEmailCode { data: any; message: string; registerCode?: string; } +export declare interface ISetSignInEmailCode { + data: any; + message: string; + signInCode?: string; +} @Injectable() export class GredisService { @@ -15,11 +20,18 @@ export class GredisService { private config; // @ 专注于注册的数据库 private readonly UserRegisterPoll = 1; + // @ 专注于登录的数据库 + private readonly UserSignInPoll = 2; + // @ 登录的相关配置文件 + private readonly SignInCFG = config().signIn; + // @ Token相关的配置文件 + private readonly TokenCFG = config().token; constructor(private readonly gtools: GtoolsService) { this.config = config().redis.starLight; this.start(); } + //#region 注册 // ? ? // ? 函数名称: start // ? 函数描述: 连接redis @@ -31,6 +43,7 @@ export class GredisService { await client.connect(); this.DB = client; } + // ? ? // ? 函数名称: setRegisterEmailCode(邮箱) // ? 函数描述: 设置注册邮箱验证 @@ -87,12 +100,12 @@ export class GredisService { const result = await this.DB.get(key); if (result === null) { rej({ - data: {}, + data: null, message: '未找到相匹配的验证码!', }); } else { res({ - data: {}, + data: null, message: '获取成功', registerCode: result as string, }); @@ -100,11 +113,12 @@ export class GredisService { } catch (e) { rej({ data: e, - message: '从Redis获取注册吗出现错误!', + message: '从Redis获取注册码出现错误!', }); } }); } + // ? ? // ? 函数名称: removeRegisterEmailCode // ? 函数描述: @@ -127,4 +141,220 @@ export class GredisService { } }); } + //#endregion + + //#region 密码登录 + // ? ? + // ? 函数名称: getSignInErrorNumber + // ? 函数描述: 获取异常登录信息 + // ? ? + public async getSignInErrorNumber(uuid) { + const key = 'SIN' + uuid; + this.DB.select(this.UserSignInPoll); + try { + const result = await this.DB.get(key); + if ( + result === null || + Number(result) < this.SignInCFG.signInErrorNumber + ) { + return { + state: true, + message: '获取成功', + error: null, + }; + } else { + const ttl = await this.DB.ttl(key); + return { + state: false, + message: '登陆异常已达上限,请稍后重试!', + ttl, + error: null, + }; + } + } catch (e) { + return { + state: false, + message: '查找登录异常数据出错!', + error: e, + }; + } + } + + // ? ? + // ? 函数名称: setSignInErrorNumber + // ? 函数描述: 设置用户登陆异常数字 + // ? ? + public async setSignInErrorNumber(uuid) { + const key = 'SIN' + uuid; + this.DB.select(this.UserSignInPoll); + try { + const exist = await this.DB.exists(key); + if (exist == 0) { + console.log(key); + await this.DB.set(key, 1); + await this.DB.expire(key, this.SignInCFG.signInErrorTimeout); + return { + error: null, + number: 1, + state: true, + }; + } else { + const number = await this.DB.get(key); + await this.DB.set(key, Number(number) + 1); + await this.DB.expire(key, this.SignInCFG.signInErrorTimeout); + return { + error: null, + number: Number(number) + 1, + state: true, + }; + } + } catch (e) { + return { + error: e, + message: '设置登陆异常数量出现错误!', + }; + } + } + + // ? ? + // ? 函数名称: setToken + // ? 函数描述: 设置Token到Redis + // ? ? + public async setToken(uuid, token) { + this.DB.select(this.UserSignInPoll); + const tokenKey = + 'TK' + this.gtools.makeHASH(token, HASHT.MD5).slice(0, 32); + const uuidKey = 'UK' + uuid; + await this.DB.setEx(tokenKey, this.TokenCFG.timeout / 1000, uuid); + + // 获取此用户的登陆数量 + let signInNumber: number; + try { + signInNumber = await this.DB.zCard(uuidKey); + } catch (e) { + return { + state: false, + message: '获取登陆数量出错!', + error: e, + }; + } + // 写入Token有序集合 + try { + const score = new Date().getTime(); + await this.DB.zAdd(uuidKey, [{ score, value: tokenKey }]); + } catch (e) { + return { + state: false, + message: '写入Token有序集合出错!', + error: e, + }; + } + // 清除最早的TokenKey和uuidKey + try { + if (signInNumber >= this.SignInCFG.onLineNumber) { + // 超出范围内 + // 删除最先的 + const uuidKeyZList = await this.DB.zRange( + uuidKey, + 0, + signInNumber, + ); + // 删除第一个 + const popTokenKey = await this.DB.zRemRangeByRank( + uuidKey, + 0, + 0, + ); + // 删除token + const delUuidKey = await this.DB.del(uuidKeyZList[0]); + // 加入新的 + } + } catch (e) { + return { + state: false, + message: '清除最早的TokenKey和uuidKey出错!', + error: e, + }; + } + return { + data: { + tokenKey, + uuidKey, + }, + state: true, + message: '设置成功!', + }; + } + + // ? ? + // ? 函数名称: setSignInEmailCode(邮箱) + // ? 函数描述: 设置邮箱登录验证 + // ? ? + public setSignInEmailCode(email): Promise { + return new Promise(async (res, rej) => { + this.DB.select(this.UserSignInPoll); + try { + const key = 'SignIn-' + email; + const result = await this.DB.get(key); + if (result === null) { + const signInCode = this.gtools.getRandomString(6); + try { + const data = await this.DB.setEx(key, 300, signInCode); + res({ + data, + signInCode, + message: '生成验证码成功。', + }); + } catch (e) { + rej({ + data: e, + message: '生成验证码失败。', + }); + } + } else { + rej({ + data: null, + message: '已发送过邮件,请稍后再重试。', + }); + } + } catch (e) { + rej({ + data: e, + message: '查找登录码失败。', + }); + } + }); + } + + // ? ? + // ? 函数名称: getSignInEmailEntryCode(邮箱) + // ? 函数描述: 读取登录验证码 + // ? ? + public getSignInEmailEntryCode(email): Promise { + return new Promise(async (res, rej) => { + this.DB.select(this.UserSignInPoll); + const key = 'SignIn-' + email; + try { + const result = await this.DB.get(key); + if (result === null) { + rej({ + data: null, + message: '未找到相匹配的验证码!', + }); + } else { + res({ + data: null, + message: '获取成功', + signInCode: result as string, + }); + } + } catch (e) { + rej({ + data: e, + message: '从Redis获取登录码出现错误!', + }); + } + }); + } + //#endregion } diff --git a/src/Gservice/GTOOLS/gtools.service.ts b/src/Gservice/GTOOLS/gtools.service.ts index 1d9847d..9a12df5 100644 --- a/src/Gservice/GTOOLS/gtools.service.ts +++ b/src/Gservice/GTOOLS/gtools.service.ts @@ -4,13 +4,13 @@ import config from '@CFG/index'; import * as jwt from 'jsonwebtoken'; // @ 不可逆加密 -enum HASHT { +export enum HASHT { MD5 = 'md5', // 32位 SHA256 = 'sha256', // 64位 SHA512 = 'sha512', // 128位 } // @ 可逆加密 -enum AEST { +export enum AEST { AES128 = 'aes-128-cbc', AES256 = 'aes-256-gcm', } diff --git a/src/starlight/dto/signIn.dto.ts b/src/starlight/dto/signIn.dto.ts index 8f1c574..157ccd8 100644 --- a/src/starlight/dto/signIn.dto.ts +++ b/src/starlight/dto/signIn.dto.ts @@ -5,8 +5,9 @@ * * @date: 2023/3/26 22:41 * * */ -import { Length } from 'class-validator'; +import { IsEmail, IsString, Length } from 'class-validator'; +// @ 密码登录 export class SignInPasswdEntryDto { // @ 用户名 @Length(8, 128, { message: '请将用户名长度控制在8到128位之间!' }) @@ -16,3 +17,16 @@ export class SignInPasswdEntryDto { @Length(8, 128, { message: '请将密码长度控制在8到128位之间!' }) password: string; } + +// 邮箱登录 +export class SignInEmailEntryDto { + // @ 邮箱 + @Length(8, 128, { message: '请将邮箱长度控制在8到128位之间!' }) + @IsEmail({}, { message: '邮箱格式错误!' }) + @IsString({ message: '邮箱应为字符串格式!' }) + email: string; + + // @ 验证码 + @Length(6, 6, { message: '验证码是6位字符!' }) + code: string; +} diff --git a/src/starlight/starlight.controller.ts b/src/starlight/starlight.controller.ts index f502866..0742c72 100644 --- a/src/starlight/starlight.controller.ts +++ b/src/starlight/starlight.controller.ts @@ -34,7 +34,10 @@ import { RegisterEmailCheckoutUsernameDto, RegisterEmailSignUpDto, } from '@/starlight/dto/register.dto'; -import { SignInPasswdEntryDto } from '@/starlight/dto/signIn.dto'; +import { + SignInEmailEntryDto, + SignInPasswdEntryDto, +} from '@/starlight/dto/signIn.dto'; import { VerifyGuard } from '@/starlight/verify.guard'; // nest g d [name] @@ -119,6 +122,26 @@ export class StarlightController { return this.starlightService.signInPasswdEntry(body); } + // ? ? + // ? 函数名称: signInEmailSendCode + // ? 函数描述: 获取邮箱登录验证码 + // ? ? + @Get('signIn/email/sendCode/:email') + signInEmailSendCode( + @Param() params: RegisterEmailCheckoutEmailDto, + ): Promise { + return this.starlightService.signInEmailSendCode(params); + } + + // ? ? + // ? 函数名称: signInEmailEntry + // ? 函数描述: 邮箱验证登录 + // ? ? + @Post('signIn/email/entry') + signInEmailEntry(@Body() body: SignInEmailEntryDto): Promise { + return this.starlightService.signInEmailEntry(body); + } + // ? ? // ? 函数名称: // ? 函数描述: 测试Token @@ -129,6 +152,7 @@ export class StarlightController { this.logger.info(user); return {}; } + //#endredion //#region 测试 diff --git a/src/starlight/starlight.service.ts b/src/starlight/starlight.service.ts index 16d9315..a9e9fc7 100644 --- a/src/starlight/starlight.service.ts +++ b/src/starlight/starlight.service.ts @@ -4,7 +4,7 @@ * * @author: x7129 * * @date: 2023-03-23 17:44 * * */ -import { Injectable } from '@nestjs/common'; +import { Injectable, UnauthorizedException } from '@nestjs/common'; import { CreateStarlightDto } from './dto/create-starlight.dto'; import { UpdateStarlightDto } from './dto/update-starlight.dto'; import { @@ -19,7 +19,10 @@ import { GdatabaseService } from '@/Gservice/GDATABASE/gdatabase.service'; import { GredisService } from '@/Gservice/GREDIS/gredis.service'; import { GemailService } from '@/Gservice/GEMAIL/gemail.service'; import { GtoolsService } from '@/Gservice/GTOOLS/gtools.service'; -import { SignInPasswdEntryDto } from '@/starlight/dto/signIn.dto'; +import { + SignInEmailEntryDto, + SignInPasswdEntryDto, +} from '@/starlight/dto/signIn.dto'; // C C // C 类名称: StarlightService @@ -53,7 +56,7 @@ export class StarlightService { // ! 从数据库用户身份表查询有没有已经使用的邮箱 const [rows] = await this.database.DB.execute( `SELECT * FROM user_info_verify WHERE email = ? AND state = 0`, - [params.email.trim()], + [params.email.trim().toLowerCase()], ); // ! 判断是否存在此邮箱 const resd = { @@ -79,7 +82,7 @@ export class StarlightService { // ! 从数据库用户身份表查询有没有已经使用的用户名 const [rows] = await this.database.DB.execute( `SELECT * FROM user_info_verify WHERE username = ? AND state = 0`, - [params.username.trim()], + [params.username.trim().toLowerCase()], ); // ! 判断是否存在此用户名 const resd = { @@ -98,7 +101,8 @@ export class StarlightService { // ? 函数描述: 发送邮箱注册验证码 // ? ? public async registerEmailSendCode(params: RegisterEmailCheckoutEmailDto) { - const { email } = params; + let { email } = params; + email = email.trim().toLowerCase(); // ! 1. 验证是否存在已经注册的 const checkoutEmail = await this.registerEmailCheckoutEmail({ email, @@ -165,6 +169,9 @@ export class StarlightService { return resd; } } catch (e) { + if (e.data != null) { + this.logger.error(e); + } resd.message = e.message; return resd; } @@ -182,9 +189,9 @@ export class StarlightService { const uuid = this.tools.makeUUID(); const createTime = new Date(); const s = { - email: email.trim(), + email: email.trim().toLowerCase(), username: username.trim(), - realname: body.realname?.trim(), + realname: body.realname?.trim().toLowerCase(), nickname: body.nickname?.trim(), birthday: new Date(body.birthday), sex: sex[body.sex], @@ -315,28 +322,275 @@ export class StarlightService { // ? ? public async signInPasswdEntry(body: SignInPasswdEntryDto) { const { username, password } = body; - // ! 加密密码 - const passwordHash = this.tools.makeHASH(password); - // ! 验证密码 - // 查找最后一条 - // ! 生成token + const resd = { + data: {}, + message: '未找到该用户信息!', + success: false, + }; + // ! 1. 从库中获取uuid + const getUserUUIDSQL = `SELECT uuid from user_info_verify where username = ? AND state = 0 ORDER BY id desc limit 1;`; + let uuid: string = null; + try { + const [rows] = await this.database.DB.execute(getUserUUIDSQL, [ + username.trim().toLowerCase(), + ]); + if (rows.length == 0) { + return resd; + } + uuid = rows[0].uuid; + } catch (e) { + const message = '查找用户UUID出错!'; + resd.message = message; + this.logger.error({ + message, + data: e, + }); + } + // ! 2. 判断是否存在用户 + // ! 2.1 查找登陆异常数量Redis + const errorNumber = await this.redis.getSignInErrorNumber(uuid); + if (!errorNumber.state) { + const message = errorNumber.message; + if (!errorNumber.ttl) { + this.logger.warn({ + message, + e: errorNumber.error, + }); + } + + if (errorNumber.ttl) { + resd.data = { + ttl: errorNumber.ttl, + }; + } + resd.message = message; + return resd; + } + // ! 3. 加密密码 + const passwordHASH = this.tools.makeHASH(password.trim()); + // ! 4. 查找,比对密码 + let sqlPassword; + const getPasswordSQL = `SELECT passwd FROM user_info_passwd WHERE uuid = ? ORDER BY id desc limit 1;`; + try { + const [rows] = await this.database.DB.execute(getPasswordSQL, [ + uuid, + ]); + if (rows.length == 0) { + resd.message = '未找到密码!'; + return resd; + } else { + this.logger.info(passwordHASH, rows[0]); + if (passwordHASH != rows[0].passwd) { + // ! 5. 登陆异常累加器 + const setErrorNumber = + await this.redis.setSignInErrorNumber(uuid); + if (!setErrorNumber.state) { + const message = setErrorNumber.message; + this.logger.error({ + data: setErrorNumber.error, + message, + }); + resd.message = message; + return resd; + } else { + resd.message = '账户和密码不匹配,请重试。'; + resd.data = { + number: setErrorNumber.number, + }; + return resd; + } + } + } + } catch (e) { + const message = '查找用户密码时出错'; + this.logger.error({ + data: e, + message, + }); + resd.message = message; + return resd; + } + // ! 6. 创建Token const token = this.tools.createToken({ username, + uuid, signInTime: new Date().getTime(), }); - this.logger.info(token); - // ! Redis 登陆存储策略 - // ! 返回数据 + // ! 7. Redis存储策略 + const setToken = await this.redis.setToken(uuid, token); + if (!setToken.state) { + const message = setToken.message; + this.logger.error({ + data: setToken.error, + message, + }); + resd.message = message; + return resd; + } return { message: '登陆成功', data: { - token: '', + token, + tokenKey: setToken.data.tokenKey, }, }; } - //#endredion + // ? ? + // ? 函数名称: signInEmailSendCode + // ? 函数描述: 获取邮箱登录验证码 + // ? ? + public async signInEmailSendCode(params: RegisterEmailCheckoutEmailDto) { + const email = params.email.trim().toLowerCase(); + // ! 1. 验证是否存在已经注册的 + const checkoutEmail = await this.registerEmailCheckoutEmail({ + email, + }); + const resd = { + data: {}, + message: '不存在此邮箱账户!', + success: false, + }; + if (checkoutEmail.success) { + return resd; + } + // ! 2. 验证是否存在验证码 + let registerCode: any; + try { + const redisResd = await this.redis.setSignInEmailCode(email); + registerCode = redisResd.signInCode as string; + } catch (e) { + this.logger.error(e); + resd.message = e.message; + return resd; + } + // ! 3. 发送验证码 + try { + const result = await this.email.sendSignInCodeMail( + email, + registerCode, + ); + resd.success = true; + resd.message = '发送验证码成功,请注意查收!'; + return resd; + } catch (e) { + this.logger.error(e); + resd.message = e.message; + return resd; + } + return {}; + } + + // ? ? + // ? 函数名称: signInEmailEntry + // ? 函数描述: 邮箱验证登录 + // ? ? + public async signInEmailEntry(body: SignInEmailEntryDto) { + let { email, code } = body; + email = email.trim().toLowerCase(); + code = code.trim().toUpperCase(); + const resd = { + data: {}, + message: '未找到该用户信息!', + success: false, + }; + // ! 1.从库中获取UUID + const getUserUUIDSQL = `SELECT uuid from user_info_verify where email = ? AND state = 0 ORDER BY id desc limit 1;`; + let uuid: string = null; + try { + const [rows] = await this.database.DB.execute(getUserUUIDSQL, [ + email.trim().toLowerCase(), + ]); + if (rows.length == 0) { + return resd; + } + uuid = rows[0].uuid; + } catch (e) { + const message = '查找用户UUID出错!'; + resd.message = message; + this.logger.error({ + message, + data: e, + }); + } + // ! 2. 查找登陆异常数量Redis + const errorNumber = await this.redis.getSignInErrorNumber(uuid); + if (!errorNumber.state) { + const message = errorNumber.message; + if (!errorNumber.ttl) { + this.logger.warn({ + message, + e: errorNumber.error, + }); + } + + if (errorNumber.ttl) { + resd.data = { + ttl: errorNumber.ttl, + }; + } + resd.message = message; + return resd; + } + // ! 3. 获取、比对验证码 + try { + const { signInCode } = await this.redis.getSignInEmailEntryCode( + email, + ); + if (signInCode != code) { + const setErrorNumber = await this.redis.setSignInErrorNumber( + uuid, + ); + if (!setErrorNumber.state) { + const message = setErrorNumber.message; + this.logger.error({ + data: setErrorNumber.error, + message, + }); + resd.message = message; + return resd; + } else { + resd.message = '验证码不匹配!'; + resd.data = { + number: setErrorNumber.number, + }; + return resd; + } + } + } catch (e) { + if (e.data != null) { + this.logger.error(e); + } + resd.message = e.message; + return resd; + } + // ! 6. 创建Token + const token = this.tools.createToken({ + email, + uuid, + signInTime: new Date().getTime(), + }); + // ! 7. Redis存储策略 + const setToken = await this.redis.setToken(uuid, token); + if (!setToken.state) { + const message = setToken.message; + this.logger.error({ + data: setToken.error, + message, + }); + resd.message = message; + return resd; + } + return { + message: '登陆成功', + data: { + token, + tokenKey: setToken.data.tokenKey, + }, + }; + } + //#endregion //#region 测试啊