插件是怎么被安全装进系统的
往生产系统里装第三方插件,本质上是在做一件危险的事:让别人写的代码和 SQL 在你的库上运行。装歪了轻则功能异常,重则数据被洗。rabshop 有一套完整的插件市场机制,代码在 app/common/service/plugin/ 下四十多个文件,这篇文章把它对「安全」的四个追问拆开讲:代码能不能信、过程崩了怎么办、并发安装怎么排队、装完事件怎么送达。每个追问对应一段真实源码。
第一问:这份安装包是谁发布的
从市场下载的插件制品(一个压缩包)先过验签,实现在 OpenSslPluginArtifactVerifier,类注释一句话定调:「通过 SHA-256 摘要和 ES256 JWS 验证制品,不接受无签名或明文回退」------没有签名的包直接出局,不存在「先装了再说」:
php
// app/common/service/plugin/OpenSslPluginArtifactVerifier.php(节选)
$actualHash = strtolower((string)hash_file('sha256', $result->path));
if ($actualHash === '' || !hash_equals($request->artifactSha256, $actualHash)) {
throw new PluginLifecycleException('制品摘要校验失败', 'ARTIFACT_HASH_MISMATCH');
}
// ...验签后还要比对载荷:
// artifact_sha256 必须等于实际摘要、plugin_code 和 version 必须与请求一致
摘要比对用 hash_equals 而不是 ==,防时序攻击;签名采用 ES256(椭圆曲线 JWS),解出的载荷要同时匹配制品摘要、插件编码、版本号三项------把「拿 A 插件的签名冒充 B 插件」「旧版本签名配新包」这些拼接攻击全部堵死。这三重绑定是整个信任链的地基。
第二问:两个人同时点安装怎么办
生命周期操作(安装、升级、禁用、卸载、回滚)统一走 runOperation,开头两步的顺序值得细看:
php
// app/common/service/plugin/PluginLifecycleService.php(runOperation 节选)
// 幂等查询优先于发布锁,重复请求不应因已有任务占锁而失败。
$existing = $this->repository->findTaskByIdempotency($request->idempotencyKey);
if ($existing !== null) {
$this->assertTaskMatches($existing, $operation, $request);
return $existing;
}
$lockHandle = $this->lock->acquire($request->pluginCode);
先查幂等键再拿锁:同一个安装请求重发两次,第二次直接返回已存在的任务,不会因为锁被第一次占着而报错。拿到锁之后还要再查一次幂等------并发请求可能在等锁期间创建了任务,双重检查防重复创建。锁本身用的是 MySQL 的 GET_LOCK,并且专门开了一条独立 PDO 连接持有它(锁的存活靠这条连接撑着,代码里按随机句柄管理这些连接);对同一插件的并发生命周期操作在这里排队,乱序操作则在更早的 assertCurrentState 被状态机拦下(比如未安装的插件不允许直接升级)。任务执行完 finally 释放锁,失败则走 handleFailure 把插件状态回滚到操作前快照。
第三问:装到一半崩了,库还剩什么
插件自带数据库迁移(建表、加字段),跑到一半断电是最麻烦的场景。系统的答案是迁移账本:执行器按「受审顺序」逐个执行迁移,每一步记进账本;下次安装或恢复时先对账:
text
// app/common/service/plugin/InstalledPluginLifecycleExecutor.php(错误信息摘录)
「迁移上次未完成,须先核对账本与实际结构」(MIGRATION_RECOVERY_REQUIRED)
「存在独立提交的迁移或卸载清理,保持禁用并按迁移账本恢复」
翻译过来:上次装了一半的插件,系统不会盲目重跑迁移,而是要求先核对账本和实际表结构再恢复------因为崩溃点之后的 DDL 可能已被别的事务独立提交,直接重跑会撞结构冲突。卸载时账本同样保留未完成迁移的记录,供下次安装核对。宁可多一步人工确认,不赌一次自动重放。
第四问:装完了,订阅方怎么知道
插件装好要广播事件(其他模块可能要在插件启用时初始化数据)。这里复用了订单篇讲过的 outbox 模式:生命周期事件随当前任务事务先落库,提交后由 PluginEventDispatchJob 队列异步派发;队列暂时不可用也不丢事件,扫描器会重新认领发件箱里的记录。事件发布失败的异常统一走异常路径,不留半成功状态。整个机制对外的样子,就是后台插件页里一键安装、进度条走完、各业务模块自动收到通知;项目内置的秒杀插件(plugins/seckill,带独立的迁移和测试)就是按这套协议落地的实例,想做自己的插件可以照着它抄结构。

四个追问连起来,就是一套插件系统的安全底线:签名保证「包没被换」,锁和状态机保证「操作不乱」,迁移账本保证「崩了可恢复」,outbox 保证「通知必达」。缺任何一环,插件市场都不敢真的开放给第三方。明天聊点轻松的,Java 里那些最容易考糊的关键字。
这套系统已开源,仓库地址:github.com/jinhangwang...
觉得写得有用,去给项目点个 Star 就是最大的支持~