Files
YwxAppThink/scripts/package_keygen.php
T
yangwx 18101cf11a feat: 上线前安全加固与发布工程化;appmall 由插件迁移为独立应用
安全(P0)
- 发布包签名:新增 scripts/package_keygen.php、scripts/package_verify.php、
  ywxapp\service\PackageVerifier;FrameworkService::apply() 解压前强制验签
  (ywxapp.framework_package_verify,默认开启)
- JWT 收紧:config/jwt.php 移除公开默认密钥,JwtService::assertStrongSecret()
  校验非空 / 长度≥32 / 已知弱值;.env.production.example 键名由 JWT_SECRET_KEY 修正为 JWT_SECRET
- /api/v1/Article:免登录白名单收窄为只读接口,写操作字段白名单 + assertOwner() 越权校验
- 开发者令牌:查找接口不再回显明文、统一失败文案 + 限流
- TemplateInstaller:文件名白名单 + realpath 归一化 + 目录内断言,修复 Zip Slip;补 use ZipArchive
- 后台登录:接入验证码、失败锁定与登录限流(config/ratelimit.php)
- 缓存 / Session 驱动可由 .env 覆盖(CACHE_DRIVER / SESSION_TYPE),集群部署改 redis
- paycenter 支付日志开关改为 PAY_LOG,不再跟随 APP_DEBUG

工程化 / 交付
- 新增 CHANGELOG.md 与 .env.production.example;composer.lock 入库锁定依赖版本
- 插件离线安装 CLI 增加 --uid,后台上传安装透传 uid,付费插件授权校验可通过
- 授权拒绝文案区分「未携带 uid」与「未找到有效授权」,便于排障
- .gitignore 放行 .env.production.example / .env.addon.example / .gitattributes / public/.htaccess

结构
- appmall 由 addon/appmall 迁移为独立应用 app/appmall(市场 / 开发者中心 / 审核 / 分润)
2026-10-22 00:53:39 +08:00

115 lines
4.5 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* 生成「发布包签名」密钥对(一次)
*
* 产出:
* - 私钥 runtime/package_private.pem **仅限打包机,绝不入库/进包**(.gitignore 已忽略 /runtime)
* - 公钥 ywxapp/data/package_public.pem 随版本发布,客户机用它验签
* - 同时把公钥写回 ywxapp\service\PackageVerifier::BUILTIN_PUBLIC_KEY,
* 作为「公钥文件被误删/被篡改」时的兜底锚点(否则删掉文件就能绕过验签)。
*
* 用法:
* php scripts/package_keygen.php # 生成 4096 位密钥对(已存在则要求 --force)
* php scripts/package_keygen.php --force # 强制轮换(旧包将全部验签失败,务必先给客户推过渡版本)
* php scripts/package_keygen.php --bits=2048
*
* 轮换策略(重要):
* 直接换 key 会让所有客户机拒绝安装旧包。正确做法是先发布一个「同时内置新旧公钥」的过渡版本,
* 待覆盖率达标后再用新私钥发新版;本脚本的 --force 只用于「私钥已泄漏」应急。
*/
declare(strict_types=1);
require_once __DIR__ . DIRECTORY_SEPARATOR . 'lib' . DIRECTORY_SEPARATOR . 'package_signature.php';
$root = dirname(__DIR__) . DIRECTORY_SEPARATOR;
$bits = 4096;
$force = false;
foreach (array_slice($argv, 1) as $arg) {
if ($arg === '--force') {
$force = true;
} elseif (strpos($arg, '--bits=') === 0) {
$bits = (int) substr($arg, 7);
if ($bits < 2048) {
fwrite(STDERR, "密钥长度至少 2048 位\n");
exit(1);
}
}
}
$privFile = $root . 'runtime' . DIRECTORY_SEPARATOR . 'package_private.pem';
$pubFile = $root . 'ywxapp' . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_SEPARATOR . 'package_public.pem';
$builtin = $root . 'ywxapp' . DIRECTORY_SEPARATOR . 'service' . DIRECTORY_SEPARATOR . 'PackageVerifier.php';
if (is_file($privFile) && ! $force) {
fwrite(STDERR, "私钥已存在:{$privFile}\n如需轮换请加 --force(会让旧发布包全部验签失败,先确认发布策略)\n");
exit(1);
}
if (! extension_loaded('openssl')) {
fwrite(STDERR, "缺少 openssl 扩展\n");
exit(1);
}
$res = openssl_pkey_new([
'private_key_bits' => $bits,
'private_key_type' => OPENSSL_KEYTYPE_RSA,
]);
if ($res === false) {
fwrite(STDERR, "生成密钥失败:" . openssl_error_string() . "\n");
exit(1);
}
if (! openssl_pkey_export($res, $privatePem)) {
fwrite(STDERR, "导出私钥失败\n");
exit(1);
}
$details = openssl_pkey_get_details($res);
$publicPem = $details['key'] ?? '';
if ($publicPem === '') {
fwrite(STDERR, "导出公钥失败\n");
exit(1);
}
if (! is_dir(dirname($privFile))) {
mkdir(dirname($privFile), 0755, true);
}
if (! is_dir(dirname($pubFile))) {
mkdir(dirname($pubFile), 0755, true);
}
file_put_contents($privFile, $privatePem);
chmod($privFile, 0600);
file_put_contents($pubFile, $publicPem);
// 写回内置公钥常量:文件可被整包升级覆盖,常量是最后的信任锚
$builtinOk = false;
if (is_file($builtin)) {
$src = (string) file_get_contents($builtin);
// 用正则而不是字符串定位:Windows 下仓库可能检出 CRLF,
// 硬匹配 "\nPEM;\n" 会漏。$count 用于判断是否真的替换到了。
$new = preg_replace_callback(
"/(private const BUILTIN_PUBLIC_KEY\s*=\s*<<<'PEM'\R)(.*?)(\R\s*PEM;)/s",
static function (array $m) use ($publicPem): string {
$raw = trim($publicPem);
// PEM 里含 - 与字母数字,作为 replacement 无需转义,但统一按行尾惯例 CRLF/LF 保持不变
return $m[1] . $raw . $m[3];
},
$src,
1,
$count
);
if ($count > 0 && $new !== null) {
file_put_contents($builtin, $new);
$builtinOk = true;
}
}
echo "=== 发布包签名密钥已生成 ===\n";
echo "[私钥] {$privFile} (仅打包机保留,切勿入库/进包)\n";
echo "[公钥] {$pubFile} (随版本发布给客户机)\n";
echo "[内置] {$builtin} → " . ($builtinOk ? '已写入 BUILTIN_PUBLIC_KEY' : '写入失败,请手工粘贴公钥到该常量') . "\n";
echo "[位数] {$bits}\n\n";
echo "下一步:\n";
echo " 1) 备份私钥到离线介质(丢了无法签发新版本,只能换 key 并强制轮换)\n";
echo " 2) php scripts/package_framework.php 1.1.2 # 打包时会自动给三个 zip 追加签名\n";
echo " 3) php scripts/package_verify.php build/framework/ywxapp-1.1.2.zip 1.1.2 # 发布前自检\n";