安全(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(市场 / 开发者中心 / 审核 / 分润)
110 lines
6.9 KiB
PHP
110 lines
6.9 KiB
PHP
<?php
|
||
|
||
// +----------------------------------------------------------------------
|
||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||
// +----------------------------------------------------------------------
|
||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||
// +----------------------------------------------------------------------
|
||
// | Author: ywxapp<admin@ywxapp.cn>
|
||
// +----------------------------------------------------------------------
|
||
|
||
// 应用私钥兼容处理:
|
||
// .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 必须是【整数】: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' => '',
|
||
],
|
||
|
||
// 微信支付(v3 API)。注:微信早已下线官方沙箱,mode 留 0 即可;
|
||
// 没有商户号时只能走 mock 或小额真实支付再退款。
|
||
// 说明:真实的下单 / 验签 / 回调已迁到支付中心插件(addon/paycenter),
|
||
// 这里保留仅为「既有部署的密钥来源」——插件 PayConfig 会用下面的值覆盖自己的同名项。
|
||
'wechat' => [
|
||
'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' => '',
|
||
],
|
||
];
|