如何用PHP项目实现用户自定义JS?

wen java案例 2

本文目录导读:

如何用PHP项目实现用户自定义JS?

  1. 方案一:前端代码编辑器 + 后端存储(最常用)
  2. 方案二:沙箱执行(高级安全需求)
  3. 方案三:工具型JS插件系统
  4. 安全策略(必须实施)
  5. 完整实现示例(简单场景)
  6. 最佳实践建议
  7. 警告

在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>

最佳实践建议

  1. 从小处着手:开始时只允许用户自定义CSS或简单的配置项
  2. 明确告知权限:让用户知道哪些API可用,哪些被限制
  3. 提供模板:给用户提供一些安全的模板开始
  4. 持续监控:记录所有执行错误和安全事件
  5. 版本控制:保存历史版本,以便回滚

警告

  • 永远不要在生产环境直接使用 eval() 执行用户代码
  • 必须使用沙箱隔离,除非代码只在前端执行(如主题定制)
  • 限制网络请求、DOM操作、数据读取等关键功能
  • 考虑使用成熟方案如 Google CajaJS-Interpreter

选择哪种方案取决于你的具体需求:如果只是允许用户修改页面行为,方案一足够;如果需要执行用户提交的脚本自动化任务,必须使用方案二的沙箱。

抱歉,评论功能暂时关闭!