设备白名单
This commit is contained in:
parent
7d91e14c1a
commit
1c828f49c9
12
package.json
12
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"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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<Context, NextFunction> {
|
||||
@Inject()
|
||||
jwtService: JwtService; // 注入 JwtService 实例
|
||||
@Inject()
|
||||
deviceWhitelistService: DeviceWhitelistService;
|
||||
|
||||
// 白名单配置
|
||||
whiteList = [
|
||||
'/user/login',
|
||||
|
|
@ -40,6 +44,30 @@ export class AuthMiddleware implements IMiddleware<Context, NextFunction> {
|
|||
|
||||
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是否有效
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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<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({
|
||||
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,
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue