2062 lines
57 KiB
Markdown
2062 lines
57 KiB
Markdown
|
|
# 安全文档
|
|||
|
|
|
|||
|
|
## 版本历史
|
|||
|
|
| 版本 | 日期 | 作者 | 变更说明 |
|
|||
|
|
|------|------|------|----------|
|
|||
|
|
| 1.0 | 2024-01-20 | 安全团队 | 初始版本 |
|
|||
|
|
|
|||
|
|
## 1. 安全概述
|
|||
|
|
|
|||
|
|
### 1.1 安全目标
|
|||
|
|
确保畜牧养殖管理平台的数据安全、系统安全和业务安全,保护用户隐私和企业资产。
|
|||
|
|
|
|||
|
|
### 1.2 安全原则
|
|||
|
|
- **最小权限原则**:用户和系统只获得完成任务所需的最小权限
|
|||
|
|
- **深度防御**:多层安全防护,避免单点故障
|
|||
|
|
- **零信任架构**:不信任任何用户或设备,持续验证
|
|||
|
|
- **数据保护**:全生命周期数据保护
|
|||
|
|
- **持续监控**:实时安全监控和威胁检测
|
|||
|
|
|
|||
|
|
### 1.3 安全合规
|
|||
|
|
- **等保2.0**:符合网络安全等级保护2.0要求
|
|||
|
|
- **GDPR**:遵循欧盟通用数据保护条例
|
|||
|
|
- **个人信息保护法**:符合中国个人信息保护法要求
|
|||
|
|
- **行业标准**:遵循畜牧业相关安全标准
|
|||
|
|
|
|||
|
|
## 2. 安全架构
|
|||
|
|
|
|||
|
|
### 2.1 安全架构图
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
graph TB
|
|||
|
|
subgraph "边界安全"
|
|||
|
|
WAF[Web应用防火墙]
|
|||
|
|
FW[网络防火墙]
|
|||
|
|
IPS[入侵防护系统]
|
|||
|
|
DDoS[DDoS防护]
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
subgraph "应用安全"
|
|||
|
|
AUTH[身份认证]
|
|||
|
|
AUTHZ[权限控制]
|
|||
|
|
ENCRYPT[数据加密]
|
|||
|
|
AUDIT[审计日志]
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
subgraph "数据安全"
|
|||
|
|
BACKUP[数据备份]
|
|||
|
|
MASK[数据脱敏]
|
|||
|
|
DLP[数据防泄漏]
|
|||
|
|
CRYPTO[加密存储]
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
subgraph "基础设施安全"
|
|||
|
|
HOST[主机安全]
|
|||
|
|
CONTAINER[容器安全]
|
|||
|
|
NETWORK[网络安全]
|
|||
|
|
MONITOR[安全监控]
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
Internet --> WAF
|
|||
|
|
WAF --> FW
|
|||
|
|
FW --> IPS
|
|||
|
|
IPS --> AUTH
|
|||
|
|
AUTH --> AUTHZ
|
|||
|
|
AUTHZ --> ENCRYPT
|
|||
|
|
ENCRYPT --> AUDIT
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 2.2 安全域划分
|
|||
|
|
|
|||
|
|
| 安全域 | 描述 | 安全等级 | 访问控制 |
|
|||
|
|
|--------|------|----------|----------|
|
|||
|
|
| 互联网区 | 面向公网的服务 | 高 | WAF + 防火墙 |
|
|||
|
|
| DMZ区 | 前端应用和负载均衡 | 高 | 严格访问控制 |
|
|||
|
|
| 应用区 | 后端API服务 | 中 | 内网访问 |
|
|||
|
|
| 数据区 | 数据库和存储 | 极高 | 最小权限访问 |
|
|||
|
|
| 管理区 | 运维和管理系统 | 极高 | VPN + 双因子认证 |
|
|||
|
|
|
|||
|
|
## 3. 身份认证与授权
|
|||
|
|
|
|||
|
|
### 3.1 用户身份认证
|
|||
|
|
|
|||
|
|
#### 3.1.1 多因子认证(MFA)
|
|||
|
|
|
|||
|
|
```javascript
|
|||
|
|
// 双因子认证实现
|
|||
|
|
class TwoFactorAuth {
|
|||
|
|
constructor() {
|
|||
|
|
this.totpWindow = 30; // 30秒时间窗口
|
|||
|
|
this.backupCodes = [];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 生成TOTP密钥
|
|||
|
|
generateSecret(userId) {
|
|||
|
|
const secret = speakeasy.generateSecret({
|
|||
|
|
name: `畜牧管理平台 (${userId})`,
|
|||
|
|
issuer: '畜牧管理平台'
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
secret: secret.base32,
|
|||
|
|
qrCode: qrcode.toDataURL(secret.otpauth_url)
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 验证TOTP令牌
|
|||
|
|
verifyToken(secret, token) {
|
|||
|
|
return speakeasy.totp.verify({
|
|||
|
|
secret: secret,
|
|||
|
|
encoding: 'base32',
|
|||
|
|
token: token,
|
|||
|
|
window: this.totpWindow
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 生成备用码
|
|||
|
|
generateBackupCodes() {
|
|||
|
|
const codes = [];
|
|||
|
|
for (let i = 0; i < 10; i++) {
|
|||
|
|
codes.push(crypto.randomBytes(4).toString('hex').toUpperCase());
|
|||
|
|
}
|
|||
|
|
return codes;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 验证备用码
|
|||
|
|
verifyBackupCode(userId, code) {
|
|||
|
|
// 从数据库获取用户备用码
|
|||
|
|
const userBackupCodes = this.getUserBackupCodes(userId);
|
|||
|
|
const index = userBackupCodes.indexOf(code);
|
|||
|
|
|
|||
|
|
if (index !== -1) {
|
|||
|
|
// 使用后删除备用码
|
|||
|
|
userBackupCodes.splice(index, 1);
|
|||
|
|
this.updateUserBackupCodes(userId, userBackupCodes);
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 3.1.2 JWT令牌安全
|
|||
|
|
|
|||
|
|
```javascript
|
|||
|
|
// JWT安全配置
|
|||
|
|
const jwtConfig = {
|
|||
|
|
// 使用RS256算法
|
|||
|
|
algorithm: 'RS256',
|
|||
|
|
// 短期访问令牌
|
|||
|
|
accessTokenExpiry: '15m',
|
|||
|
|
// 长期刷新令牌
|
|||
|
|
refreshTokenExpiry: '7d',
|
|||
|
|
// 令牌发行者
|
|||
|
|
issuer: 'xlxumu-platform',
|
|||
|
|
// 令牌受众
|
|||
|
|
audience: 'xlxumu-users'
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
class JWTManager {
|
|||
|
|
constructor() {
|
|||
|
|
this.privateKey = fs.readFileSync('private.pem');
|
|||
|
|
this.publicKey = fs.readFileSync('public.pem');
|
|||
|
|
this.blacklist = new Set(); // 令牌黑名单
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 生成访问令牌
|
|||
|
|
generateAccessToken(payload) {
|
|||
|
|
return jwt.sign(payload, this.privateKey, {
|
|||
|
|
algorithm: jwtConfig.algorithm,
|
|||
|
|
expiresIn: jwtConfig.accessTokenExpiry,
|
|||
|
|
issuer: jwtConfig.issuer,
|
|||
|
|
audience: jwtConfig.audience,
|
|||
|
|
jwtid: uuidv4() // 唯一标识符
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 生成刷新令牌
|
|||
|
|
generateRefreshToken(userId) {
|
|||
|
|
const payload = {
|
|||
|
|
userId: userId,
|
|||
|
|
type: 'refresh',
|
|||
|
|
tokenFamily: uuidv4() // 令牌族标识
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
return jwt.sign(payload, this.privateKey, {
|
|||
|
|
algorithm: jwtConfig.algorithm,
|
|||
|
|
expiresIn: jwtConfig.refreshTokenExpiry,
|
|||
|
|
issuer: jwtConfig.issuer,
|
|||
|
|
audience: jwtConfig.audience,
|
|||
|
|
jwtid: uuidv4()
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 验证令牌
|
|||
|
|
verifyToken(token) {
|
|||
|
|
try {
|
|||
|
|
// 检查黑名单
|
|||
|
|
if (this.blacklist.has(token)) {
|
|||
|
|
throw new Error('Token is blacklisted');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const decoded = jwt.verify(token, this.publicKey, {
|
|||
|
|
algorithms: [jwtConfig.algorithm],
|
|||
|
|
issuer: jwtConfig.issuer,
|
|||
|
|
audience: jwtConfig.audience
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
return decoded;
|
|||
|
|
} catch (error) {
|
|||
|
|
throw new Error('Invalid token');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 撤销令牌
|
|||
|
|
revokeToken(token) {
|
|||
|
|
this.blacklist.add(token);
|
|||
|
|
// 可以设置定时清理过期的黑名单令牌
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3.2 权限控制系统
|
|||
|
|
|
|||
|
|
#### 3.2.1 RBAC权限模型
|
|||
|
|
|
|||
|
|
```javascript
|
|||
|
|
// 基于角色的访问控制
|
|||
|
|
class RBACManager {
|
|||
|
|
constructor() {
|
|||
|
|
this.roles = new Map();
|
|||
|
|
this.permissions = new Map();
|
|||
|
|
this.userRoles = new Map();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 定义权限
|
|||
|
|
definePermission(name, resource, action) {
|
|||
|
|
this.permissions.set(name, {
|
|||
|
|
resource: resource,
|
|||
|
|
action: action,
|
|||
|
|
description: `${action} on ${resource}`
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 定义角色
|
|||
|
|
defineRole(name, permissions) {
|
|||
|
|
this.roles.set(name, {
|
|||
|
|
name: name,
|
|||
|
|
permissions: permissions,
|
|||
|
|
inherits: []
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 角色继承
|
|||
|
|
addRoleInheritance(childRole, parentRole) {
|
|||
|
|
if (this.roles.has(childRole) && this.roles.has(parentRole)) {
|
|||
|
|
this.roles.get(childRole).inherits.push(parentRole);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 分配角色给用户
|
|||
|
|
assignRole(userId, roleName) {
|
|||
|
|
if (!this.userRoles.has(userId)) {
|
|||
|
|
this.userRoles.set(userId, []);
|
|||
|
|
}
|
|||
|
|
this.userRoles.get(userId).push(roleName);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查用户权限
|
|||
|
|
hasPermission(userId, permission) {
|
|||
|
|
const userRoles = this.userRoles.get(userId) || [];
|
|||
|
|
|
|||
|
|
for (const roleName of userRoles) {
|
|||
|
|
if (this.roleHasPermission(roleName, permission)) {
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查角色权限
|
|||
|
|
roleHasPermission(roleName, permission) {
|
|||
|
|
const role = this.roles.get(roleName);
|
|||
|
|
if (!role) return false;
|
|||
|
|
|
|||
|
|
// 检查直接权限
|
|||
|
|
if (role.permissions.includes(permission)) {
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查继承权限
|
|||
|
|
for (const parentRole of role.inherits) {
|
|||
|
|
if (this.roleHasPermission(parentRole, permission)) {
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 权限中间件
|
|||
|
|
const permissionMiddleware = (permission) => {
|
|||
|
|
return async (req, res, next) => {
|
|||
|
|
try {
|
|||
|
|
const token = req.headers.authorization?.replace('Bearer ', '');
|
|||
|
|
const decoded = jwtManager.verifyToken(token);
|
|||
|
|
|
|||
|
|
if (!rbacManager.hasPermission(decoded.userId, permission)) {
|
|||
|
|
return res.status(403).json({
|
|||
|
|
error: 'Insufficient permissions',
|
|||
|
|
required: permission
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
req.user = decoded;
|
|||
|
|
next();
|
|||
|
|
} catch (error) {
|
|||
|
|
res.status(401).json({ error: 'Unauthorized' });
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
};
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 3.2.2 权限配置
|
|||
|
|
|
|||
|
|
```yaml
|
|||
|
|
# 权限配置文件
|
|||
|
|
permissions:
|
|||
|
|
# 用户管理权限
|
|||
|
|
user.create: "创建用户"
|
|||
|
|
user.read: "查看用户"
|
|||
|
|
user.update: "更新用户"
|
|||
|
|
user.delete: "删除用户"
|
|||
|
|
|
|||
|
|
# 农场管理权限
|
|||
|
|
farm.create: "创建农场"
|
|||
|
|
farm.read: "查看农场"
|
|||
|
|
farm.update: "更新农场"
|
|||
|
|
farm.delete: "删除农场"
|
|||
|
|
|
|||
|
|
# 动物管理权限
|
|||
|
|
animal.create: "添加动物"
|
|||
|
|
animal.read: "查看动物"
|
|||
|
|
animal.update: "更新动物信息"
|
|||
|
|
animal.delete: "删除动物"
|
|||
|
|
|
|||
|
|
# 系统管理权限
|
|||
|
|
system.config: "系统配置"
|
|||
|
|
system.monitor: "系统监控"
|
|||
|
|
system.backup: "数据备份"
|
|||
|
|
|
|||
|
|
roles:
|
|||
|
|
# 超级管理员
|
|||
|
|
super_admin:
|
|||
|
|
permissions:
|
|||
|
|
- "*" # 所有权限
|
|||
|
|
|
|||
|
|
# 系统管理员
|
|||
|
|
admin:
|
|||
|
|
permissions:
|
|||
|
|
- "user.*"
|
|||
|
|
- "farm.*"
|
|||
|
|
- "animal.*"
|
|||
|
|
- "system.config"
|
|||
|
|
- "system.monitor"
|
|||
|
|
|
|||
|
|
# 农场主
|
|||
|
|
farm_owner:
|
|||
|
|
permissions:
|
|||
|
|
- "farm.read"
|
|||
|
|
- "farm.update"
|
|||
|
|
- "animal.*"
|
|||
|
|
|
|||
|
|
# 普通用户
|
|||
|
|
user:
|
|||
|
|
permissions:
|
|||
|
|
- "farm.read"
|
|||
|
|
- "animal.read"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 4. 数据安全
|
|||
|
|
|
|||
|
|
### 4.1 数据加密
|
|||
|
|
|
|||
|
|
#### 4.1.1 传输加密
|
|||
|
|
|
|||
|
|
```nginx
|
|||
|
|
# Nginx SSL配置
|
|||
|
|
server {
|
|||
|
|
listen 443 ssl http2;
|
|||
|
|
server_name www.xlxumu.com;
|
|||
|
|
|
|||
|
|
# SSL证书配置
|
|||
|
|
ssl_certificate /etc/letsencrypt/live/www.xlxumu.com/fullchain.pem;
|
|||
|
|
ssl_certificate_key /etc/letsencrypt/live/www.xlxumu.com/privkey.pem;
|
|||
|
|
|
|||
|
|
# SSL安全配置
|
|||
|
|
ssl_protocols TLSv1.2 TLSv1.3;
|
|||
|
|
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384;
|
|||
|
|
ssl_prefer_server_ciphers off;
|
|||
|
|
ssl_session_cache shared:SSL:10m;
|
|||
|
|
ssl_session_timeout 10m;
|
|||
|
|
|
|||
|
|
# HSTS配置
|
|||
|
|
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
|
|||
|
|
|
|||
|
|
# 其他安全头
|
|||
|
|
add_header X-Frame-Options DENY always;
|
|||
|
|
add_header X-Content-Type-Options nosniff always;
|
|||
|
|
add_header X-XSS-Protection "1; mode=block" always;
|
|||
|
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
|||
|
|
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; frame-ancestors 'none';" always;
|
|||
|
|
|
|||
|
|
location / {
|
|||
|
|
proxy_pass http://backend;
|
|||
|
|
proxy_set_header Host $host;
|
|||
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|||
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|||
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# 强制HTTPS重定向
|
|||
|
|
server {
|
|||
|
|
listen 80;
|
|||
|
|
server_name www.xlxumu.com;
|
|||
|
|
return 301 https://$server_name$request_uri;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 4.1.2 存储加密
|
|||
|
|
|
|||
|
|
```javascript
|
|||
|
|
// 数据库字段加密
|
|||
|
|
const crypto = require('crypto');
|
|||
|
|
|
|||
|
|
class FieldEncryption {
|
|||
|
|
constructor() {
|
|||
|
|
this.algorithm = 'aes-256-gcm';
|
|||
|
|
this.keyLength = 32;
|
|||
|
|
this.ivLength = 16;
|
|||
|
|
this.tagLength = 16;
|
|||
|
|
this.masterKey = process.env.ENCRYPTION_MASTER_KEY;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 生成数据加密密钥
|
|||
|
|
generateDataKey() {
|
|||
|
|
return crypto.randomBytes(this.keyLength);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 加密数据
|
|||
|
|
encrypt(plaintext, dataKey) {
|
|||
|
|
const iv = crypto.randomBytes(this.ivLength);
|
|||
|
|
const cipher = crypto.createCipher(this.algorithm, dataKey, iv);
|
|||
|
|
|
|||
|
|
let encrypted = cipher.update(plaintext, 'utf8', 'hex');
|
|||
|
|
encrypted += cipher.final('hex');
|
|||
|
|
|
|||
|
|
const tag = cipher.getAuthTag();
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
encrypted: encrypted,
|
|||
|
|
iv: iv.toString('hex'),
|
|||
|
|
tag: tag.toString('hex')
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 解密数据
|
|||
|
|
decrypt(encryptedData, dataKey) {
|
|||
|
|
const decipher = crypto.createDecipher(
|
|||
|
|
this.algorithm,
|
|||
|
|
dataKey,
|
|||
|
|
Buffer.from(encryptedData.iv, 'hex')
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
decipher.setAuthTag(Buffer.from(encryptedData.tag, 'hex'));
|
|||
|
|
|
|||
|
|
let decrypted = decipher.update(encryptedData.encrypted, 'hex', 'utf8');
|
|||
|
|
decrypted += decipher.final('utf8');
|
|||
|
|
|
|||
|
|
return decrypted;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 加密敏感字段
|
|||
|
|
encryptSensitiveFields(data, fields) {
|
|||
|
|
const dataKey = this.generateDataKey();
|
|||
|
|
const encryptedData = { ...data };
|
|||
|
|
|
|||
|
|
fields.forEach(field => {
|
|||
|
|
if (data[field]) {
|
|||
|
|
encryptedData[field] = this.encrypt(data[field], dataKey);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 加密数据密钥
|
|||
|
|
encryptedData._dataKey = this.encryptDataKey(dataKey);
|
|||
|
|
|
|||
|
|
return encryptedData;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 解密敏感字段
|
|||
|
|
decryptSensitiveFields(encryptedData, fields) {
|
|||
|
|
const dataKey = this.decryptDataKey(encryptedData._dataKey);
|
|||
|
|
const data = { ...encryptedData };
|
|||
|
|
|
|||
|
|
fields.forEach(field => {
|
|||
|
|
if (encryptedData[field]) {
|
|||
|
|
data[field] = this.decrypt(encryptedData[field], dataKey);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
delete data._dataKey;
|
|||
|
|
return data;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 使用主密钥加密数据密钥
|
|||
|
|
encryptDataKey(dataKey) {
|
|||
|
|
const iv = crypto.randomBytes(this.ivLength);
|
|||
|
|
const cipher = crypto.createCipher(this.algorithm, this.masterKey, iv);
|
|||
|
|
|
|||
|
|
let encrypted = cipher.update(dataKey, null, 'hex');
|
|||
|
|
encrypted += cipher.final('hex');
|
|||
|
|
|
|||
|
|
const tag = cipher.getAuthTag();
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
encrypted: encrypted,
|
|||
|
|
iv: iv.toString('hex'),
|
|||
|
|
tag: tag.toString('hex')
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 使用主密钥解密数据密钥
|
|||
|
|
decryptDataKey(encryptedDataKey) {
|
|||
|
|
const decipher = crypto.createDecipher(
|
|||
|
|
this.algorithm,
|
|||
|
|
this.masterKey,
|
|||
|
|
Buffer.from(encryptedDataKey.iv, 'hex')
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
decipher.setAuthTag(Buffer.from(encryptedDataKey.tag, 'hex'));
|
|||
|
|
|
|||
|
|
let decrypted = decipher.update(encryptedDataKey.encrypted, 'hex');
|
|||
|
|
decrypted += decipher.final();
|
|||
|
|
|
|||
|
|
return decrypted;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 4.2 数据脱敏
|
|||
|
|
|
|||
|
|
```javascript
|
|||
|
|
// 数据脱敏工具
|
|||
|
|
class DataMasking {
|
|||
|
|
// 手机号脱敏
|
|||
|
|
maskPhone(phone) {
|
|||
|
|
if (!phone || phone.length < 11) return phone;
|
|||
|
|
return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 身份证号脱敏
|
|||
|
|
maskIdCard(idCard) {
|
|||
|
|
if (!idCard || idCard.length < 15) return idCard;
|
|||
|
|
return idCard.replace(/(\d{6})\d{8}(\d{4})/, '$1********$2');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 邮箱脱敏
|
|||
|
|
maskEmail(email) {
|
|||
|
|
if (!email || !email.includes('@')) return email;
|
|||
|
|
const [username, domain] = email.split('@');
|
|||
|
|
const maskedUsername = username.length > 2
|
|||
|
|
? username.substring(0, 2) + '*'.repeat(username.length - 2)
|
|||
|
|
: username;
|
|||
|
|
return `${maskedUsername}@${domain}`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 银行卡号脱敏
|
|||
|
|
maskBankCard(cardNumber) {
|
|||
|
|
if (!cardNumber || cardNumber.length < 16) return cardNumber;
|
|||
|
|
return cardNumber.replace(/(\d{4})\d{8}(\d{4})/, '$1********$2');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 姓名脱敏
|
|||
|
|
maskName(name) {
|
|||
|
|
if (!name || name.length < 2) return name;
|
|||
|
|
return name.charAt(0) + '*'.repeat(name.length - 1);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 地址脱敏
|
|||
|
|
maskAddress(address) {
|
|||
|
|
if (!address || address.length < 10) return address;
|
|||
|
|
return address.substring(0, 6) + '*'.repeat(address.length - 6);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 批量脱敏
|
|||
|
|
maskObject(obj, maskRules) {
|
|||
|
|
const masked = { ...obj };
|
|||
|
|
|
|||
|
|
Object.keys(maskRules).forEach(field => {
|
|||
|
|
if (masked[field]) {
|
|||
|
|
const maskType = maskRules[field];
|
|||
|
|
switch (maskType) {
|
|||
|
|
case 'phone':
|
|||
|
|
masked[field] = this.maskPhone(masked[field]);
|
|||
|
|
break;
|
|||
|
|
case 'email':
|
|||
|
|
masked[field] = this.maskEmail(masked[field]);
|
|||
|
|
break;
|
|||
|
|
case 'idCard':
|
|||
|
|
masked[field] = this.maskIdCard(masked[field]);
|
|||
|
|
break;
|
|||
|
|
case 'bankCard':
|
|||
|
|
masked[field] = this.maskBankCard(masked[field]);
|
|||
|
|
break;
|
|||
|
|
case 'name':
|
|||
|
|
masked[field] = this.maskName(masked[field]);
|
|||
|
|
break;
|
|||
|
|
case 'address':
|
|||
|
|
masked[field] = this.maskAddress(masked[field]);
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
return masked;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 使用示例
|
|||
|
|
const dataMasking = new DataMasking();
|
|||
|
|
|
|||
|
|
// API响应数据脱敏中间件
|
|||
|
|
const maskingMiddleware = (maskRules) => {
|
|||
|
|
return (req, res, next) => {
|
|||
|
|
const originalSend = res.send;
|
|||
|
|
|
|||
|
|
res.send = function(data) {
|
|||
|
|
if (typeof data === 'object' && data !== null) {
|
|||
|
|
if (Array.isArray(data)) {
|
|||
|
|
data = data.map(item => dataMasking.maskObject(item, maskRules));
|
|||
|
|
} else {
|
|||
|
|
data = dataMasking.maskObject(data, maskRules);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
originalSend.call(this, data);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
next();
|
|||
|
|
};
|
|||
|
|
};
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 4.3 数据备份与恢复
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
#!/bin/bash
|
|||
|
|
# secure-backup.sh - 安全备份脚本
|
|||
|
|
|
|||
|
|
BACKUP_DIR="/secure-backup"
|
|||
|
|
ENCRYPTION_KEY_FILE="/etc/backup-encryption.key"
|
|||
|
|
DATE=$(date +%Y%m%d_%H%M%S)
|
|||
|
|
|
|||
|
|
# 创建加密密钥(首次运行)
|
|||
|
|
create_encryption_key() {
|
|||
|
|
if [ ! -f "$ENCRYPTION_KEY_FILE" ]; then
|
|||
|
|
openssl rand -base64 32 > "$ENCRYPTION_KEY_FILE"
|
|||
|
|
chmod 600 "$ENCRYPTION_KEY_FILE"
|
|||
|
|
chown root:root "$ENCRYPTION_KEY_FILE"
|
|||
|
|
fi
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# 加密备份文件
|
|||
|
|
encrypt_backup() {
|
|||
|
|
local source_file=$1
|
|||
|
|
local encrypted_file="${source_file}.enc"
|
|||
|
|
|
|||
|
|
openssl enc -aes-256-cbc -salt -in "$source_file" -out "$encrypted_file" -pass file:"$ENCRYPTION_KEY_FILE"
|
|||
|
|
|
|||
|
|
if [ $? -eq 0 ]; then
|
|||
|
|
rm "$source_file" # 删除明文备份
|
|||
|
|
echo "✅ 备份文件已加密: $encrypted_file"
|
|||
|
|
else
|
|||
|
|
echo "❌ 备份文件加密失败"
|
|||
|
|
return 1
|
|||
|
|
fi
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# 解密备份文件
|
|||
|
|
decrypt_backup() {
|
|||
|
|
local encrypted_file=$1
|
|||
|
|
local decrypted_file="${encrypted_file%.enc}"
|
|||
|
|
|
|||
|
|
openssl enc -aes-256-cbc -d -in "$encrypted_file" -out "$decrypted_file" -pass file:"$ENCRYPTION_KEY_FILE"
|
|||
|
|
|
|||
|
|
if [ $? -eq 0 ]; then
|
|||
|
|
echo "✅ 备份文件已解密: $decrypted_file"
|
|||
|
|
else
|
|||
|
|
echo "❌ 备份文件解密失败"
|
|||
|
|
return 1
|
|||
|
|
fi
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# 安全备份主函数
|
|||
|
|
secure_backup() {
|
|||
|
|
echo "开始安全备份: $DATE"
|
|||
|
|
|
|||
|
|
# 创建备份目录
|
|||
|
|
mkdir -p "$BACKUP_DIR"
|
|||
|
|
|
|||
|
|
# 创建加密密钥
|
|||
|
|
create_encryption_key
|
|||
|
|
|
|||
|
|
# 备份数据库
|
|||
|
|
echo "备份数据库..."
|
|||
|
|
docker exec mysql-master mysqldump -u root -p${MYSQL_ROOT_PASSWORD} \
|
|||
|
|
--single-transaction \
|
|||
|
|
--routines \
|
|||
|
|
--triggers \
|
|||
|
|
--all-databases > "$BACKUP_DIR/mysql_backup_$DATE.sql"
|
|||
|
|
|
|||
|
|
# 加密数据库备份
|
|||
|
|
encrypt_backup "$BACKUP_DIR/mysql_backup_$DATE.sql"
|
|||
|
|
|
|||
|
|
# 备份配置文件
|
|||
|
|
echo "备份配置文件..."
|
|||
|
|
tar -czf "$BACKUP_DIR/config_backup_$DATE.tar.gz" ./config ./nginx .env.production
|
|||
|
|
encrypt_backup "$BACKUP_DIR/config_backup_$DATE.tar.gz"
|
|||
|
|
|
|||
|
|
# 生成备份校验和
|
|||
|
|
echo "生成备份校验和..."
|
|||
|
|
find "$BACKUP_DIR" -name "*_$DATE.*.enc" -exec sha256sum {} \; > "$BACKUP_DIR/checksums_$DATE.txt"
|
|||
|
|
|
|||
|
|
echo "安全备份完成"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# 验证备份完整性
|
|||
|
|
verify_backup() {
|
|||
|
|
local checksum_file=$1
|
|||
|
|
|
|||
|
|
if [ ! -f "$checksum_file" ]; then
|
|||
|
|
echo "校验和文件不存在: $checksum_file"
|
|||
|
|
return 1
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
echo "验证备份完整性..."
|
|||
|
|
sha256sum -c "$checksum_file"
|
|||
|
|
|
|||
|
|
if [ $? -eq 0 ]; then
|
|||
|
|
echo "✅ 备份完整性验证通过"
|
|||
|
|
else
|
|||
|
|
echo "❌ 备份完整性验证失败"
|
|||
|
|
return 1
|
|||
|
|
fi
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# 主函数
|
|||
|
|
case $1 in
|
|||
|
|
"backup")
|
|||
|
|
secure_backup
|
|||
|
|
;;
|
|||
|
|
"decrypt")
|
|||
|
|
decrypt_backup $2
|
|||
|
|
;;
|
|||
|
|
"verify")
|
|||
|
|
verify_backup $2
|
|||
|
|
;;
|
|||
|
|
*)
|
|||
|
|
echo "使用方法: $0 {backup|decrypt <file>|verify <checksum_file>}"
|
|||
|
|
;;
|
|||
|
|
esac
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 5. 网络安全
|
|||
|
|
|
|||
|
|
### 5.1 防火墙配置
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
#!/bin/bash
|
|||
|
|
# firewall-config.sh - 防火墙配置脚本
|
|||
|
|
|
|||
|
|
# 清空现有规则
|
|||
|
|
iptables -F
|
|||
|
|
iptables -X
|
|||
|
|
iptables -t nat -F
|
|||
|
|
iptables -t nat -X
|
|||
|
|
|
|||
|
|
# 设置默认策略
|
|||
|
|
iptables -P INPUT DROP
|
|||
|
|
iptables -P FORWARD DROP
|
|||
|
|
iptables -P OUTPUT ACCEPT
|
|||
|
|
|
|||
|
|
# 允许本地回环
|
|||
|
|
iptables -A INPUT -i lo -j ACCEPT
|
|||
|
|
iptables -A OUTPUT -o lo -j ACCEPT
|
|||
|
|
|
|||
|
|
# 允许已建立的连接
|
|||
|
|
iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
|
|||
|
|
|
|||
|
|
# 允许SSH(修改为非标准端口)
|
|||
|
|
iptables -A INPUT -p tcp --dport 2222 -j ACCEPT
|
|||
|
|
|
|||
|
|
# 允许HTTP和HTTPS
|
|||
|
|
iptables -A INPUT -p tcp --dport 80 -j ACCEPT
|
|||
|
|
iptables -A INPUT -p tcp --dport 443 -j ACCEPT
|
|||
|
|
|
|||
|
|
# 允许内网访问数据库端口
|
|||
|
|
iptables -A INPUT -p tcp -s 10.0.0.0/8 --dport 3306 -j ACCEPT
|
|||
|
|
iptables -A INPUT -p tcp -s 172.16.0.0/12 --dport 3306 -j ACCEPT
|
|||
|
|
iptables -A INPUT -p tcp -s 192.168.0.0/16 --dport 3306 -j ACCEPT
|
|||
|
|
|
|||
|
|
# 允许Redis访问
|
|||
|
|
iptables -A INPUT -p tcp -s 10.0.0.0/8 --dport 6379 -j ACCEPT
|
|||
|
|
iptables -A INPUT -p tcp -s 172.16.0.0/12 --dport 6379 -j ACCEPT
|
|||
|
|
iptables -A INPUT -p tcp -s 192.168.0.0/16 --dport 6379 -j ACCEPT
|
|||
|
|
|
|||
|
|
# 防止DDoS攻击
|
|||
|
|
iptables -A INPUT -p tcp --dport 80 -m limit --limit 25/minute --limit-burst 100 -j ACCEPT
|
|||
|
|
iptables -A INPUT -p tcp --dport 443 -m limit --limit 25/minute --limit-burst 100 -j ACCEPT
|
|||
|
|
|
|||
|
|
# 防止端口扫描
|
|||
|
|
iptables -A INPUT -m recent --name portscan --rcheck --seconds 86400 -j DROP
|
|||
|
|
iptables -A INPUT -m recent --name portscan --remove
|
|||
|
|
iptables -A INPUT -p tcp -m tcp --dport 139 -m recent --name portscan --set -j LOG --log-prefix "portscan:"
|
|||
|
|
iptables -A INPUT -p tcp -m tcp --dport 139 -j DROP
|
|||
|
|
|
|||
|
|
# 记录被丢弃的包
|
|||
|
|
iptables -A INPUT -m limit --limit 5/min -j LOG --log-prefix "iptables denied: " --log-level 7
|
|||
|
|
|
|||
|
|
# 保存规则
|
|||
|
|
iptables-save > /etc/iptables/rules.v4
|
|||
|
|
|
|||
|
|
echo "防火墙配置完成"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 5.2 入侵检测系统
|
|||
|
|
|
|||
|
|
```yaml
|
|||
|
|
# fail2ban配置
|
|||
|
|
# /etc/fail2ban/jail.local
|
|||
|
|
[DEFAULT]
|
|||
|
|
# 禁用时间(秒)
|
|||
|
|
bantime = 3600
|
|||
|
|
# 查找时间窗口(秒)
|
|||
|
|
findtime = 600
|
|||
|
|
# 最大重试次数
|
|||
|
|
maxretry = 3
|
|||
|
|
# 忽略的IP地址
|
|||
|
|
ignoreip = 127.0.0.1/8 ::1 192.168.1.0/24
|
|||
|
|
|
|||
|
|
[sshd]
|
|||
|
|
enabled = true
|
|||
|
|
port = 2222
|
|||
|
|
filter = sshd
|
|||
|
|
logpath = /var/log/auth.log
|
|||
|
|
maxretry = 3
|
|||
|
|
bantime = 3600
|
|||
|
|
|
|||
|
|
[nginx-http-auth]
|
|||
|
|
enabled = true
|
|||
|
|
filter = nginx-http-auth
|
|||
|
|
logpath = /var/log/nginx/error.log
|
|||
|
|
maxretry = 3
|
|||
|
|
bantime = 3600
|
|||
|
|
|
|||
|
|
[nginx-limit-req]
|
|||
|
|
enabled = true
|
|||
|
|
filter = nginx-limit-req
|
|||
|
|
logpath = /var/log/nginx/error.log
|
|||
|
|
maxretry = 3
|
|||
|
|
bantime = 3600
|
|||
|
|
|
|||
|
|
[nginx-botsearch]
|
|||
|
|
enabled = true
|
|||
|
|
filter = nginx-botsearch
|
|||
|
|
logpath = /var/log/nginx/access.log
|
|||
|
|
maxretry = 2
|
|||
|
|
bantime = 7200
|
|||
|
|
|
|||
|
|
# 自定义过滤器
|
|||
|
|
# /etc/fail2ban/filter.d/nginx-botsearch.conf
|
|||
|
|
[Definition]
|
|||
|
|
failregex = ^<HOST> -.*"(GET|POST).*HTTP.*" (404|444) .*$
|
|||
|
|
ignoreregex =
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
#!/bin/bash
|
|||
|
|
# intrusion-detection.sh - 入侵检测脚本
|
|||
|
|
|
|||
|
|
LOG_FILE="/var/log/intrusion-detection.log"
|
|||
|
|
ALERT_EMAIL="security@xlxumu.com"
|
|||
|
|
|
|||
|
|
# 检查异常登录
|
|||
|
|
check_suspicious_logins() {
|
|||
|
|
echo "检查异常登录..." | tee -a $LOG_FILE
|
|||
|
|
|
|||
|
|
# 检查失败登录次数
|
|||
|
|
failed_logins=$(grep "Failed password" /var/log/auth.log | grep "$(date +%b\ %d)" | wc -l)
|
|||
|
|
if [ $failed_logins -gt 10 ]; then
|
|||
|
|
echo "警告: 今日失败登录次数过多 ($failed_logins)" | tee -a $LOG_FILE
|
|||
|
|
send_alert "异常登录检测" "今日失败登录次数: $failed_logins"
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
# 检查异常IP
|
|||
|
|
suspicious_ips=$(grep "Failed password" /var/log/auth.log | grep "$(date +%b\ %d)" | awk '{print $11}' | sort | uniq -c | sort -nr | head -5)
|
|||
|
|
if [ ! -z "$suspicious_ips" ]; then
|
|||
|
|
echo "可疑IP地址:" | tee -a $LOG_FILE
|
|||
|
|
echo "$suspicious_ips" | tee -a $LOG_FILE
|
|||
|
|
fi
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# 检查异常网络连接
|
|||
|
|
check_network_connections() {
|
|||
|
|
echo "检查异常网络连接..." | tee -a $LOG_FILE
|
|||
|
|
|
|||
|
|
# 检查大量连接的IP
|
|||
|
|
high_conn_ips=$(netstat -an | grep :80 | grep ESTABLISHED | awk '{print $5}' | cut -d: -f1 | sort | uniq -c | sort -nr | head -10)
|
|||
|
|
echo "高连接数IP:" | tee -a $LOG_FILE
|
|||
|
|
echo "$high_conn_ips" | tee -a $LOG_FILE
|
|||
|
|
|
|||
|
|
# 检查异常端口连接
|
|||
|
|
unusual_ports=$(netstat -tuln | grep -v -E "(22|80|443|3306|6379|27017)" | grep LISTEN)
|
|||
|
|
if [ ! -z "$unusual_ports" ]; then
|
|||
|
|
echo "异常监听端口:" | tee -a $LOG_FILE
|
|||
|
|
echo "$unusual_ports" | tee -a $LOG_FILE
|
|||
|
|
send_alert "异常端口检测" "发现异常监听端口: $unusual_ports"
|
|||
|
|
fi
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# 检查文件完整性
|
|||
|
|
check_file_integrity() {
|
|||
|
|
echo "检查文件完整性..." | tee -a $LOG_FILE
|
|||
|
|
|
|||
|
|
# 检查关键系统文件
|
|||
|
|
critical_files=("/etc/passwd" "/etc/shadow" "/etc/ssh/sshd_config" "/etc/sudoers")
|
|||
|
|
|
|||
|
|
for file in "${critical_files[@]}"; do
|
|||
|
|
if [ -f "$file" ]; then
|
|||
|
|
current_hash=$(sha256sum "$file" | awk '{print $1}')
|
|||
|
|
stored_hash_file="/var/lib/integrity/${file//\//_}.hash"
|
|||
|
|
|
|||
|
|
if [ -f "$stored_hash_file" ]; then
|
|||
|
|
stored_hash=$(cat "$stored_hash_file")
|
|||
|
|
if [ "$current_hash" != "$stored_hash" ]; then
|
|||
|
|
echo "警告: 文件 $file 已被修改" | tee -a $LOG_FILE
|
|||
|
|
send_alert "文件完整性检测" "关键文件 $file 已被修改"
|
|||
|
|
fi
|
|||
|
|
else
|
|||
|
|
# 首次运行,存储哈希值
|
|||
|
|
mkdir -p "/var/lib/integrity"
|
|||
|
|
echo "$current_hash" > "$stored_hash_file"
|
|||
|
|
fi
|
|||
|
|
fi
|
|||
|
|
done
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# 检查恶意进程
|
|||
|
|
check_malicious_processes() {
|
|||
|
|
echo "检查恶意进程..." | tee -a $LOG_FILE
|
|||
|
|
|
|||
|
|
# 检查高CPU使用率进程
|
|||
|
|
high_cpu_processes=$(ps aux --sort=-%cpu | head -10 | awk '$3 > 80 {print $2, $11}')
|
|||
|
|
if [ ! -z "$high_cpu_processes" ]; then
|
|||
|
|
echo "高CPU使用率进程:" | tee -a $LOG_FILE
|
|||
|
|
echo "$high_cpu_processes" | tee -a $LOG_FILE
|
|||
|
|
fi
|
|||
|
|
|
|||
|
|
# 检查可疑进程名
|
|||
|
|
suspicious_processes=$(ps aux | grep -E "(nc|netcat|ncat|socat|wget|curl)" | grep -v grep)
|
|||
|
|
if [ ! -z "$suspicious_processes" ]; then
|
|||
|
|
echo "可疑进程:" | tee -a $LOG_FILE
|
|||
|
|
echo "$suspicious_processes" | tee -a $LOG_FILE
|
|||
|
|
send_alert "可疑进程检测" "发现可疑进程: $suspicious_processes"
|
|||
|
|
fi
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# 发送告警
|
|||
|
|
send_alert() {
|
|||
|
|
local subject=$1
|
|||
|
|
local message=$2
|
|||
|
|
|
|||
|
|
# 发送邮件告警
|
|||
|
|
echo "$message" | mail -s "$subject" "$ALERT_EMAIL"
|
|||
|
|
|
|||
|
|
# 发送钉钉告警
|
|||
|
|
curl -X POST "https://oapi.dingtalk.com/robot/send?access_token=YOUR_TOKEN" \
|
|||
|
|
-H 'Content-Type: application/json' \
|
|||
|
|
-d "{\"msgtype\": \"text\",\"text\": {\"content\": \"🚨 安全告警: $subject\\n$message\"}}"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# 主函数
|
|||
|
|
main() {
|
|||
|
|
echo "=== 入侵检测开始 $(date) ===" | tee -a $LOG_FILE
|
|||
|
|
|
|||
|
|
check_suspicious_logins
|
|||
|
|
check_network_connections
|
|||
|
|
check_file_integrity
|
|||
|
|
check_malicious_processes
|
|||
|
|
|
|||
|
|
echo "=== 入侵检测完成 ===" | tee -a $LOG_FILE
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
main
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 6. 应用安全
|
|||
|
|
|
|||
|
|
### 6.1 输入验证与过滤
|
|||
|
|
|
|||
|
|
```javascript
|
|||
|
|
// 输入验证中间件
|
|||
|
|
const validator = require('validator');
|
|||
|
|
const xss = require('xss');
|
|||
|
|
|
|||
|
|
class InputValidator {
|
|||
|
|
// 通用验证规则
|
|||
|
|
static rules = {
|
|||
|
|
username: {
|
|||
|
|
required: true,
|
|||
|
|
minLength: 3,
|
|||
|
|
maxLength: 20,
|
|||
|
|
pattern: /^[a-zA-Z0-9_]+$/,
|
|||
|
|
message: '用户名只能包含字母、数字和下划线,长度3-20位'
|
|||
|
|
},
|
|||
|
|
email: {
|
|||
|
|
required: true,
|
|||
|
|
validator: validator.isEmail,
|
|||
|
|
message: '请输入有效的邮箱地址'
|
|||
|
|
},
|
|||
|
|
phone: {
|
|||
|
|
required: true,
|
|||
|
|
pattern: /^1[3-9]\d{9}$/,
|
|||
|
|
message: '请输入有效的手机号码'
|
|||
|
|
},
|
|||
|
|
password: {
|
|||
|
|
required: true,
|
|||
|
|
minLength: 8,
|
|||
|
|
maxLength: 128,
|
|||
|
|
pattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/,
|
|||
|
|
message: '密码必须包含大小写字母、数字和特殊字符,长度8-128位'
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 验证单个字段
|
|||
|
|
static validateField(value, rule) {
|
|||
|
|
const errors = [];
|
|||
|
|
|
|||
|
|
// 必填验证
|
|||
|
|
if (rule.required && (!value || value.toString().trim() === '')) {
|
|||
|
|
errors.push('该字段为必填项');
|
|||
|
|
return errors;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!value) return errors;
|
|||
|
|
|
|||
|
|
const stringValue = value.toString();
|
|||
|
|
|
|||
|
|
// 长度验证
|
|||
|
|
if (rule.minLength && stringValue.length < rule.minLength) {
|
|||
|
|
errors.push(`最小长度为 ${rule.minLength}`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (rule.maxLength && stringValue.length > rule.maxLength) {
|
|||
|
|
errors.push(`最大长度为 ${rule.maxLength}`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 正则验证
|
|||
|
|
if (rule.pattern && !rule.pattern.test(stringValue)) {
|
|||
|
|
errors.push(rule.message || '格式不正确');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 自定义验证器
|
|||
|
|
if (rule.validator && !rule.validator(stringValue)) {
|
|||
|
|
errors.push(rule.message || '验证失败');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return errors;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 验证对象
|
|||
|
|
static validate(data, rules) {
|
|||
|
|
const errors = {};
|
|||
|
|
|
|||
|
|
Object.keys(rules).forEach(field => {
|
|||
|
|
const fieldErrors = this.validateField(data[field], rules[field]);
|
|||
|
|
if (fieldErrors.length > 0) {
|
|||
|
|
errors[field] = fieldErrors;
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
isValid: Object.keys(errors).length === 0,
|
|||
|
|
errors: errors
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// XSS过滤
|
|||
|
|
static sanitizeInput(input) {
|
|||
|
|
if (typeof input === 'string') {
|
|||
|
|
return xss(input, {
|
|||
|
|
whiteList: {}, // 不允许任何HTML标签
|
|||
|
|
stripIgnoreTag: true,
|
|||
|
|
stripIgnoreTagBody: ['script']
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (typeof input === 'object' && input !== null) {
|
|||
|
|
const sanitized = {};
|
|||
|
|
Object.keys(input).forEach(key => {
|
|||
|
|
sanitized[key] = this.sanitizeInput(input[key]);
|
|||
|
|
});
|
|||
|
|
return sanitized;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return input;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// SQL注入防护
|
|||
|
|
static escapeSql(input) {
|
|||
|
|
if (typeof input === 'string') {
|
|||
|
|
return input.replace(/[\0\x08\x09\x1a\n\r"'\\\%]/g, function (char) {
|
|||
|
|
switch (char) {
|
|||
|
|
case "\0":
|
|||
|
|
return "\\0";
|
|||
|
|
case "\x08":
|
|||
|
|
return "\\b";
|
|||
|
|
case "\x09":
|
|||
|
|
return "\\t";
|
|||
|
|
case "\x1a":
|
|||
|
|
return "\\z";
|
|||
|
|
case "\n":
|
|||
|
|
return "\\n";
|
|||
|
|
case "\r":
|
|||
|
|
return "\\r";
|
|||
|
|
case "\"":
|
|||
|
|
case "'":
|
|||
|
|
case "\\":
|
|||
|
|
case "%":
|
|||
|
|
return "\\" + char;
|
|||
|
|
default:
|
|||
|
|
return char;
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
return input;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 验证中间件
|
|||
|
|
const validationMiddleware = (rules) => {
|
|||
|
|
return (req, res, next) => {
|
|||
|
|
// XSS过滤
|
|||
|
|
req.body = InputValidator.sanitizeInput(req.body);
|
|||
|
|
req.query = InputValidator.sanitizeInput(req.query);
|
|||
|
|
req.params = InputValidator.sanitizeInput(req.params);
|
|||
|
|
|
|||
|
|
// 输入验证
|
|||
|
|
const validation = InputValidator.validate(req.body, rules);
|
|||
|
|
|
|||
|
|
if (!validation.isValid) {
|
|||
|
|
return res.status(400).json({
|
|||
|
|
error: 'Validation failed',
|
|||
|
|
details: validation.errors
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
next();
|
|||
|
|
};
|
|||
|
|
};
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 6.2 API安全
|
|||
|
|
|
|||
|
|
```javascript
|
|||
|
|
// API安全中间件
|
|||
|
|
const rateLimit = require('express-rate-limit');
|
|||
|
|
const helmet = require('helmet');
|
|||
|
|
const cors = require('cors');
|
|||
|
|
|
|||
|
|
// 速率限制
|
|||
|
|
const createRateLimit = (windowMs, max, message) => {
|
|||
|
|
return rateLimit({
|
|||
|
|
windowMs: windowMs,
|
|||
|
|
max: max,
|
|||
|
|
message: {
|
|||
|
|
error: 'Too many requests',
|
|||
|
|
message: message,
|
|||
|
|
retryAfter: Math.ceil(windowMs / 1000)
|
|||
|
|
},
|
|||
|
|
standardHeaders: true,
|
|||
|
|
legacyHeaders: false,
|
|||
|
|
// 自定义键生成器(基于IP和用户ID)
|
|||
|
|
keyGenerator: (req) => {
|
|||
|
|
return req.user ? `${req.ip}-${req.user.id}` : req.ip;
|
|||
|
|
},
|
|||
|
|
// 跳过成功的请求
|
|||
|
|
skipSuccessfulRequests: true
|
|||
|
|
});
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 不同类型的速率限制
|
|||
|
|
const rateLimits = {
|
|||
|
|
// 通用API限制
|
|||
|
|
general: createRateLimit(15 * 60 * 1000, 100, '请求过于频繁,请稍后再试'),
|
|||
|
|
|
|||
|
|
// 登录限制
|
|||
|
|
auth: createRateLimit(15 * 60 * 1000, 5, '登录尝试过于频繁,请15分钟后再试'),
|
|||
|
|
|
|||
|
|
// 注册限制
|
|||
|
|
register: createRateLimit(60 * 60 * 1000, 3, '注册请求过于频繁,请1小时后再试'),
|
|||
|
|
|
|||
|
|
// 密码重置限制
|
|||
|
|
passwordReset: createRateLimit(60 * 60 * 1000, 3, '密码重置请求过于频繁,请1小时后再试'),
|
|||
|
|
|
|||
|
|
// 文件上传限制
|
|||
|
|
upload: createRateLimit(60 * 60 * 1000, 10, '文件上传过于频繁,请1小时后再试')
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// CORS配置
|
|||
|
|
const corsOptions = {
|
|||
|
|
origin: function (origin, callback) {
|
|||
|
|
const allowedOrigins = [
|
|||
|
|
'https://www.xlxumu.com',
|
|||
|
|
'https://admin.xlxumu.com',
|
|||
|
|
'https://api.xlxumu.com'
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
// 允许移动应用和开发环境
|
|||
|
|
if (!origin || allowedOrigins.includes(origin) ||
|
|||
|
|
(process.env.NODE_ENV === 'development' && origin.includes('localhost'))) {
|
|||
|
|
callback(null, true);
|
|||
|
|
} else {
|
|||
|
|
callback(new Error('Not allowed by CORS'));
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
credentials: true,
|
|||
|
|
optionsSuccessStatus: 200,
|
|||
|
|
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
|||
|
|
allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With']
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 安全头配置
|
|||
|
|
const helmetOptions = {
|
|||
|
|
contentSecurityPolicy: {
|
|||
|
|
directives: {
|
|||
|
|
defaultSrc: ["'self'"],
|
|||
|
|
styleSrc: ["'self'", "'unsafe-inline'"],
|
|||
|
|
scriptSrc: ["'self'"],
|
|||
|
|
imgSrc: ["'self'", "data:", "https:"],
|
|||
|
|
connectSrc: ["'self'"],
|
|||
|
|
fontSrc: ["'self'"],
|
|||
|
|
objectSrc: ["'none'"],
|
|||
|
|
mediaSrc: ["'self'"],
|
|||
|
|
frameSrc: ["'none'"]
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
hsts: {
|
|||
|
|
maxAge: 31536000,
|
|||
|
|
includeSubDomains: true,
|
|||
|
|
preload: true
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// API安全中间件组合
|
|||
|
|
const apiSecurity = (app) => {
|
|||
|
|
// 基础安全头
|
|||
|
|
app.use(helmet(helmetOptions));
|
|||
|
|
|
|||
|
|
// CORS配置
|
|||
|
|
app.use(cors(corsOptions));
|
|||
|
|
|
|||
|
|
// 通用速率限制
|
|||
|
|
app.use('/api/', rateLimits.general);
|
|||
|
|
|
|||
|
|
// 特定路由的速率限制
|
|||
|
|
app.use('/api/auth/login', rateLimits.auth);
|
|||
|
|
app.use('/api/auth/register', rateLimits.register);
|
|||
|
|
app.use('/api/auth/reset-password', rateLimits.passwordReset);
|
|||
|
|
app.use('/api/upload', rateLimits.upload);
|
|||
|
|
|
|||
|
|
// 请求大小限制
|
|||
|
|
app.use(express.json({ limit: '10mb' }));
|
|||
|
|
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
|
|||
|
|
|
|||
|
|
// 隐藏技术栈信息
|
|||
|
|
app.disable('x-powered-by');
|
|||
|
|
|
|||
|
|
// API版本控制
|
|||
|
|
app.use('/api/v1', require('./routes/v1'));
|
|||
|
|
|
|||
|
|
// 404处理
|
|||
|
|
app.use('/api/*', (req, res) => {
|
|||
|
|
res.status(404).json({
|
|||
|
|
error: 'API endpoint not found',
|
|||
|
|
path: req.path,
|
|||
|
|
method: req.method
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 错误处理
|
|||
|
|
app.use((error, req, res, next) => {
|
|||
|
|
// 记录错误日志
|
|||
|
|
console.error('API Error:', {
|
|||
|
|
error: error.message,
|
|||
|
|
stack: error.stack,
|
|||
|
|
url: req.url,
|
|||
|
|
method: req.method,
|
|||
|
|
ip: req.ip,
|
|||
|
|
userAgent: req.get('User-Agent')
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 不暴露内部错误信息
|
|||
|
|
if (process.env.NODE_ENV === 'production') {
|
|||
|
|
res.status(500).json({
|
|||
|
|
error: 'Internal server error',
|
|||
|
|
message: 'Something went wrong'
|
|||
|
|
});
|
|||
|
|
} else {
|
|||
|
|
res.status(500).json({
|
|||
|
|
error: error.message,
|
|||
|
|
stack: error.stack
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
};
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 6.3 文件上传安全
|
|||
|
|
|
|||
|
|
```javascript
|
|||
|
|
// 文件上传安全配置
|
|||
|
|
const multer = require('multer');
|
|||
|
|
const path = require('path');
|
|||
|
|
const crypto = require('crypto');
|
|||
|
|
const sharp = require('sharp');
|
|||
|
|
|
|||
|
|
class SecureFileUpload {
|
|||
|
|
constructor() {
|
|||
|
|
this.allowedMimeTypes = {
|
|||
|
|
image: ['image/jpeg', 'image/png', 'image/gif', 'image/webp'],
|
|||
|
|
document: ['application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'],
|
|||
|
|
excel: ['application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet']
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
this.maxFileSizes = {
|
|||
|
|
image: 5 * 1024 * 1024, // 5MB
|
|||
|
|
document: 10 * 1024 * 1024, // 10MB
|
|||
|
|
excel: 20 * 1024 * 1024 // 20MB
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
this.uploadDir = './uploads';
|
|||
|
|
this.quarantineDir = './quarantine';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 文件类型检测
|
|||
|
|
detectFileType(buffer) {
|
|||
|
|
// 检查文件头魔数
|
|||
|
|
const signatures = {
|
|||
|
|
'image/jpeg': [0xFF, 0xD8, 0xFF],
|
|||
|
|
'image/png': [0x89, 0x50, 0x4E, 0x47],
|
|||
|
|
'image/gif': [0x47, 0x49, 0x46],
|
|||
|
|
'application/pdf': [0x25, 0x50, 0x44, 0x46]
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
for (const [mimeType, signature] of Object.entries(signatures)) {
|
|||
|
|
if (this.checkSignature(buffer, signature)) {
|
|||
|
|
return mimeType;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查文件签名
|
|||
|
|
checkSignature(buffer, signature) {
|
|||
|
|
if (buffer.length < signature.length) return false;
|
|||
|
|
|
|||
|
|
for (let i = 0; i < signature.length; i++) {
|
|||
|
|
if (buffer[i] !== signature[i]) return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 文件名安全化
|
|||
|
|
sanitizeFilename(filename) {
|
|||
|
|
// 移除危险字符
|
|||
|
|
const sanitized = filename.replace(/[^a-zA-Z0-9.-]/g, '_');
|
|||
|
|
|
|||
|
|
// 生成唯一文件名
|
|||
|
|
const ext = path.extname(sanitized);
|
|||
|
|
const name = path.basename(sanitized, ext);
|
|||
|
|
const hash = crypto.randomBytes(8).toString('hex');
|
|||
|
|
|
|||
|
|
return `${name}_${hash}${ext}`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 病毒扫描(集成ClamAV)
|
|||
|
|
async scanForVirus(filePath) {
|
|||
|
|
return new Promise((resolve, reject) => {
|
|||
|
|
const { exec } = require('child_process');
|
|||
|
|
|
|||
|
|
exec(`clamscan --no-summary ${filePath}`, (error, stdout, stderr) => {
|
|||
|
|
if (error) {
|
|||
|
|
if (error.code === 1) {
|
|||
|
|
// 发现病毒
|
|||
|
|
resolve({ infected: true, virus: stdout.trim() });
|
|||
|
|
} else {
|
|||
|
|
// 扫描错误
|
|||
|
|
reject(new Error('Virus scan failed'));
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
// 文件安全
|
|||
|
|
resolve({ infected: false });
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 图片安全处理
|
|||
|
|
async processImage(inputPath, outputPath) {
|
|||
|
|
try {
|
|||
|
|
// 使用sharp重新处理图片,移除EXIF数据
|
|||
|
|
await sharp(inputPath)
|
|||
|
|
.jpeg({ quality: 90, progressive: true })
|
|||
|
|
.png({ compressionLevel: 9 })
|
|||
|
|
.removeAlpha()
|
|||
|
|
.toFile(outputPath);
|
|||
|
|
|
|||
|
|
return true;
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('Image processing failed:', error);
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 创建安全的multer配置
|
|||
|
|
createMulterConfig(fileType) {
|
|||
|
|
const storage = multer.diskStorage({
|
|||
|
|
destination: (req, file, cb) => {
|
|||
|
|
cb(null, this.uploadDir);
|
|||
|
|
},
|
|||
|
|
filename: (req, file, cb) => {
|
|||
|
|
const safeFilename = this.sanitizeFilename(file.originalname);
|
|||
|
|
cb(null, safeFilename);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const fileFilter = (req, file, cb) => {
|
|||
|
|
// 检查MIME类型
|
|||
|
|
if (!this.allowedMimeTypes[fileType].includes(file.mimetype)) {
|
|||
|
|
return cb(new Error(`不支持的文件类型: ${file.mimetype}`));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
cb(null, true);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
return multer({
|
|||
|
|
storage: storage,
|
|||
|
|
limits: {
|
|||
|
|
fileSize: this.maxFileSizes[fileType],
|
|||
|
|
files: 5 // 最多5个文件
|
|||
|
|
},
|
|||
|
|
fileFilter: fileFilter
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 文件上传后处理
|
|||
|
|
async postProcessFile(file) {
|
|||
|
|
const filePath = file.path;
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
// 1. 检查文件头
|
|||
|
|
const buffer = require('fs').readFileSync(filePath);
|
|||
|
|
const detectedType = this.detectFileType(buffer);
|
|||
|
|
|
|||
|
|
if (!detectedType || detectedType !== file.mimetype) {
|
|||
|
|
throw new Error('文件类型不匹配');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 2. 病毒扫描
|
|||
|
|
const scanResult = await this.scanForVirus(filePath);
|
|||
|
|
if (scanResult.infected) {
|
|||
|
|
// 移动到隔离区
|
|||
|
|
const quarantinePath = path.join(this.quarantineDir, file.filename);
|
|||
|
|
require('fs').renameSync(filePath, quarantinePath);
|
|||
|
|
throw new Error(`检测到病毒: ${scanResult.virus}`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 3. 图片特殊处理
|
|||
|
|
if (file.mimetype.startsWith('image/')) {
|
|||
|
|
const processedPath = filePath + '.processed';
|
|||
|
|
const success = await this.processImage(filePath, processedPath);
|
|||
|
|
|
|||
|
|
if (success) {
|
|||
|
|
require('fs').renameSync(processedPath, filePath);
|
|||
|
|
} else {
|
|||
|
|
throw new Error('图片处理失败');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
success: true,
|
|||
|
|
file: {
|
|||
|
|
filename: file.filename,
|
|||
|
|
originalname: file.originalname,
|
|||
|
|
mimetype: file.mimetype,
|
|||
|
|
size: file.size,
|
|||
|
|
path: filePath
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
} catch (error) {
|
|||
|
|
// 删除有问题的文件
|
|||
|
|
if (require('fs').existsSync(filePath)) {
|
|||
|
|
require('fs').unlinkSync(filePath);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
throw error;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 使用示例
|
|||
|
|
const secureUpload = new SecureFileUpload();
|
|||
|
|
|
|||
|
|
// 图片上传路由
|
|||
|
|
app.post('/api/upload/image',
|
|||
|
|
secureUpload.createMulterConfig('image').single('image'),
|
|||
|
|
async (req, res) => {
|
|||
|
|
try {
|
|||
|
|
if (!req.file) {
|
|||
|
|
return res.status(400).json({ error: '没有上传文件' });
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const result = await secureUpload.postProcessFile(req.file);
|
|||
|
|
res.json(result);
|
|||
|
|
|
|||
|
|
} catch (error) {
|
|||
|
|
res.status(400).json({ error: error.message });
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 7. 安全监控与审计
|
|||
|
|
|
|||
|
|
### 7.1 安全日志记录
|
|||
|
|
|
|||
|
|
```javascript
|
|||
|
|
// 安全审计日志
|
|||
|
|
class SecurityAuditLogger {
|
|||
|
|
constructor() {
|
|||
|
|
this.winston = require('winston');
|
|||
|
|
this.logger = this.winston.createLogger({
|
|||
|
|
level: 'info',
|
|||
|
|
format: this.winston.format.combine(
|
|||
|
|
this.winston.format.timestamp(),
|
|||
|
|
this.winston.format.json()
|
|||
|
|
),
|
|||
|
|
transports: [
|
|||
|
|
new this.winston.transports.File({
|
|||
|
|
filename: '/var/log/security/security-audit.log',
|
|||
|
|
maxsize: 100 * 1024 * 1024, // 100MB
|
|||
|
|
maxFiles: 10
|
|||
|
|
}),
|
|||
|
|
new this.winston.transports.Console({
|
|||
|
|
format: this.winston.format.simple()
|
|||
|
|
})
|
|||
|
|
]
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 记录认证事件
|
|||
|
|
logAuthEvent(event, userId, ip, userAgent, success, details = {}) {
|
|||
|
|
this.logger.info('AUTH_EVENT', {
|
|||
|
|
event: event,
|
|||
|
|
userId: userId,
|
|||
|
|
ip: ip,
|
|||
|
|
userAgent: userAgent,
|
|||
|
|
success: success,
|
|||
|
|
timestamp: new Date().toISOString(),
|
|||
|
|
details: details
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 记录权限事件
|
|||
|
|
logPermissionEvent(userId, resource, action, granted, ip) {
|
|||
|
|
this.logger.info('PERMISSION_EVENT', {
|
|||
|
|
userId: userId,
|
|||
|
|
resource: resource,
|
|||
|
|
action: action,
|
|||
|
|
granted: granted,
|
|||
|
|
ip: ip,
|
|||
|
|
timestamp: new Date().toISOString()
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 记录数据访问事件
|
|||
|
|
logDataAccess(userId, dataType, recordId, action, ip) {
|
|||
|
|
this.logger.info('DATA_ACCESS', {
|
|||
|
|
userId: userId,
|
|||
|
|
dataType: dataType,
|
|||
|
|
recordId: recordId,
|
|||
|
|
action: action,
|
|||
|
|
ip: ip,
|
|||
|
|
timestamp: new Date().toISOString()
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 记录安全事件
|
|||
|
|
logSecurityEvent(eventType, severity, description, ip, details = {}) {
|
|||
|
|
this.logger.warn('SECURITY_EVENT', {
|
|||
|
|
eventType: eventType,
|
|||
|
|
severity: severity,
|
|||
|
|
description: description,
|
|||
|
|
ip: ip,
|
|||
|
|
timestamp: new Date().toISOString(),
|
|||
|
|
details: details
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 记录系统事件
|
|||
|
|
logSystemEvent(eventType, description, details = {}) {
|
|||
|
|
this.logger.info('SYSTEM_EVENT', {
|
|||
|
|
eventType: eventType,
|
|||
|
|
description: description,
|
|||
|
|
timestamp: new Date().toISOString(),
|
|||
|
|
details: details
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 审计中间件
|
|||
|
|
const auditMiddleware = (auditLogger) => {
|
|||
|
|
return (req, res, next) => {
|
|||
|
|
const startTime = Date.now();
|
|||
|
|
|
|||
|
|
// 记录请求开始
|
|||
|
|
const requestId = require('crypto').randomUUID();
|
|||
|
|
req.requestId = requestId;
|
|||
|
|
|
|||
|
|
// 重写res.json以记录响应
|
|||
|
|
const originalJson = res.json;
|
|||
|
|
res.json = function(data) {
|
|||
|
|
const endTime = Date.now();
|
|||
|
|
const duration = endTime - startTime;
|
|||
|
|
|
|||
|
|
// 记录API访问
|
|||
|
|
auditLogger.logger.info('API_ACCESS', {
|
|||
|
|
requestId: requestId,
|
|||
|
|
method: req.method,
|
|||
|
|
url: req.url,
|
|||
|
|
ip: req.ip,
|
|||
|
|
userAgent: req.get('User-Agent'),
|
|||
|
|
userId: req.user?.id,
|
|||
|
|
statusCode: res.statusCode,
|
|||
|
|
duration: duration,
|
|||
|
|
timestamp: new Date().toISOString()
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 记录敏感操作
|
|||
|
|
if (['POST', 'PUT', 'DELETE'].includes(req.method)) {
|
|||
|
|
auditLogger.logDataAccess(
|
|||
|
|
req.user?.id,
|
|||
|
|
req.url.split('/')[2], // 提取资源类型
|
|||
|
|
req.params.id,
|
|||
|
|
req.method,
|
|||
|
|
req.ip
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return originalJson.call(this, data);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
next();
|
|||
|
|
};
|
|||
|
|
};
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 7.2 实时安全监控
|
|||
|
|
|
|||
|
|
```javascript
|
|||
|
|
// 实时安全监控系统
|
|||
|
|
class SecurityMonitor {
|
|||
|
|
constructor() {
|
|||
|
|
this.redis = require('redis').createClient();
|
|||
|
|
this.alerts = [];
|
|||
|
|
this.thresholds = {
|
|||
|
|
failedLogins: { count: 5, window: 300 }, // 5分钟内5次失败
|
|||
|
|
apiCalls: { count: 1000, window: 60 }, // 1分钟内1000次调用
|
|||
|
|
dataAccess: { count: 100, window: 300 } // 5分钟内100次数据访问
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查失败登录
|
|||
|
|
async checkFailedLogins(ip) {
|
|||
|
|
const key = `failed_logins:${ip}`;
|
|||
|
|
const count = await this.redis.incr(key);
|
|||
|
|
|
|||
|
|
if (count === 1) {
|
|||
|
|
await this.redis.expire(key, this.thresholds.failedLogins.window);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (count >= this.thresholds.failedLogins.count) {
|
|||
|
|
this.triggerAlert('FAILED_LOGIN_THRESHOLD', {
|
|||
|
|
ip: ip,
|
|||
|
|
count: count,
|
|||
|
|
threshold: this.thresholds.failedLogins.count
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查API调用频率
|
|||
|
|
async checkApiCalls(userId, endpoint) {
|
|||
|
|
const key = `api_calls:${userId}:${endpoint}`;
|
|||
|
|
const count = await this.redis.incr(key);
|
|||
|
|
|
|||
|
|
if (count === 1) {
|
|||
|
|
await this.redis.expire(key, this.thresholds.apiCalls.window);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (count >= this.thresholds.apiCalls.count) {
|
|||
|
|
this.triggerAlert('API_RATE_LIMIT', {
|
|||
|
|
userId: userId,
|
|||
|
|
endpoint: endpoint,
|
|||
|
|
count: count,
|
|||
|
|
threshold: this.thresholds.apiCalls.count
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 触发安全告警
|
|||
|
|
triggerAlert(alertType, data) {
|
|||
|
|
const alert = {
|
|||
|
|
id: require('crypto').randomUUID(),
|
|||
|
|
type: alertType,
|
|||
|
|
timestamp: new Date().toISOString(),
|
|||
|
|
data: data,
|
|||
|
|
severity: this.getAlertSeverity(alertType)
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
this.alerts.push(alert);
|
|||
|
|
this.sendAlert(alert);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 获取告警严重程度
|
|||
|
|
getAlertSeverity(alertType) {
|
|||
|
|
const severityMap = {
|
|||
|
|
'FAILED_LOGIN_THRESHOLD': 'HIGH',
|
|||
|
|
'API_RATE_LIMIT': 'MEDIUM',
|
|||
|
|
'SUSPICIOUS_ACTIVITY': 'HIGH',
|
|||
|
|
'DATA_BREACH_ATTEMPT': 'CRITICAL'
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
return severityMap[alertType] || 'LOW';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 发送告警
|
|||
|
|
async sendAlert(alert) {
|
|||
|
|
// 发送到监控系统
|
|||
|
|
console.log('🚨 安全告警:', alert);
|
|||
|
|
|
|||
|
|
// 发送邮件通知
|
|||
|
|
if (alert.severity === 'CRITICAL' || alert.severity === 'HIGH') {
|
|||
|
|
await this.sendEmailAlert(alert);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 发送钉钉通知
|
|||
|
|
await this.sendDingTalkAlert(alert);
|
|||
|
|
|
|||
|
|
// 记录到数据库
|
|||
|
|
await this.saveAlertToDatabase(alert);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 7.3 安全事件响应
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
#!/bin/bash
|
|||
|
|
# security-incident-response.sh - 安全事件响应脚本
|
|||
|
|
|
|||
|
|
INCIDENT_LOG="/var/log/security/incidents.log"
|
|||
|
|
BACKUP_DIR="/secure-backup/incident-$(date +%Y%m%d_%H%M%S)"
|
|||
|
|
|
|||
|
|
# 事件响应等级
|
|||
|
|
declare -A RESPONSE_LEVELS=(
|
|||
|
|
["LOW"]="记录日志"
|
|||
|
|
["MEDIUM"]="通知管理员"
|
|||
|
|
["HIGH"]="立即响应"
|
|||
|
|
["CRITICAL"]="紧急响应"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# 记录安全事件
|
|||
|
|
log_incident() {
|
|||
|
|
local severity=$1
|
|||
|
|
local event_type=$2
|
|||
|
|
local description=$3
|
|||
|
|
local affected_systems=$4
|
|||
|
|
|
|||
|
|
echo "$(date '+%Y-%m-%d %H:%M:%S') [$severity] $event_type: $description (影响系统: $affected_systems)" >> $INCIDENT_LOG
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# 隔离受影响系统
|
|||
|
|
isolate_system() {
|
|||
|
|
local system_ip=$1
|
|||
|
|
|
|||
|
|
echo "隔离系统: $system_ip"
|
|||
|
|
|
|||
|
|
# 阻止该IP的所有连接
|
|||
|
|
iptables -I INPUT -s $system_ip -j DROP
|
|||
|
|
iptables -I OUTPUT -d $system_ip -j DROP
|
|||
|
|
|
|||
|
|
# 记录隔离操作
|
|||
|
|
log_incident "HIGH" "SYSTEM_ISOLATION" "系统已被隔离" "$system_ip"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# 紧急备份
|
|||
|
|
emergency_backup() {
|
|||
|
|
echo "执行紧急备份..."
|
|||
|
|
|
|||
|
|
mkdir -p $BACKUP_DIR
|
|||
|
|
|
|||
|
|
# 备份关键数据
|
|||
|
|
docker exec mysql-master mysqldump -u root -p${MYSQL_ROOT_PASSWORD} --all-databases > $BACKUP_DIR/emergency_db_backup.sql
|
|||
|
|
|
|||
|
|
# 备份配置文件
|
|||
|
|
cp -r ./config $BACKUP_DIR/
|
|||
|
|
cp -r ./nginx $BACKUP_DIR/
|
|||
|
|
|
|||
|
|
# 备份日志文件
|
|||
|
|
cp -r /var/log $BACKUP_DIR/
|
|||
|
|
|
|||
|
|
echo "紧急备份完成: $BACKUP_DIR"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# 收集取证信息
|
|||
|
|
collect_forensics() {
|
|||
|
|
local incident_id=$1
|
|||
|
|
local forensics_dir="/var/log/security/forensics/$incident_id"
|
|||
|
|
|
|||
|
|
mkdir -p $forensics_dir
|
|||
|
|
|
|||
|
|
echo "收集取证信息..."
|
|||
|
|
|
|||
|
|
# 系统信息
|
|||
|
|
uname -a > $forensics_dir/system_info.txt
|
|||
|
|
ps aux > $forensics_dir/processes.txt
|
|||
|
|
netstat -tuln > $forensics_dir/network_connections.txt
|
|||
|
|
|
|||
|
|
# 用户信息
|
|||
|
|
who > $forensics_dir/logged_users.txt
|
|||
|
|
last -n 50 > $forensics_dir/login_history.txt
|
|||
|
|
|
|||
|
|
# 文件系统信息
|
|||
|
|
find /tmp -type f -mtime -1 > $forensics_dir/recent_tmp_files.txt
|
|||
|
|
find /var/log -name "*.log" -mtime -1 -exec ls -la {} \; > $forensics_dir/recent_logs.txt
|
|||
|
|
|
|||
|
|
# 网络流量
|
|||
|
|
tcpdump -i any -w $forensics_dir/network_traffic.pcap -c 1000 &
|
|||
|
|
|
|||
|
|
echo "取证信息收集完成: $forensics_dir"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# 事件响应主函数
|
|||
|
|
incident_response() {
|
|||
|
|
local severity=$1
|
|||
|
|
local event_type=$2
|
|||
|
|
local description=$3
|
|||
|
|
local affected_systems=$4
|
|||
|
|
|
|||
|
|
local incident_id="INC-$(date +%Y%m%d%H%M%S)"
|
|||
|
|
|
|||
|
|
echo "=== 安全事件响应开始 ==="
|
|||
|
|
echo "事件ID: $incident_id"
|
|||
|
|
echo "严重程度: $severity"
|
|||
|
|
echo "事件类型: $event_type"
|
|||
|
|
echo "描述: $description"
|
|||
|
|
echo "影响系统: $affected_systems"
|
|||
|
|
|
|||
|
|
# 记录事件
|
|||
|
|
log_incident $severity $event_type "$description" "$affected_systems"
|
|||
|
|
|
|||
|
|
# 根据严重程度执行响应
|
|||
|
|
case $severity in
|
|||
|
|
"CRITICAL")
|
|||
|
|
echo "执行紧急响应..."
|
|||
|
|
emergency_backup
|
|||
|
|
collect_forensics $incident_id
|
|||
|
|
isolate_system $affected_systems
|
|||
|
|
send_critical_alert "$incident_id" "$description"
|
|||
|
|
;;
|
|||
|
|
"HIGH")
|
|||
|
|
echo "执行高级响应..."
|
|||
|
|
collect_forensics $incident_id
|
|||
|
|
send_high_alert "$incident_id" "$description"
|
|||
|
|
;;
|
|||
|
|
"MEDIUM")
|
|||
|
|
echo "执行中级响应..."
|
|||
|
|
send_medium_alert "$incident_id" "$description"
|
|||
|
|
;;
|
|||
|
|
"LOW")
|
|||
|
|
echo "记录低级事件..."
|
|||
|
|
;;
|
|||
|
|
esac
|
|||
|
|
|
|||
|
|
echo "=== 安全事件响应完成 ==="
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# 发送告警通知
|
|||
|
|
send_critical_alert() {
|
|||
|
|
local incident_id=$1
|
|||
|
|
local description=$2
|
|||
|
|
|
|||
|
|
# 发送邮件
|
|||
|
|
echo "🚨 紧急安全事件 - $incident_id: $description" | mail -s "紧急安全告警" security@xlxumu.com
|
|||
|
|
|
|||
|
|
# 发送短信(集成短信服务)
|
|||
|
|
curl -X POST "https://sms-api.example.com/send" \
|
|||
|
|
-H "Authorization: Bearer $SMS_TOKEN" \
|
|||
|
|
-d "phone=13800138000&message=紧急安全事件: $incident_id"
|
|||
|
|
|
|||
|
|
# 发送钉钉通知
|
|||
|
|
curl -X POST "https://oapi.dingtalk.com/robot/send?access_token=$DINGTALK_TOKEN" \
|
|||
|
|
-H 'Content-Type: application/json' \
|
|||
|
|
-d "{\"msgtype\": \"text\",\"text\": {\"content\": \"🚨 紧急安全事件\\n事件ID: $incident_id\\n描述: $description\\n请立即处理!\"}}"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# 使用示例
|
|||
|
|
case $1 in
|
|||
|
|
"test")
|
|||
|
|
incident_response "HIGH" "INTRUSION_ATTEMPT" "检测到入侵尝试" "192.168.1.100"
|
|||
|
|
;;
|
|||
|
|
"isolate")
|
|||
|
|
isolate_system $2
|
|||
|
|
;;
|
|||
|
|
"backup")
|
|||
|
|
emergency_backup
|
|||
|
|
;;
|
|||
|
|
"forensics")
|
|||
|
|
collect_forensics $2
|
|||
|
|
;;
|
|||
|
|
*)
|
|||
|
|
echo "使用方法: $0 {test|isolate <ip>|backup|forensics <incident_id>}"
|
|||
|
|
;;
|
|||
|
|
esac
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 8. 安全培训与意识
|
|||
|
|
|
|||
|
|
### 8.1 安全培训计划
|
|||
|
|
|
|||
|
|
| 培训对象 | 培训内容 | 频率 | 时长 |
|
|||
|
|
|----------|----------|------|------|
|
|||
|
|
| 开发人员 | 安全编码、OWASP Top 10、代码审计 | 季度 | 4小时 |
|
|||
|
|
| 运维人员 | 系统安全、网络安全、应急响应 | 季度 | 6小时 |
|
|||
|
|
| 管理人员 | 安全管理、合规要求、风险评估 | 半年 | 2小时 |
|
|||
|
|
| 全体员工 | 安全意识、钓鱼邮件识别、密码安全 | 月度 | 1小时 |
|
|||
|
|
|
|||
|
|
### 8.2 安全检查清单
|
|||
|
|
|
|||
|
|
#### 8.2.1 日常安全检查
|
|||
|
|
|
|||
|
|
- [ ] 检查系统补丁更新状态
|
|||
|
|
- [ ] 审查用户权限分配
|
|||
|
|
- [ ] 检查防火墙规则
|
|||
|
|
- [ ] 监控异常登录活动
|
|||
|
|
- [ ] 验证备份完整性
|
|||
|
|
- [ ] 检查SSL证书有效期
|
|||
|
|
- [ ] 审查安全日志
|
|||
|
|
- [ ] 测试入侵检测系统
|
|||
|
|
|
|||
|
|
#### 8.2.2 月度安全评估
|
|||
|
|
|
|||
|
|
- [ ] 漏洞扫描
|
|||
|
|
- [ ] 渗透测试
|
|||
|
|
- [ ] 代码安全审计
|
|||
|
|
- [ ] 权限审计
|
|||
|
|
- [ ] 安全配置检查
|
|||
|
|
- [ ] 应急预案演练
|
|||
|
|
- [ ] 安全培训效果评估
|
|||
|
|
- [ ] 合规性检查
|
|||
|
|
|
|||
|
|
## 9. 应急预案
|
|||
|
|
|
|||
|
|
### 9.1 数据泄露应急预案
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
flowchart TD
|
|||
|
|
A[发现数据泄露] --> B[立即隔离]
|
|||
|
|
B --> C[评估影响范围]
|
|||
|
|
C --> D[通知相关人员]
|
|||
|
|
D --> E[收集证据]
|
|||
|
|
E --> F[修复漏洞]
|
|||
|
|
F --> G[恢复服务]
|
|||
|
|
G --> H[事后分析]
|
|||
|
|
H --> I[改进措施]
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 9.2 系统入侵应急预案
|
|||
|
|
|
|||
|
|
1. **发现阶段**
|
|||
|
|
- 监控系统告警
|
|||
|
|
- 异常行为检测
|
|||
|
|
- 用户举报
|
|||
|
|
|
|||
|
|
2. **响应阶段**
|
|||
|
|
- 立即隔离受影响系统
|
|||
|
|
- 保护现场证据
|
|||
|
|
- 通知安全团队
|
|||
|
|
|
|||
|
|
3. **恢复阶段**
|
|||
|
|
- 清除恶意代码
|
|||
|
|
- 修复安全漏洞
|
|||
|
|
- 恢复正常服务
|
|||
|
|
|
|||
|
|
4. **总结阶段**
|
|||
|
|
- 事件分析报告
|
|||
|
|
- 改进安全措施
|
|||
|
|
- 更新应急预案
|
|||
|
|
|
|||
|
|
## 10. 总结
|
|||
|
|
|
|||
|
|
### 10.1 安全管理要点
|
|||
|
|
|
|||
|
|
1. **全面防护**:从网络、系统、应用、数据多个层面构建安全防护体系
|
|||
|
|
2. **持续监控**:建立7×24小时安全监控和告警机制
|
|||
|
|
3. **快速响应**:制定完善的安全事件响应流程和应急预案
|
|||
|
|
4. **定期评估**:定期进行安全评估和渗透测试
|
|||
|
|
5. **人员培训**:提高全员安全意识和技能水平
|
|||
|
|
|
|||
|
|
### 10.2 安全发展规划
|
|||
|
|
|
|||
|
|
1. **短期目标**(1-3个月)
|
|||
|
|
- 完善基础安全防护
|
|||
|
|
- 建立安全监控体系
|
|||
|
|
- 制定安全管理制度
|
|||
|
|
|
|||
|
|
2. **中期目标**(3-6个月)
|
|||
|
|
- 实施零信任架构
|
|||
|
|
- 建立安全运营中心
|
|||
|
|
- 完善应急响应能力
|
|||
|
|
|
|||
|
|
3. **长期目标**(6-12个月)
|
|||
|
|
- 通过等保2.0认证
|
|||
|
|
- 建立安全文化
|
|||
|
|
- 实现自动化安全运营
|
|||
|
|
|
|||
|
|
### 10.3 联系方式
|
|||
|
|
|
|||
|
|
- **安全团队邮箱**:security@xlxumu.com
|
|||
|
|
- **应急响应热线**:400-XXX-XXXX
|
|||
|
|
- **安全事件报告**:incident@xlxumu.com
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
*本文档将根据安全威胁变化和业务发展需要持续更新*
|