在Node.js中实现类似PHP FastCGI的多租户虚拟空间需要考虑进程隔离、文件系统隔离和资源限制。以下是完整的实现方案:
1. 主控制器(Master Process)
// master.js
const cluster = require('cluster');
const fs = require('fs');
const path = require('path');
const http = require('http');
const { Worker } = require('worker_threads');
class VirtualHostManager {
constructor() {
this.tenants = new Map();
this.workers = new Map();
}
// 注册租户
registerTenant(tenantId, config) {
this.tenants.set(tenantId, {
id: tenantId,
rootDir: config.rootDir,
uid: config.uid,
gid: config.gid,
maxMemory: config.maxMemory || 128 * 1024 * 1024, // 128MB
maxCpu: config.maxCpu || 0.5,
domain: config.domain,
port: config.port
});
}
// 为每个租户创建隔离的工作进程
spawnTenantWorker(tenantId) {
const tenant = this.tenants.get(tenantId);
if (!tenant) return;
if (cluster.isMaster) {
const worker = cluster.fork({
TENANT_ID: tenantId,
TENANT_ROOT: tenant.rootDir,
TENANT_UID: tenant.uid,
TENANT_GID: tenant.gid,
NODE_OPTIONS: `--max-old-space-size=${Math.floor(tenant.maxMemory / 1024 / 1024)}`
});
this.workers.set(tenantId, worker);
worker.on('exit', (code, signal) => {
console.log(`Tenant ${tenantId} worker died, restarting...`);
this.spawnTenantWorker(tenantId);
});
}
}
} 2. 沙箱环境(Sandbox)
// sandbox.js
const vm = require('vm');
const fs = require('fs');
const path = require('path');
const { promisify } = require('util');
class TenantSandbox {
constructor(tenantConfig) {
this.tenantId = tenantConfig.id;
this.rootDir = tenantConfig.rootDir;
this.uid = tenantConfig.uid;
this.gid = tenantConfig.gid;
this.context = this.createContext();
}
// 创建隔离的执行上下文
createContext() {
const sandbox = {
console: console,
setTimeout: setTimeout,
setInterval: setInterval,
clearTimeout: clearTimeout,
clearInterval: clearInterval,
Buffer: Buffer,
process: {
env: {},
version: process.version,
platform: process.platform,
arch: process.arch,
cwd: () => this.rootDir,
memoryUsage: process.memoryUsage
},
require: this.createSafeRequire(),
__dirname: this.rootDir,
__filename: ''
};
// 添加安全的文件系统API
sandbox.fs = this.createSafeFS();
return vm.createContext(sandbox);
}
// 创建受限的require函数
createSafeRequire() {
const allowedModules = ['http', 'https', 'url', 'querystring', 'crypto'];
return (moduleName) => {
// 只允许加载白名单模块
if (allowedModules.includes(moduleName)) {
return require(moduleName);
}
// 检查是否是相对路径模块
if (moduleName.startsWith('./') || moduleName.startsWith('../')) {
const modulePath = path.resolve(this.rootDir, moduleName);
// 确保模块在租户目录内
if (!modulePath.startsWith(this.rootDir)) {
throw new Error(`Access denied: ${moduleName}`);
}
return require(modulePath);
}
throw new Error(`Module not allowed: ${moduleName}`);
};
}
// 创建受限的文件系统API
createSafeFS() {
const safeFS = {};
const self = this;
// 包装fs方法,限制在租户目录内
const wrapFSMethod = (method) => {
return (...args) => {
// 检查路径参数
if (args[0] && typeof args[0] === 'string') {
const resolvedPath = path.resolve(self.rootDir, args[0]);
// 确保路径在租户目录内
if (!resolvedPath.startsWith(self.rootDir)) {
throw new Error(`Access denied: ${args[0]}`);
}
args[0] = resolvedPath;
}
return fs[method](...args);
};
};
// 包装常用的fs方法
['readFile', 'writeFile', 'readdir', 'stat', 'mkdir', 'rmdir', 'unlink']
.forEach(method => {
safeFS[method] = wrapFSMethod(method);
safeFS[method + 'Sync'] = wrapFSMethod(method + 'Sync');
});
return safeFS;
}
// 执行租户代码
async execute(code, filename = 'index.js') {
try {
// 设置进程用户和组(需要root权限)
if (process.platform !== 'win32' && process.getuid() === 0) {
process.setgid(this.gid);
process.setuid(this.uid);
}
const script = new vm.Script(code, {
filename: path.join(this.rootDir, filename),
timeout: 30000, // 30秒超时
displayErrors: true
});
return script.runInContext(this.context);
} catch (error) {
console.error(`Sandbox execution error for tenant ${this.tenantId}:`, error);
throw error;
}
}
}
module.exports = TenantSandbox; 3. 工作进程(Worker Process)
// worker.js
const http = require('http');
const express = require('express');
const TenantSandbox = require('./sandbox');
if (!cluster.isMaster) {
const tenantId = process.env.TENANT_ID;
const tenantRoot = process.env.TENANT_ROOT;
const tenantUid = parseInt(process.env.TENANT_UID);
const tenantGid = parseInt(process.env.TENANT_GID);
// 创建租户沙箱
const sandbox = new TenantSandbox({
id: tenantId,
rootDir: tenantRoot,
uid: tenantUid,
gid: tenantGid
});
// 创建Express应用
const app = express();
// 中间件:检查请求路径
app.use((req, res, next) => {
// 确保请求路径在租户目录内
const requestPath = path.join(tenantRoot, req.path);
if (!requestPath.startsWith(tenantRoot)) {
return res.status(403).send('Access Denied');
}
next();
});
// 处理静态文件
app.use(express.static(tenantRoot, {
dotfiles: 'deny',
index: ['index.js', 'index.html']
}));
// 处理Node.js脚本执行
app.get('*.js', async (req, res) => {
try {
const scriptPath = path.join(tenantRoot, req.path);
// 检查文件是否存在
if (!fs.existsSync(scriptPath)) {
return res.status(404).send('Not Found');
}
// 读取并执行脚本
const code = fs.readFileSync(scriptPath, 'utf8');
// 在沙箱中执行
const result = await sandbox.execute(code, req.path);
res.send(result);
} catch (error) {
res.status(500).send(`Error: ${error.message}`);
}
});
// 启动服务器
const port = process.env.PORT || 3000;
app.listen(port, () => {
console.log(`Tenant ${tenantId} worker listening on port ${port}`);
});
} 4. 使用Docker实现更强的隔离
// docker-sandbox.js
const Docker = require('dockerode');
const docker = new Docker();
class DockerSandbox {
constructor(tenantConfig) {
this.tenantId = tenantConfig.id;
this.config = tenantConfig;
this.container = null;
}
async createContainer() {
// 创建Docker容器
this.container = await docker.createContainer({
Image: 'node:14-alpine',
name: `tenant-${this.tenantId}`,
Hostname: `tenant-${this.tenantId}`,
// 资源限制
HostConfig: {
Memory: this.config.maxMemory,
CpuShares: Math.floor(this.config.maxCpu * 1024),
// 绑定挂载租户目录
Binds: [
`${this.config.rootDir}:/app:rw`
],
// 网络隔离
NetworkMode: 'bridge',
// 只读根文件系统
ReadonlyRootfs: false,
// 安全选项
SecurityOpt: ['no-new-privileges'],
CapDrop: ['ALL'],
CapAdd: ['NET_BIND_SERVICE']
},
// 环境变量
Env: [
`TENANT_ID=${this.tenantId}`,
`NODE_ENV=production`
],
// 工作目录
WorkingDir: '/app',
// 用户
User: `${this.config.uid}:${this.config.gid}`,
// 命令
Cmd: ['node', '/app/index.js']
});
await this.container.start();
}
async executeCode(code) {
// 在容器中执行代码
const exec = await this.container.exec({
Cmd: ['node', '-e', code],
AttachStdout: true,
AttachStderr: true,
WorkingDir: '/app'
});
const stream = await exec.start();
return new Promise((resolve, reject) => {
let output = '';
stream.on('data', (chunk) => {
output += chunk.toString();
});
stream.on('end', () => {
resolve(output);
});
stream.on('error', reject);
});
}
async stop() {
if (this.container) {
await this.container.stop();
await this.container.remove();
}
}
} 5. 路由分发器
// dispatcher.js
const httpProxy = require('http-proxy');
const proxy = httpProxy.createProxyServer({});
class RequestDispatcher {
constructor(virtualHostManager) {
this.vhm = virtualHostManager;
}
createServer() {
return http.createServer((req, res) => {
// 根据域名获取租户
const host = req.headers.host;
const tenant = this.findTenantByHost(host);
if (!tenant) {
res.writeHead(404);
res.end('Tenant not found');
return;
}
// 检查并启动租户工作进程
if (!this.vhm.workers.has(tenant.id)) {
this.vhm.spawnTenantWorker(tenant.id);
}
// 代理请求到租户工作进程
proxy.web(req, res, {
target: `http://localhost:${tenant.port}`
});
});
}
findTenantByHost(host) {
for (const [id, tenant] of this.vhm.tenants) {
if (tenant.domain === host) {
return tenant;
}
}
return null;
}
} 6. 配置示例
// config.js
module.exports = {
tenants: [
{
id: 'tenant1',
domain: 'tenant1.example.com',
rootDir: '/var/www/tenants/tenant1',
uid: 1001,
gid: 1001,
maxMemory: 134217728, // 128MB
maxCpu: 0.5,
port: 3001
},
{
id: 'tenant2',
domain: 'tenant2.example.com',
rootDir: '/var/www/tenants/tenant2',
uid: 1002,
gid: 1002,
maxMemory: 268435456, // 256MB
maxCpu: 1.0,
port: 3002
}
]
}; 7. 主程序
// main.js
const VirtualHostManager = require('./master');
const RequestDispatcher = require('./dispatcher');
const config = require('./config');
const vhm = new VirtualHostManager();
// 注册所有租户
config.tenants.forEach(tenant => {
vhm.registerTenant(tenant.id, tenant);
vhm.spawnTenantWorker(tenant.id);
});
// 创建分发服务器
const dispatcher = new RequestDispatcher(vhm);
const server = dispatcher.createServer();
server.listen(80, () => {
console.log('Multi-tenant server running on port 80');
}); 这个方案提供了:
进程隔离:每个租户运行在独立的进程中
文件系统隔离:限制访问范围在租户目录内
资源限制:CPU和内存限制
安全沙箱:VM模块提供代码执行隔离
Docker支持:可选的容器级隔离
网友回复
如何用python实现一个公网代理访问软件?
如何用go实现一个公网代理访问软件?
如何用python实现一个内网穿透打洞程序,实现内网的80端口暴露到公网上可以访问?
如何用go实现一个内网穿透打洞程序,实现内网的80端口暴露到公网上可以访问?
何为Shadowsocks 代理?
python如何实现类似php的opendir目录相互隔离的fastcgi多租户虚拟空间?
nodejs如何实现类似php的opendir目录相互隔离的fastcgi多租户虚拟空间?
如何用html写出网页滚动视频播放卡片视觉差异效果的代码?
程序员如何低成本搭建代理进行科学上网学习技术?
threejs如何做个三维搭积木的游戏?


