mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-09-24 16:05:44 +08:00
feat(payment): support wxpay legacy platform-certificate verifier mode
之前的改动只保留了微信支付"新公钥验签"路径,对仍在用平台证书的老商户不可用。 - wxpay.go: publicKeyId 从必填移除;buildVerifier 根据是否配置 publicKeyId 自动切换: - 有 → 新公钥方式(NewSHA256WithRSAPubkeyVerifier + 配置的 publicKey) - 无 → 平台证书方式(downloader 自动下载平台证书 + NewSHA256WithRSAVerifier) publicKey/publicKeyId 必须成对提供,单独一个视为配置错误 - providerConfig.ts: publicKey/publicKeyId 标记为 optional,certSerial 保留必填 - wxpay_test.go: 补 legacy 模式成功用例和成对校验错误用例
This commit is contained in:
@@ -14,7 +14,9 @@ import (
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/payment"
|
||||
"github.com/wechatpay-apiv3/wechatpay-go/core"
|
||||
"github.com/wechatpay-apiv3/wechatpay-go/core/auth"
|
||||
"github.com/wechatpay-apiv3/wechatpay-go/core/auth/verifiers"
|
||||
"github.com/wechatpay-apiv3/wechatpay-go/core/downloader"
|
||||
"github.com/wechatpay-apiv3/wechatpay-go/core/notify"
|
||||
"github.com/wechatpay-apiv3/wechatpay-go/core/option"
|
||||
"github.com/wechatpay-apiv3/wechatpay-go/services/payments"
|
||||
@@ -57,7 +59,7 @@ type Wxpay struct {
|
||||
}
|
||||
|
||||
func NewWxpay(instanceID string, config map[string]string) (*Wxpay, error) {
|
||||
required := []string{"appId", "mchId", "privateKey", "apiV3Key", "publicKey", "publicKeyId", "certSerial"}
|
||||
required := []string{"appId", "mchId", "privateKey", "apiV3Key", "certSerial"}
|
||||
for _, k := range required {
|
||||
if config[k] == "" {
|
||||
return nil, fmt.Errorf("wxpay config missing required key: %s", k)
|
||||
@@ -66,6 +68,13 @@ func NewWxpay(instanceID string, config map[string]string) (*Wxpay, error) {
|
||||
if len(config["apiV3Key"]) != 32 {
|
||||
return nil, fmt.Errorf("wxpay apiV3Key must be exactly 32 bytes, got %d", len(config["apiV3Key"]))
|
||||
}
|
||||
// publicKey + publicKeyId are a pair used by the new pubkey verifier.
|
||||
// If either is set, both must be set; otherwise fall back to legacy platform certificate mode.
|
||||
hasPubKey := config["publicKey"] != ""
|
||||
hasPubKeyID := config["publicKeyId"] != ""
|
||||
if hasPubKey != hasPubKeyID {
|
||||
return nil, fmt.Errorf("wxpay publicKey and publicKeyId must be provided together")
|
||||
}
|
||||
return &Wxpay{instanceID: instanceID, config: config}, nil
|
||||
}
|
||||
|
||||
@@ -89,14 +98,16 @@ func (w *Wxpay) ensureClient() (*core.Client, error) {
|
||||
if w.coreClient != nil {
|
||||
return w.coreClient, nil
|
||||
}
|
||||
privateKey, publicKey, err := w.loadKeyPair()
|
||||
privateKey, err := utils.LoadPrivateKey(formatPEM(w.config["privateKey"], "PRIVATE KEY"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("wxpay load private key: %w", err)
|
||||
}
|
||||
verifier, err := w.buildVerifier(privateKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
certSerial := w.config["certSerial"]
|
||||
verifier := verifiers.NewSHA256WithRSAPubkeyVerifier(w.config["publicKeyId"], *publicKey)
|
||||
client, err := core.NewClient(context.Background(),
|
||||
option.WithMerchantCredential(w.config["mchId"], certSerial, privateKey),
|
||||
option.WithMerchantCredential(w.config["mchId"], w.config["certSerial"], privateKey),
|
||||
option.WithVerifier(verifier))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("wxpay init client: %w", err)
|
||||
@@ -110,16 +121,23 @@ func (w *Wxpay) ensureClient() (*core.Client, error) {
|
||||
return w.coreClient, nil
|
||||
}
|
||||
|
||||
func (w *Wxpay) loadKeyPair() (*rsa.PrivateKey, *rsa.PublicKey, error) {
|
||||
privateKey, err := utils.LoadPrivateKey(formatPEM(w.config["privateKey"], "PRIVATE KEY"))
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("wxpay load private key: %w", err)
|
||||
// buildVerifier picks the verifier mode based on whether publicKeyId is configured:
|
||||
// - publicKeyId set → new pubkey verifier using the configured publicKey
|
||||
// - publicKeyId empty → legacy mode: auto-download platform certs with apiV3Key
|
||||
func (w *Wxpay) buildVerifier(privateKey *rsa.PrivateKey) (auth.Verifier, error) {
|
||||
if w.config["publicKeyId"] != "" {
|
||||
publicKey, err := utils.LoadPublicKey(formatPEM(w.config["publicKey"], "PUBLIC KEY"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("wxpay load public key: %w", err)
|
||||
}
|
||||
return verifiers.NewSHA256WithRSAPubkeyVerifier(w.config["publicKeyId"], *publicKey), nil
|
||||
}
|
||||
publicKey, err := utils.LoadPublicKey(formatPEM(w.config["publicKey"], "PUBLIC KEY"))
|
||||
mgr := downloader.MgrInstance()
|
||||
err := mgr.RegisterDownloaderWithPrivateKey(context.Background(), privateKey, w.config["certSerial"], w.config["mchId"], w.config["apiV3Key"])
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("wxpay load public key: %w", err)
|
||||
return nil, fmt.Errorf("wxpay register platform cert downloader: %w", err)
|
||||
}
|
||||
return privateKey, publicKey, nil
|
||||
return verifiers.NewSHA256WithRSAVerifier(mgr.GetCertificateVisitor(w.config["mchId"])), nil
|
||||
}
|
||||
|
||||
func (w *Wxpay) CreatePayment(ctx context.Context, req payment.CreatePaymentRequest) (*payment.CreatePaymentResponse, error) {
|
||||
|
||||
@@ -207,16 +207,27 @@ func TestNewWxpay(t *testing.T) {
|
||||
errSubstr: "apiV3Key",
|
||||
},
|
||||
{
|
||||
name: "missing publicKey",
|
||||
config: withOverride(map[string]string{"publicKey": ""}),
|
||||
name: "missing certSerial",
|
||||
config: withOverride(map[string]string{"certSerial": ""}),
|
||||
wantErr: true,
|
||||
errSubstr: "publicKey",
|
||||
errSubstr: "certSerial",
|
||||
},
|
||||
{
|
||||
name: "missing publicKeyId",
|
||||
name: "legacy mode: publicKey+publicKeyId both empty",
|
||||
config: withOverride(map[string]string{"publicKey": "", "publicKeyId": ""}),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "publicKey without publicKeyId",
|
||||
config: withOverride(map[string]string{"publicKeyId": ""}),
|
||||
wantErr: true,
|
||||
errSubstr: "publicKeyId",
|
||||
errSubstr: "must be provided together",
|
||||
},
|
||||
{
|
||||
name: "publicKeyId without publicKey",
|
||||
config: withOverride(map[string]string{"publicKey": ""}),
|
||||
wantErr: true,
|
||||
errSubstr: "must be provided together",
|
||||
},
|
||||
{
|
||||
name: "apiV3Key too short",
|
||||
|
||||
@@ -99,9 +99,9 @@ export const PROVIDER_CONFIG_FIELDS: Record<string, ConfigFieldDef[]> = {
|
||||
{ key: 'mchId', label: '', sensitive: false },
|
||||
{ key: 'privateKey', label: '', sensitive: true },
|
||||
{ key: 'apiV3Key', label: '', sensitive: true },
|
||||
{ key: 'publicKey', label: '', sensitive: true },
|
||||
{ key: 'publicKeyId', label: '', sensitive: false },
|
||||
{ key: 'certSerial', label: '', sensitive: false },
|
||||
{ key: 'publicKey', label: '', sensitive: true, optional: true },
|
||||
{ key: 'publicKeyId', label: '', sensitive: false, optional: true },
|
||||
],
|
||||
stripe: [
|
||||
{ key: 'secretKey', label: '', sensitive: true },
|
||||
|
||||
Reference in New Issue
Block a user