安全(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(市场 / 开发者中心 / 审核 / 分润)
106 lines
6.1 KiB
PHP
106 lines
6.1 KiB
PHP
<?php
|
||
// +----------------------------------------------------------------------
|
||
// | 支付中心插件静态配置(随插件分发,env 驱动)
|
||
// +----------------------------------------------------------------------
|
||
// 与表单 config.php 分工:
|
||
// config.php → 后台可编辑的「开关 / 默认场景」
|
||
// config/paycenter.php → 部署相关的「商户密钥 / 证书」,只从 .env 读,绝不写数据库
|
||
//
|
||
// 若核心 config/pay.php 已存在且非空的同名项,由 PayConfig 用核心值覆盖(兼容既有部署)。
|
||
|
||
// 应用私钥兼容处理:
|
||
// .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 正确的一侧。
|
||
$normalizePrivateKey = static function (?string $secret): string {
|
||
$value = trim((string) $secret);
|
||
if ($value === '') {
|
||
return '';
|
||
}
|
||
if (is_file($value)) {
|
||
$value = (string) file_get_contents($value);
|
||
}
|
||
$body = (string) preg_replace('/-----[^-]+-----|\s+/', '', $value);
|
||
if ($body === '') {
|
||
return '';
|
||
}
|
||
$der = base64_decode($body, 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");
|
||
|
||
return $isPkcs8
|
||
? "-----BEGIN PRIVATE KEY-----\n" . wordwrap($body, 64, "\n", true) . "\n-----END PRIVATE KEY-----"
|
||
: $body;
|
||
};
|
||
|
||
// 证书路径统一按项目根目录解析成绝对路径:
|
||
// SDK 的 get_public_cert() 是 `is_file($key) ? file_get_contents($key) : $key`,
|
||
// 相对路径按【进程 CWD】解析:CLI 在根目录能读到,Web 入口 CWD 是 public/ 就读不到,
|
||
// 下单会抛「配置异常: 解析 `app_public_cert_path` 失败」,回调验签同样失败。
|
||
$resolveCertPath = static function (?string $path): string {
|
||
$path = trim((string) $path);
|
||
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__, 3) . DIRECTORY_SEPARATOR;
|
||
|
||
return $root . ltrim($path, '/\\');
|
||
};
|
||
|
||
return [
|
||
// 模拟支付开关:仅在 APP_DEBUG=true 且无商户号时生效(判定在业务侧,这里只做透传)
|
||
'mock_enable' => env('PAY_MOCK', false),
|
||
|
||
// 支付 SDK 日志开关。
|
||
// 不要跟 APP_DEBUG 绑定:生产一旦误开 debug,会把明文请求/签名细节写进 runtime/log/pay.log。
|
||
// 需要排查时显式 PAY_LOG=true 打开,查完务必关掉。
|
||
'log_enable' => (bool) env('PAY_LOG', false),
|
||
'log_level' => (string) env('PAY_LOG_LEVEL', 'debug'),
|
||
|
||
// 支付宝(Yansongda Pay v3 —— 仅支持「证书模式」,普通公钥模式不再被识别)
|
||
'alipay' => [
|
||
// mode 必须是【整数】:0=正式 1=沙箱 2=服务商(Yansongda\Pay\Pay::MODE_*)
|
||
'mode' => (int) env('PAY_ALIPAY_MODE', 0),
|
||
'app_id' => env('PAY_ALIPAY_APP_ID', ''),
|
||
'app_secret_cert' => $normalizePrivateKey((string) env('PAY_ALIPAY_APP_SECRET_CERT', '')),
|
||
'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)。注:微信早已下线官方沙箱,mode 留 0 即可。
|
||
'wechat' => [
|
||
'mode' => (int) env('PAY_WECHAT_MODE', 0),
|
||
'mch_id' => env('PAY_WECHAT_MCH_ID', ''),
|
||
// 直连模式填公众号/小程序/移动应用的 appid。
|
||
// SDK 按场景取不同的键(见 get_wechat_type_key):mp → mp_app_id、mini → mini_app_id、app → app_id,
|
||
// 未单独配置时统一回落到 PAY_WECHAT_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 密钥(微信商户平台 → API 安全 → 设置 APIv3 密钥)
|
||
'mch_secret_key' => env('PAY_WECHAT_SECRET_KEY', ''),
|
||
// 商户 API 私钥:PEM 全文、单行 base64(上方已自动识别 PKCS8/PKCS1)或文件路径
|
||
'mch_secret_cert' => $normalizePrivateKey((string) env('PAY_WECHAT_SECRET_CERT', '')),
|
||
// 商户 API 证书(apiclient_cert.pem):用于提取证书序列号
|
||
'mch_public_cert_path' => $resolveCertPath((string) env('PAY_WECHAT_PUBLIC_CERT_PATH', '')),
|
||
// 微信平台证书(回调验签用):留空时 SDK 会凭 APIv3 密钥自动下载并缓存在内存,
|
||
// 生产环境建议用 `php think paycenter:check --channel=wechat` 触发一次后把证书落到
|
||
// runtime/cert/wechat/ 并在此配置 [序列号 => 文件路径],避免每次请求都去拉取。
|
||
'wechat_public_cert_path' => [],
|
||
'return_url' => '',
|
||
'notify_url' => '',
|
||
],
|
||
];
|