forked from yoone/API
设备白名单
This commit is contained in:
parent
7d91e14c1a
commit
1c828f49c9
12
package.json
12
package.json
|
|
@ -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"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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';
|
} 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是否有效
|
||||||
|
|
|
||||||
|
|
@ -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
|
// 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,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue