feat(安全): 增强JWT安全配置并移除重复路由
添加JWT安全配置到.env.example并实现严格的环境检查 移除devices.js中重复的批量操作路由
This commit is contained in:
@@ -35,3 +35,25 @@ MYSQL_PORT=3306 # MySQL端口号
|
|||||||
MYSQL_USERNAME=root # MySQL用户名
|
MYSQL_USERNAME=root # MySQL用户名
|
||||||
MYSQL_PASSWORD= # MySQL密码(为空则无密码)
|
MYSQL_PASSWORD= # MySQL密码(为空则无密码)
|
||||||
MYSQL_DATABASE=idc_management # MySQL数据库名
|
MYSQL_DATABASE=idc_management # MySQL数据库名
|
||||||
|
|
||||||
|
# ==============================================
|
||||||
|
# 安全配置(必填)
|
||||||
|
# ==============================================
|
||||||
|
|
||||||
|
# JWT 密钥 - 用于签名和验证用户身份令牌
|
||||||
|
# ⚠️ 安全警告:
|
||||||
|
# - 生产环境必须修改默认值,使用强随机密钥
|
||||||
|
# - 至少 32 位字符,推荐 64 位
|
||||||
|
# - 定期更换(建议每3-6个月)
|
||||||
|
# - 不要提交到代码仓库,仅保存在服务器环境变量
|
||||||
|
#
|
||||||
|
# 生成强密钥命令:
|
||||||
|
# PowerShell: -join ((48..57) + (65..90) + (97..122) | Get-Random -Count 64 | ForEach-Object { [char]$_ })
|
||||||
|
# Linux/Mac: openssl rand -base64 64
|
||||||
|
# Node.js: require('crypto').randomBytes(64).toString('hex')
|
||||||
|
#
|
||||||
|
JWT_SECRET=your-strong-secret-key-minimum-32-characters-change-in-production
|
||||||
|
|
||||||
|
# Token 过期时间(格式:数字+单位,如 24h, 2h, 30m)
|
||||||
|
# 建议:开发环境 24h,生产环境 2h 或更短
|
||||||
|
TOKEN_EXPIRY=24h
|
||||||
@@ -1,7 +1,41 @@
|
|||||||
const jwt = require('jsonwebtoken');
|
const jwt = require('jsonwebtoken');
|
||||||
|
const crypto = require('crypto');
|
||||||
const User = require('../models/User');
|
const User = require('../models/User');
|
||||||
|
|
||||||
const JWT_SECRET = process.env.JWT_SECRET || 'idc-management-secret-key-2024';
|
/**
|
||||||
|
* 获取 JWT Secret
|
||||||
|
* - 生产环境:强制从环境变量读取,未设置则抛出错误
|
||||||
|
* - 开发环境:未设置时自动生成临时密钥(重启后失效)
|
||||||
|
*/
|
||||||
|
function getJwtSecret() {
|
||||||
|
const envSecret = process.env.JWT_SECRET;
|
||||||
|
|
||||||
|
// 生产环境强制校验
|
||||||
|
if (process.env.NODE_ENV === 'production') {
|
||||||
|
if (!envSecret) {
|
||||||
|
throw new Error(
|
||||||
|
'[致命错误] 生产环境未设置 JWT_SECRET 环境变量!\n' +
|
||||||
|
'请在服务器环境变量中设置强密钥(至少32位随机字符)。\n' +
|
||||||
|
'生成命令(PowerShell):-join ((48..57) + (65..90) + (97..122) | Get-Random -Count 64 | ForEach-Object { [char]$_ })'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (envSecret.length < 32) {
|
||||||
|
throw new Error('[致命错误] 生产环境 JWT_SECRET 长度必须至少32位!当前长度:' + envSecret.length);
|
||||||
|
}
|
||||||
|
return envSecret;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开发环境:使用环境变量或临时密钥
|
||||||
|
if (!envSecret) {
|
||||||
|
const tempSecret = crypto.randomBytes(32).toString('hex');
|
||||||
|
console.warn('⚠️ [开发模式] JWT_SECRET 未设置,使用临时密钥(重启后所有 Token 失效)');
|
||||||
|
return tempSecret;
|
||||||
|
}
|
||||||
|
|
||||||
|
return envSecret;
|
||||||
|
}
|
||||||
|
|
||||||
|
const JWT_SECRET = getJwtSecret();
|
||||||
const TOKEN_EXPIRY = process.env.TOKEN_EXPIRY || '24h';
|
const TOKEN_EXPIRY = process.env.TOKEN_EXPIRY || '24h';
|
||||||
|
|
||||||
const getBrowserInfo = (userAgent) => {
|
const getBrowserInfo = (userAgent) => {
|
||||||
|
|||||||
@@ -989,27 +989,6 @@ router.put('/:deviceId', validateBody(updateDeviceSchema), async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 批量下线设备
|
|
||||||
router.put('/batch-offline', async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { deviceIds } = req.body;
|
|
||||||
|
|
||||||
if (!deviceIds || !Array.isArray(deviceIds)) {
|
|
||||||
return res.status(400).json({ error: '无效的设备ID列表' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新设备状态为离线
|
|
||||||
const updated = await Device.update(
|
|
||||||
{ status: 'offline' },
|
|
||||||
{ where: { deviceId: deviceIds } }
|
|
||||||
);
|
|
||||||
|
|
||||||
res.json({ message: `成功下线 ${updated[0]} 个设备` });
|
|
||||||
} catch (error) {
|
|
||||||
res.status(500).json({ error: error.message });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 批量删除设备
|
// 批量删除设备
|
||||||
router.delete('/batch-delete', validateBody(batchDeviceIdsSchema), async (req, res) => {
|
router.delete('/batch-delete', validateBody(batchDeviceIdsSchema), async (req, res) => {
|
||||||
const t = await sequelize.transaction();
|
const t = await sequelize.transaction();
|
||||||
@@ -1247,179 +1226,6 @@ router.delete('/:deviceId', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 批量上线设备
|
|
||||||
router.put('/batch-online', async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { deviceIds } = req.body;
|
|
||||||
|
|
||||||
if (!deviceIds || !Array.isArray(deviceIds) || deviceIds.length === 0) {
|
|
||||||
return res.status(400).json({ error: '请提供有效的设备ID列表' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新设备状态为运行中
|
|
||||||
const [affectedCount] = await Device.update(
|
|
||||||
{ status: 'running' },
|
|
||||||
{ where: { deviceId: { [Op.in]: deviceIds } } }
|
|
||||||
);
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
message: `批量上线成功,已更新 ${affectedCount} 个设备`,
|
|
||||||
affectedCount
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
res.status(500).json({ error: error.message });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 批量下线设备
|
|
||||||
router.put('/batch-offline', async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { deviceIds } = req.body;
|
|
||||||
|
|
||||||
if (!deviceIds || !Array.isArray(deviceIds) || deviceIds.length === 0) {
|
|
||||||
return res.status(400).json({ error: '请提供有效的设备ID列表' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新设备状态为离线
|
|
||||||
const [affectedCount] = await Device.update(
|
|
||||||
{ status: 'offline' },
|
|
||||||
{ where: { deviceId: { [Op.in]: deviceIds } } }
|
|
||||||
);
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
message: `批量下线成功,已更新 ${affectedCount} 个设备`,
|
|
||||||
affectedCount
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
res.status(500).json({ error: error.message });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 批量变更设备状态
|
|
||||||
router.put('/batch-status', async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { deviceIds, status } = req.body;
|
|
||||||
|
|
||||||
if (!deviceIds || !Array.isArray(deviceIds) || deviceIds.length === 0) {
|
|
||||||
return res.status(400).json({ error: '请提供有效的设备ID列表' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查数据库中是否存在这些设备
|
|
||||||
const existingDevices = await Device.findAll({
|
|
||||||
where: { deviceId: { [Op.in]: deviceIds } },
|
|
||||||
attributes: ['deviceId']
|
|
||||||
});
|
|
||||||
|
|
||||||
// 检查是否有不存在的设备
|
|
||||||
const existingIds = existingDevices.map(d => d.deviceId);
|
|
||||||
const missingIds = deviceIds.filter(id => !existingIds.includes(id));
|
|
||||||
|
|
||||||
if (missingIds.length > 0) {
|
|
||||||
return res.status(404).json({ error: `设备不存在: ${missingIds.join(', ')}` });
|
|
||||||
}
|
|
||||||
|
|
||||||
const validStatus = ['running', 'maintenance', 'offline', 'fault'];
|
|
||||||
if (!validStatus.includes(status)) {
|
|
||||||
return res.status(400).json({
|
|
||||||
error: `状态值无效,有效值为:${validStatus.join('、')}`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 状态映射
|
|
||||||
const statusText = {
|
|
||||||
running: '运行中',
|
|
||||||
maintenance: '维护中',
|
|
||||||
offline: '离线',
|
|
||||||
fault: '故障'
|
|
||||||
};
|
|
||||||
|
|
||||||
// 更新设备状态
|
|
||||||
const [affectedCount] = await Device.update(
|
|
||||||
{ status },
|
|
||||||
{ where: { deviceId: { [Op.in]: deviceIds } } }
|
|
||||||
);
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
message: `批量状态变更成功,已将 ${affectedCount} 个设备状态变更为"${statusText[status]}"`,
|
|
||||||
affectedCount,
|
|
||||||
newStatus: status
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
res.status(500).json({ error: error.message });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 批量移动设备
|
|
||||||
router.put('/batch-move', async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { deviceIds, targetRackId, startPosition } = req.body;
|
|
||||||
|
|
||||||
if (!deviceIds || !Array.isArray(deviceIds) || deviceIds.length === 0) {
|
|
||||||
return res.status(400).json({ error: '请提供有效的设备ID列表' });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!targetRackId) {
|
|
||||||
return res.status(400).json({ error: '请提供目标机柜ID' });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (startPosition === undefined || startPosition === null) {
|
|
||||||
return res.status(400).json({ error: '请提供起始U位' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证目标机柜是否存在
|
|
||||||
const targetRack = await Rack.findByPk(targetRackId);
|
|
||||||
if (!targetRack) {
|
|
||||||
return res.status(404).json({ error: '目标机柜不存在' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取要移动的设备
|
|
||||||
const devices = await Device.findAll({
|
|
||||||
where: { deviceId: { [Op.in]: deviceIds } }
|
|
||||||
});
|
|
||||||
|
|
||||||
if (devices.length === 0) {
|
|
||||||
return res.status(404).json({ error: '未找到指定的设备' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const movedDevices = [];
|
|
||||||
let currentPosition = startPosition;
|
|
||||||
|
|
||||||
for (const device of devices) {
|
|
||||||
const oldRackId = device.rackId;
|
|
||||||
const oldPosition = device.position;
|
|
||||||
const deviceHeight = device.height || 1;
|
|
||||||
|
|
||||||
const [updated] = await Device.update(
|
|
||||||
{
|
|
||||||
rackId: targetRackId,
|
|
||||||
position: currentPosition
|
|
||||||
},
|
|
||||||
{ where: { deviceId: device.deviceId } }
|
|
||||||
);
|
|
||||||
|
|
||||||
if (updated) {
|
|
||||||
movedDevices.push({
|
|
||||||
deviceId: device.deviceId,
|
|
||||||
name: device.name,
|
|
||||||
oldRackId,
|
|
||||||
oldPosition,
|
|
||||||
newRackId: targetRackId,
|
|
||||||
newPosition: currentPosition
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
currentPosition += deviceHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
message: `批量移动成功,已将 ${movedDevices.length} 个设备移动到机柜 ${targetRackId}`,
|
|
||||||
movedDevices
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
res.status(500).json({ error: error.message });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 增强导出设备数据(支持自定义字段)
|
// 增强导出设备数据(支持自定义字段)
|
||||||
router.get('/enhanced-export', async (req, res) => {
|
router.get('/enhanced-export', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user