From 1c828f49c975d1b3e1c6c4b34054ee3417a0bfe5 Mon Sep 17 00:00:00 2001 From: cll <931958862@qq.com> Date: Tue, 29 Jul 2025 17:30:17 +0800 Subject: [PATCH] =?UTF-8?q?=E8=AE=BE=E5=A4=87=E7=99=BD=E5=90=8D=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 12 +------ src/config/config.default.ts | 13 ++++++++ src/controller/user.controller.ts | 8 ++--- src/entity/auth_code.ts | 13 ++++++++ src/entity/device_whitelist.ts | 14 ++++++++ src/middleware/auth.middleware.ts | 28 ++++++++++++++++ src/service/authCode.service.ts | 46 ++++++++++++++++++++++++++ src/service/deviceWhitelist.service.ts | 33 ++++++++++++++++++ src/service/mail.service.ts | 29 ++++++++++++++++ src/service/user.service.ts | 46 ++++++++++++++++++++++++-- 10 files changed, 224 insertions(+), 18 deletions(-) create mode 100644 src/entity/auth_code.ts create mode 100644 src/entity/device_whitelist.ts create mode 100644 src/service/authCode.service.ts create mode 100644 src/service/deviceWhitelist.service.ts create mode 100644 src/service/mail.service.ts diff --git a/package.json b/package.json index 3310d10..947b071 100644 --- a/package.json +++ b/package.json @@ -21,21 +21,11 @@ "class-transformer": "^0.5.1", "dayjs": "^1.11.13", "mysql2": "^3.11.5", + "nodemailer": "^7.0.5", "swagger-ui-dist": "^5.18.2", "typeorm": "^0.3.20", "xml2js": "^0.6.2" }, - "devDependencies": { - "@midwayjs/mock": "^3.20.0", - "@types/jest": "^29.2.0", - "@types/node": "14", - "cross-env": "^6.0.0", - "jest": "^29.2.2", - "mwts": "^1.3.0", - "mwtsc": "^1.4.0", - "ts-jest": "^29.0.3", - "typescript": "~4.8.0" - }, "engines": { "node": ">=12.0.0" }, diff --git a/src/config/config.default.ts b/src/config/config.default.ts index 55909ad..6fe7ac8 100644 --- a/src/config/config.default.ts +++ b/src/config/config.default.ts @@ -29,6 +29,8 @@ import { Strength } from '../entity/strength.entity'; import { Flavors } from '../entity/flavors.entity'; import { CustomerTag } from '../entity/customer_tag.entity'; import { Customer } from '../entity/customer.entity'; +import { DeviceWhitelist } from '../entity/device_whitelist'; +import { AuthCode } from '../entity/auth_code'; export default { // use for cookie sign key, should change to your own and keep security @@ -66,6 +68,8 @@ export default { TransferItem, CustomerTag, Customer, + DeviceWhitelist, + AuthCode, ], synchronize: true, logging: false, @@ -110,4 +114,13 @@ export default { addSecurityRequirements: true, }, }, + mailer: { + host: 'smtphz.qiye.163.com', + port: 465, + secure: true, + auth: { + user: 'info@canpouches.com', + pass: 'WWqQ4aZq4Jrm9uwz', + }, +} } as MidwayConfig; diff --git a/src/controller/user.controller.ts b/src/controller/user.controller.ts index 9bce0d5..805f6a5 100644 --- a/src/controller/user.controller.ts +++ b/src/controller/user.controller.ts @@ -5,7 +5,6 @@ import { UserService } from '../service/user.service'; import { errorResponse, successResponse } from '../utils/response.util'; import { ApiOkResponse } from '@midwayjs/swagger'; import { BooleanRes, LoginRes } from '../dto/reponse.dto'; -import { LoginDTO } from '../dto/user.dto'; import { User } from '../decorator/user.decorator'; @Controller('/user') @@ -17,13 +16,12 @@ export class UserController { type: LoginRes, }) @Post('/login') - async login(@Body() body: LoginDTO) { - const { username, password } = body; + async login(@Body() body) { try { - const result = await this.userService.login(username, password); + const result = await this.userService.login(body); return successResponse(result, '登录成功'); } catch (error) { - return errorResponse(error?.message || '登录失败'); + return errorResponse(error?.message || '登录失败', error?.code); } } diff --git a/src/entity/auth_code.ts b/src/entity/auth_code.ts new file mode 100644 index 0000000..da76af1 --- /dev/null +++ b/src/entity/auth_code.ts @@ -0,0 +1,13 @@ +import { Entity, Column, PrimaryColumn } from 'typeorm'; + +@Entity('auth_code') +export class AuthCode { + @PrimaryColumn({ length: 255 }) + device_id: string; + + @Column({ length: 10 }) + code: string; + + @Column() + expires_at: Date; +} diff --git a/src/entity/device_whitelist.ts b/src/entity/device_whitelist.ts new file mode 100644 index 0000000..f60582b --- /dev/null +++ b/src/entity/device_whitelist.ts @@ -0,0 +1,14 @@ +import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn } from "typeorm"; + +@Entity('device_whitelist') +export class DeviceWhitelist { + @PrimaryGeneratedColumn() + id: number; + + @Column({ name: 'device_id', unique: true }) + deviceId: string; + + + @CreateDateColumn({ name: 'created_at' }) + createdAt: Date; +} diff --git a/src/middleware/auth.middleware.ts b/src/middleware/auth.middleware.ts index a548b88..12675e0 100644 --- a/src/middleware/auth.middleware.ts +++ b/src/middleware/auth.middleware.ts @@ -8,11 +8,15 @@ import { } from '@midwayjs/core'; import { Context } from '@midwayjs/koa'; import { JwtService } from '@midwayjs/jwt'; // 引入 JwtService 类型 +import { DeviceWhitelistService } from '../service/deviceWhitelist.service'; @Middleware() export class AuthMiddleware implements IMiddleware { @Inject() jwtService: JwtService; // 注入 JwtService 实例 + @Inject() + deviceWhitelistService: DeviceWhitelistService; + // 白名单配置 whiteList = [ '/user/login', @@ -40,6 +44,30 @@ export class AuthMiddleware implements IMiddleware { const [scheme, token] = parts; + if (!/^Bearer$/i.test(scheme)) { + throw new httpError.UnauthorizedError('Invalid Token Scheme'); + } + + try { + // 2. 校验 JWT 并解析 payload + const decoded: any = await this.jwtService.verify(token); + + const deviceId: string = decoded.deviceId; + if (!deviceId) { + throw new httpError.UnauthorizedError('Missing deviceId in token'); + } + + // 3. 校验设备是否在白名单 + const isWhite = await this.deviceWhitelistService.isWhitelisted( + deviceId + ); + if (!isWhite) { + throw new httpError.UnauthorizedError('Device not authorized'); + } + } catch (error) { + throw new httpError.UnauthorizedError('Invalid or expired token'); + } + if (/^Bearer$/i.test(scheme)) { try { //jwt.verify方法验证token是否有效 diff --git a/src/service/authCode.service.ts b/src/service/authCode.service.ts new file mode 100644 index 0000000..012c451 --- /dev/null +++ b/src/service/authCode.service.ts @@ -0,0 +1,46 @@ +import { Provide } from '@midwayjs/core'; +import { InjectEntityModel } from '@midwayjs/typeorm'; +import { Repository } from 'typeorm'; +import { AuthCode } from '../entity/auth_code'; + +@Provide() +export class AuthCodeService { + @InjectEntityModel(AuthCode) + authCodeModel: Repository; + + private generateCodeNumber() { + return Math.floor(100000 + Math.random() * 900000).toString(); + } + + // 生成并保存验证码 + async generateCode(deviceId: string): Promise { + const code = this.generateCodeNumber(); + const expiresAt = new Date(Date.now() + 10 * 60 * 1000); // 10分钟有效期 + + await this.authCodeModel.save({ + device_id: deviceId, + code, + expires_at: expiresAt + }); + + return code; + } + + // 校验验证码 + async verifyCode(deviceId: string, code: string): Promise { + const record = await this.authCodeModel.findOne({ + where: { device_id: deviceId }, + }); + if (!record) return false; + + if (record.expires_at.getTime() < Date.now()) { + await this.authCodeModel.delete({ device_id: deviceId }); + return false; + } + + if (record.code !== code) return false; + + await this.authCodeModel.delete({ device_id: deviceId }); + return true; + } +} diff --git a/src/service/deviceWhitelist.service.ts b/src/service/deviceWhitelist.service.ts new file mode 100644 index 0000000..9ae16da --- /dev/null +++ b/src/service/deviceWhitelist.service.ts @@ -0,0 +1,33 @@ +// src/service/deviceWhitelist.service.ts +import { Provide, Scope, ScopeEnum } from '@midwayjs/core'; +import { InjectEntityModel } from '@midwayjs/typeorm'; +import { Repository } from 'typeorm'; +import { DeviceWhitelist } from '../entity/device_whitelist'; +@Provide() +@Scope(ScopeEnum.Request, { allowDowngrade: true }) +export class DeviceWhitelistService { + @InjectEntityModel(DeviceWhitelist) + whitelistRepo: Repository; + + async isWhitelisted(deviceId: string): Promise { + const found = await this.whitelistRepo.findOne({ where: { deviceId } }); + return !!found; + } + + async addToWhitelist(deviceId: string) { + const exist = await this.whitelistRepo.findOne({ where: { deviceId } }); + if (exist) return exist; + + const record = new DeviceWhitelist(); + record.deviceId = deviceId; + return this.whitelistRepo.save(record); + } + + async removeFromWhitelist(deviceId: string) { + return this.whitelistRepo.delete({ deviceId }); + } + + async getAll() { + return this.whitelistRepo.find(); + } +} diff --git a/src/service/mail.service.ts b/src/service/mail.service.ts new file mode 100644 index 0000000..c734f0b --- /dev/null +++ b/src/service/mail.service.ts @@ -0,0 +1,29 @@ +// src/service/mail.service.ts +import { Provide, Config } from '@midwayjs/core'; +import * as nodemailer from 'nodemailer'; + +@Provide() +export class MailService { + @Config('mailer') + mailConfig; + + private transporter; + + async init() { + if (!this.transporter) { + this.transporter = nodemailer.createTransport(this.mailConfig); + } + } + + async sendMail(to: string, subject: string, text: string) { + await this.init(); + + const info = await this.transporter.sendMail({ + from: `"Canpouches" <${this.mailConfig.auth.user}>`, + to, + subject, + text, + }); + return info; + } +} diff --git a/src/service/user.service.ts b/src/service/user.service.ts index c45eaf8..4943e9a 100644 --- a/src/service/user.service.ts +++ b/src/service/user.service.ts @@ -1,5 +1,5 @@ // src/service/user.service.ts -import { Inject, Provide } from '@midwayjs/core'; +import { Body, httpError, Inject, Provide } from '@midwayjs/core'; import { InjectEntityModel } from '@midwayjs/typeorm'; import { Repository } from 'typeorm'; import * as bcrypt from 'bcryptjs'; @@ -7,6 +7,9 @@ import { JwtService } from '@midwayjs/jwt'; import { User } from '../entity/user.entity'; import { LoginResDTO } from '../dto/user.dto'; import { plainToInstance } from 'class-transformer'; +import { MailService } from './mail.service'; +import { AuthCodeService } from './authCode.service'; +import { DeviceWhitelistService } from './deviceWhitelist.service'; @Provide() export class UserService { @@ -16,17 +19,56 @@ export class UserService { @Inject() jwtService: JwtService; - async login(username: string, password: string): Promise { + @Inject() + mailService: MailService; + + @Inject() + authCodeService: AuthCodeService; + + @Inject() + deviceWhitelistService: DeviceWhitelistService; + + async requestAuthCode(deviceId: string) { + const code = await this.authCodeService.generateCode(deviceId); + + await this.mailService.sendMail( + 'info@canpouches.com', + 'Your Login Authorization Code', + `Your authorization code is: ${code}, valid for 10 minutes.` + ); + } + + async login(@Body() body): Promise { + const { username, password, deviceId, authCode } = body; + const user = await this.userModel.findOne({ where: { username, isActive: true }, }); if (!user || !(await bcrypt.compare(password, user.password))) { throw new Error('用户名或者密码错误'); } + const isWhite = await this.deviceWhitelistService.isWhitelisted(deviceId); + if (!isWhite) { + if (!authCode) { + await this.requestAuthCode(deviceId); + const err = new httpError.BadRequestError('非白名单设备请填写验证码'); + (err as any).code = 10001; // 添加业务错误码 + throw err; + } + + const valid = await this.authCodeService.verifyCode(deviceId, authCode); + if (!valid) { + throw new Error('验证码错误'); + } + + // 校验通过后,将设备加入白名单 + await this.deviceWhitelistService.addToWhitelist(deviceId); + } // 生成 JWT,包含角色和权限信息 const token = await this.jwtService.sign({ id: user.id, + deviceId, username: user.username, });