zksu
/
API
forked from yoone/API
1
0
Fork 0

设备白名单

This commit is contained in:
cll 2025-07-29 17:30:17 +08:00
parent 7d91e14c1a
commit 1c828f49c9
10 changed files with 224 additions and 18 deletions

View File

@ -21,21 +21,11 @@
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"mysql2": "^3.11.5", "mysql2": "^3.11.5",
"nodemailer": "^7.0.5",
"swagger-ui-dist": "^5.18.2", "swagger-ui-dist": "^5.18.2",
"typeorm": "^0.3.20", "typeorm": "^0.3.20",
"xml2js": "^0.6.2" "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": { "engines": {
"node": ">=12.0.0" "node": ">=12.0.0"
}, },

View File

@ -29,6 +29,8 @@ import { Strength } from '../entity/strength.entity';
import { Flavors } from '../entity/flavors.entity'; import { Flavors } from '../entity/flavors.entity';
import { CustomerTag } from '../entity/customer_tag.entity'; import { CustomerTag } from '../entity/customer_tag.entity';
import { Customer } from '../entity/customer.entity'; import { Customer } from '../entity/customer.entity';
import { DeviceWhitelist } from '../entity/device_whitelist';
import { AuthCode } from '../entity/auth_code';
export default { export default {
// use for cookie sign key, should change to your own and keep security // use for cookie sign key, should change to your own and keep security
@ -66,6 +68,8 @@ export default {
TransferItem, TransferItem,
CustomerTag, CustomerTag,
Customer, Customer,
DeviceWhitelist,
AuthCode,
], ],
synchronize: true, synchronize: true,
logging: false, logging: false,
@ -110,4 +114,13 @@ export default {
addSecurityRequirements: true, addSecurityRequirements: true,
}, },
}, },
mailer: {
host: 'smtphz.qiye.163.com',
port: 465,
secure: true,
auth: {
user: 'info@canpouches.com',
pass: 'WWqQ4aZq4Jrm9uwz',
},
}
} as MidwayConfig; } as MidwayConfig;

View File

@ -5,7 +5,6 @@ import { UserService } from '../service/user.service';
import { errorResponse, successResponse } from '../utils/response.util'; import { errorResponse, successResponse } from '../utils/response.util';
import { ApiOkResponse } from '@midwayjs/swagger'; import { ApiOkResponse } from '@midwayjs/swagger';
import { BooleanRes, LoginRes } from '../dto/reponse.dto'; import { BooleanRes, LoginRes } from '../dto/reponse.dto';
import { LoginDTO } from '../dto/user.dto';
import { User } from '../decorator/user.decorator'; import { User } from '../decorator/user.decorator';
@Controller('/user') @Controller('/user')
@ -17,13 +16,12 @@ export class UserController {
type: LoginRes, type: LoginRes,
}) })
@Post('/login') @Post('/login')
async login(@Body() body: LoginDTO) { async login(@Body() body) {
const { username, password } = body;
try { try {
const result = await this.userService.login(username, password); const result = await this.userService.login(body);
return successResponse(result, '登录成功'); return successResponse(result, '登录成功');
} catch (error) { } catch (error) {
return errorResponse(error?.message || '登录失败'); return errorResponse(error?.message || '登录失败', error?.code);
} }
} }

13
src/entity/auth_code.ts Normal file
View File

@ -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;
}

View File

@ -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;
}

View File

@ -8,11 +8,15 @@ import {
} from '@midwayjs/core'; } from '@midwayjs/core';
import { Context } from '@midwayjs/koa'; import { Context } from '@midwayjs/koa';
import { JwtService } from '@midwayjs/jwt'; // 引入 JwtService 类型 import { JwtService } from '@midwayjs/jwt'; // 引入 JwtService 类型
import { DeviceWhitelistService } from '../service/deviceWhitelist.service';
@Middleware() @Middleware()
export class AuthMiddleware implements IMiddleware<Context, NextFunction> { export class AuthMiddleware implements IMiddleware<Context, NextFunction> {
@Inject() @Inject()
jwtService: JwtService; // 注入 JwtService 实例 jwtService: JwtService; // 注入 JwtService 实例
@Inject()
deviceWhitelistService: DeviceWhitelistService;
// 白名单配置 // 白名单配置
whiteList = [ whiteList = [
'/user/login', '/user/login',
@ -40,6 +44,30 @@ export class AuthMiddleware implements IMiddleware<Context, NextFunction> {
const [scheme, token] = parts; 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)) { if (/^Bearer$/i.test(scheme)) {
try { try {
//jwt.verify方法验证token是否有效 //jwt.verify方法验证token是否有效

View File

@ -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<AuthCode>;
private generateCodeNumber() {
return Math.floor(100000 + Math.random() * 900000).toString();
}
// 生成并保存验证码
async generateCode(deviceId: string): Promise<string> {
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<boolean> {
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;
}
}

View File

@ -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<DeviceWhitelist>;
async isWhitelisted(deviceId: string): Promise<boolean> {
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();
}
}

View File

@ -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;
}
}

View File

@ -1,5 +1,5 @@
// src/service/user.service.ts // 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 { InjectEntityModel } from '@midwayjs/typeorm';
import { Repository } from 'typeorm'; import { Repository } from 'typeorm';
import * as bcrypt from 'bcryptjs'; import * as bcrypt from 'bcryptjs';
@ -7,6 +7,9 @@ import { JwtService } from '@midwayjs/jwt';
import { User } from '../entity/user.entity'; import { User } from '../entity/user.entity';
import { LoginResDTO } from '../dto/user.dto'; import { LoginResDTO } from '../dto/user.dto';
import { plainToInstance } from 'class-transformer'; import { plainToInstance } from 'class-transformer';
import { MailService } from './mail.service';
import { AuthCodeService } from './authCode.service';
import { DeviceWhitelistService } from './deviceWhitelist.service';
@Provide() @Provide()
export class UserService { export class UserService {
@ -16,17 +19,56 @@ export class UserService {
@Inject() @Inject()
jwtService: JwtService; jwtService: JwtService;
async login(username: string, password: string): Promise<LoginResDTO> { @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<LoginResDTO> {
const { username, password, deviceId, authCode } = body;
const user = await this.userModel.findOne({ const user = await this.userModel.findOne({
where: { username, isActive: true }, where: { username, isActive: true },
}); });
if (!user || !(await bcrypt.compare(password, user.password))) { if (!user || !(await bcrypt.compare(password, user.password))) {
throw new Error('用户名或者密码错误'); 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包含角色和权限信息 // 生成 JWT包含角色和权限信息
const token = await this.jwtService.sign({ const token = await this.jwtService.sign({
id: user.id, id: user.id,
deviceId,
username: user.username, username: user.username,
}); });