本文目录导读:

在PHP项目中实现用户自定义JavaScript功能,需要仔细考虑安全性、执行环境和使用场景,以下是几种实现方案及其对应的安全策略:
前端代码编辑器 + 后端存储(最常用)
实现流程
用户编辑JS → 提交到PHP后端 → 验证和过滤 → 存入数据库 → 页面加载时输出
PHP后端代码示例
// 保存用户自定义JS
function saveUserJs($userId, $jsCode) {
// 1. 安全过滤(移除危险代码)
$cleanJs = cleanJavaScript($jsCode);
// 2. 可选:语法检查
// 需要安装 Node.js 环境,通过 exec 调用
$validation = validateJavaScript($cleanJs);
if (!$validation['valid']) {
return ['error' => $validation['message']];
}
// 3. 存入数据库
$db->query("UPDATE users SET custom_js = ? WHERE id = ?", [$cleanJs, $userId]);
return ['success' => true];
}
// 前端页面加载时输出
function getUserJs($userId) {
$jsCode = $db->query("SELECT custom_js FROM users WHERE id = ?", [$userId])->fetchColumn();
if ($jsCode) {
echo "<script>{$jsCode}</script>";
}
}
前端编辑器HTML
<textarea id="jsEditor" name="customJs"></textarea>
<script>
// 使用 CodeMirror 或 Monaco Editor 提供语法高亮
const editor = CodeMirror.fromTextArea(document.getElementById('jsEditor'), {
mode: 'javascript',
lineNumbers: true,
theme: 'monokai'
});
</script>
沙箱执行(高级安全需求)
如果需要实际执行用户JS(如运行用户脚本),必须使用沙箱:
使用 Node.js 沙箱(推荐)
// PHP调用Node.js沙箱执行
function executeUserJsInSandbox($jsCode, $timeout = 5) {
$sandboxScript = <<<JS
const vm = require('vm');
const sandbox = {
console: { log: () => {} }, // 限制console
setTimeout: null, // 禁用定时器
XMLHttpRequest: null, // 禁用网络请求
require: null // 禁用模块加载
};
try {
const context = vm.createContext(sandbox);
vm.runInContext(${jsonEncode($jsCode)}, context, {
timeout: ${$timeout * 1000},
filename: 'user-script-${uniqid()}.js'
});
echo json_encode(['success' => true]);
} catch (e) {
echo json_encode(['error' => e.message]);
}
JS;
$result = exec("node -e " . escapeshellarg($sandboxScript));
return json_decode($result, true);
}
浏览器端沙箱(IFrame + postMessage)
<!-- 主页面 -->
<iframe id="sandbox" src="/sandbox.html" sandbox="allow-scripts"></iframe>
<script>
document.getElementById('sandbox').contentWindow.postMessage({
type: 'execute_js',
code: userJsCode
}, '*');
</script>
<!-- sandbox.html(同源iframe,受sandbox属性保护) -->
<script>
window.addEventListener('message', function(event) {
if (event.data.type === 'execute_js') {
try {
eval(event.data.code); // 运行在受限的iframe中
} catch(e) {
console.error('User JS error:', e);
}
}
});
</script>
工具型JS插件系统
定义明确的API接口
// 用户只能使用预定义的函数
$allowedFunctions = [
'alert(message)',
'getCurrentUser()',
'logEvent(eventName)'
];
// PHP端提供这些服务的实现
function getCurrentUser() {
return json_encode($_SESSION['user']);
}
用户编辑区只允许调用API
// 用户只能这样写:
function afterPageLoad() {
alert('欢迎!');
console.log(getCurrentUser().name);
}
安全策略(必须实施)
输入过滤与清理
function cleanJavaScript($js) {
// 移除危险模式
$dangerous = [
'/document\.write/i',
'/new\s+Function/i',
'/eval\s*\(/i',
'/setTimeout\s*\(/i',
'/setInterval\s*\(/i',
'/XMLHttpRequest/i',
'/fetch\s*\(/i',
'/window\./i',
'/document\.cookie/i',
'/localStorage/i',
'/sessionStorage/i'
];
foreach ($dangerous as $pattern) {
if (preg_match($pattern, $js)) {
throw new Exception('包含危险代码');
}
}
return $js;
}
内容安全策略(CSP)
在HTTP响应头中设置:
header("Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline'");
速率限制
// 限制用户每日提交次数
if ($redis->get("js_submit_limit:{$userId}") >= 10) {
die('超出每日提交限制');
}
$redis->incr("js_submit_limit:{$userId}");
$redis->expire("js_submit_limit:{$userId}", 86400);
完整实现示例(简单场景)
数据库表结构
CREATE TABLE user_custom_js (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL,
js_code TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY (user_id)
);
控制器逻辑
// routes/web.php
Route::post('/save-custom-js', function() {
$userId = auth()->id();
$jsCode = request()->input('js_code');
// 安全过滤
$cleanJs = cleanJavaScript($jsCode);
// 保存到数据库
UserCustomJs::updateOrCreate(
['user_id' => $userId],
['js_code' => $cleanJs]
);
return response()->json(['success' => true]);
});
// 在页面模板中输出
<script>
$(document).ready(function() {
<?= $user->customJs ?? '' ?>
});
</script>
最佳实践建议
- 从小处着手:开始时只允许用户自定义CSS或简单的配置项
- 明确告知权限:让用户知道哪些API可用,哪些被限制
- 提供模板:给用户提供一些安全的模板开始
- 持续监控:记录所有执行错误和安全事件
- 版本控制:保存历史版本,以便回滚
警告
- 永远不要在生产环境直接使用
eval()执行用户代码 - 必须使用沙箱隔离,除非代码只在前端执行(如主题定制)
- 限制网络请求、DOM操作、数据读取等关键功能
- 考虑使用成熟方案如 Google Caja 或 JS-Interpreter
选择哪种方案取决于你的具体需求:如果只是允许用户修改页面行为,方案一足够;如果需要执行用户提交的脚本自动化任务,必须使用方案二的沙箱。