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(市场 / 开发者中心 / 审核 / 分润)
This commit is contained in:
yangwx committed 2026-10-22 00:53:39 +08:00
1 parent 1d49e6f5ee
commit 18101cf11a
636 files changed
+58517 -9519

No files matched your search

+92 -19
View File
@@ -8,29 +8,102 @@
// | Author: ywxapp<admin@ywxapp.cn>
// +----------------------------------------------------------------------
return [
// 模拟支付开关:无商户号时直接完成下单并签发授权,便于本地/自托管环境跑通购买闭环。
// 生产环境务必在 .env 设置 PAY_MOCK=false 关闭,否则任何人可「免费」购得付费插件。
'mock_enable' => env('PAY_MOCK', true),
// 应用私钥兼容处理:
// .env 只能放单行,私钥通常以「裸 base64」贴进来(带 PEM 头尾的多行内容塞不进 .env)。
// SDK 的 get_private_cert()(vendor/yansongda/pay/src/Functions.php)规则很挑食:
// ① 是文件路径 → 读文件内容(内容仍要符合 ②③)
// ② 以 '-----BEGIN PRIVATE KEY-----' 开头 → 原样返回(必须是真正的 PKCS8)
// ③ 其余一律再包一层 '-----BEGIN RSA PRIVATE KEY-----'(按 PKCS1 处理)
// 由此得出:PKCS8 必须自己拼好 PKCS8 的 PEM 头;PKCS1 反而必须交【裸 base64】让 SDK 去包。
// 反过来做(给 PKCS1 套 'PRIVATE KEY' 头 / 给已是 PEM 的内容再套一层 RSA 头)就会:
// openssl_sign(): Supplied key param cannot be coerced into a private key
// 所以这里按 DER 里是否含 rsaEncryption 的 OID 判定 PKCS8/PKCS1,再交给 SDK 正确的一侧。
$alipaySecretCert = (string) env('PAY_ALIPAY_APP_SECRET_CERT', '');
if ($alipaySecretCert !== '') {
if (is_file($alipaySecretCert)) {
$alipaySecretCert = (string) file_get_contents($alipaySecretCert);
}
$privateKeyBody = (string) preg_replace('/-----[^-]+-----|\s+/', '', $alipaySecretCert);
$der = base64_decode($privateKeyBody, true) ?: '';
// rsaEncryption OID:1.2.840.113549.1.1.1 —— 只出现在 PKCS8 的结构里
$isPkcs8 = str_contains(substr($der, 0, 64), "\x06\x09\x2a\x86\x48\x86\xf7\x0d\x01\x01\x01");
$wrapped = wordwrap($privateKeyBody, 64, "\n", true);
$alipaySecretCert = $isPkcs8
? "-----BEGIN PRIVATE KEY-----\n{$wrapped}\n-----END PRIVATE KEY-----"
: $privateKeyBody;
}
// 支付宝
// 证书路径统一按项目根目录解析成绝对路径:
// SDK 的 get_public_cert() 是 `is_file($key) ? file_get_contents($key) : $key`
// (见 vendor/yansongda/pay/src/Functions.php),相对路径按【进程 CWD】解析:
// CLI 在项目根目录 → 能读到;Web 入口是 public/index.php、CWD 变成 public/ → 读不到,
// 下单会抛「配置异常: 解析 `app_public_cert_path` 失败」,回调验签同样失败。
// 所以在配置层补全一次,业务代码(MarketService / Market / PayCheck)都无需各自处理。
$resolveCertPath = static function (string $path): string {
if ($path === '') {
return '';
}
if (str_starts_with($path, '/') || str_starts_with($path, '\\') || preg_match('/^[A-Za-z]:[\\\\\/]/', $path)) {
return $path;
}
$root = function_exists('root_path') ? root_path() : dirname(__DIR__) . DIRECTORY_SEPARATOR;
return $root . ltrim($path, '/\\');
};
return [
// 模拟支付开关:仅在 APP_DEBUG=true 且无商户号时生效,便于本地/自托管环境跑通购买闭环。
// 默认 false(防御性默认,避免默认开启后被人"免费"购得付费插件授权)。
// 三重防御确保生产安全:
// 1) 本开关默认关闭(防止默认值泄露);
// 2) 安装向导 public/install/Installer.php::writeEnv 会强制把 PAY_MOCK=false 写入 .env;
// 3) 即使 .env 被手误改错,MarketService / Market 在生产(APP_DEBUG=false)也会拒绝执行 mock。
// 本地调试:.env 中显式设置 PAY_MOCK=true(与 APP_DEBUG=true 配对使用)。
'mock_enable' => env('PAY_MOCK', false),
// 支付宝(Yansongda Pay v3.7 —— 仅支持「证书模式」,普通公钥模式不再被识别)。
// 证书在开放平台 → 应用 → 接口加签方式(沙箱同理)下载:
// app_public_cert_path → appCertPublicKey_*.crt(应用公钥证书)
// alipay_public_cert_path → alipayCertPublicKey_*.crt(支付宝公钥证书,验签用)
// alipay_root_cert_path → alipayRootCert.crt(支付宝根证书)
// 缺任一项都会抛「配置异常: 缺少支付宝配置 -- [xxx]」,见
// vendor/yansongda/pay/src/Plugin/Alipay/V2/StartPlugin.php
// vendor/yansongda/pay/src/Plugin/Alipay/V2/AddPayloadSignaturePlugin.php
'alipay' => [
'mode' => env('PAY_ALIPAY_MODE', 'normal'), // normal | dev | query
'app_id' => env('PAY_ALIPAY_APP_ID', ''),
'ali_public_key' => env('PAY_ALIPAY_PUBLIC_KEY', ''),
'private_key' => env('PAY_ALIPAY_PRIVATE_KEY', ''),
'return_url' => '',
'notify_url' => '',
// mode 必须是【整数】:0=正式 1=沙箱 2=服务商(Yansongda\Pay\Pay::MODE_*)。
// 写成 'normal'/'dev' 字符串会在拼网关 URL 时数组越界。
'mode' => (int) env('PAY_ALIPAY_MODE', 0),
'app_id' => env('PAY_ALIPAY_APP_ID', ''),
// 应用私钥:PEM 全文、单行 base64(已在上方自动补全 PEM 头并识别 PKCS8/PKCS1),或私钥文件路径
'app_secret_cert' => $alipaySecretCert,
'app_public_cert_path' => $resolveCertPath((string) env('PAY_ALIPAY_APP_PUBLIC_CERT_PATH', '')),
'alipay_public_cert_path' => $resolveCertPath((string) env('PAY_ALIPAY_PUBLIC_CERT_PATH', '')),
'alipay_root_cert_path' => $resolveCertPath((string) env('PAY_ALIPAY_ROOT_CERT_PATH', '')),
'return_url' => '',
'notify_url' => '',
],
// 微信支付(如需,在 buy() 中按相同方式调用 Pay::wechat(...) 即可)
// 微信支付(v3 API)。注:微信早已下线官方沙箱,mode 留 0 即可;
// 没有商户号时只能走 mock 或小额真实支付再退款。
// 说明:真实的下单 / 验签 / 回调已迁到支付中心插件(addon/paycenter),
// 这里保留仅为「既有部署的密钥来源」——插件 PayConfig 会用下面的值覆盖自己的同名项。
'wechat' => [
'mch_id' => env('PAY_WECHAT_MCH_ID', ''),
'app_id' => env('PAY_WECHAT_APP_ID', ''),
'secret_key' => env('PAY_WECHAT_SECRET_KEY', ''),
'cert_client' => '',
'cert_key' => '',
'return_url' => '',
'notify_url' => '',
'mode' => (int) env('PAY_WECHAT_MODE', 0),
'mch_id' => env('PAY_WECHAT_MCH_ID', ''),
// SDK 按场景取键:mp → mp_app_id、mini → mini_app_id、app → app_id
'app_id' => env('PAY_WECHAT_APP_ID', ''),
'mp_app_id' => env('PAY_WECHAT_MP_APP_ID', env('PAY_WECHAT_APP_ID', '')),
'mini_app_id' => env('PAY_WECHAT_MINI_APP_ID', env('PAY_WECHAT_APP_ID', '')),
// APIv3 密钥
'mch_secret_key' => env('PAY_WECHAT_SECRET_KEY', ''),
// 商户 API 私钥:文件路径或 PEM 内容(插件会再做一次 PKCS8/PKCS1 判定)
'mch_secret_cert' => env('PAY_WECHAT_SECRET_CERT', ''),
// 商户 API 证书:用于提取证书序列号
'mch_public_cert_path' => env('PAY_WECHAT_PUBLIC_CERT_PATH', ''),
// 微信平台证书(回调验签用):留空时 SDK 凭 APIv3 密钥自动下载;
// 生产环境建议落到本地并配置 [证书序列号 => 文件路径],避免每次请求都去拉取
'wechat_public_cert_path' => [],
'return_url' => '',
'notify_url' => '',
],
];