Files
YwxAppThink/config/pay.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

110 lines
6.9 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
// +----------------------------------------------------------------------
// | 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' => '',
],
];