Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

回调支持多种签名和加密类型 #125

Merged
merged 9 commits into from
Oct 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ jobs:
strategy:
matrix:
go:
- "1.19"
- "1.18"
- "1.17"
- "1.16"
- "1.15"
- "1.14"
Expand Down Expand Up @@ -51,6 +54,9 @@ jobs:
strategy:
matrix:
go:
- "1.19"
- "1.18"
- "1.17"
- "1.16"
- "1.15"
- "1.14"
Expand Down
154 changes: 88 additions & 66 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
## 功能介绍

1. 接口 SDK。详见 [接口介绍](services)。
1. HTTP 客户端 `core.Client`,支持请求签名和应答验签。如果 SDK 未支持你需要的接口,请用此客户端发起请求。
2. HTTP 客户端 `core.Client`,支持请求签名和应答验签。如果 SDK 未支持你需要的接口,请用此客户端发起请求。
3. 回调通知处理库 `core/notify`,支持微信支付回调通知的验签和解密。详见 [回调通知验签与解密](#回调通知的验签与解密)。
4. 证书下载、[敏感信息加解密](#敏感信息加解密) 等辅助能力。

Expand Down Expand Up @@ -217,71 +217,6 @@ if err != nil {
}
```

## 敏感信息加解密

为了保证通信过程中敏感信息字段(如用户的住址、银行卡号、手机号码等)的机密性,

+ 微信支付要求加密上送的敏感信息
+ 微信支付会加密下行的敏感信息

详见 [接口规则 - 敏感信息加解密](https://wechatpay-api.gitbook.io/wechatpay-api-v3/qian-ming-zhi-nan-1/min-gan-xin-xi-jia-mi)。

### 使用加解密算法工具包

使用工具包 [utils](utils) 中的函数,手动对敏感信息加解密。

```go
package utils

// EncryptOAEPWithPublicKey 使用公钥加密
func EncryptOAEPWithPublicKey(message string, publicKey *rsa.PublicKey) (ciphertext string, err error)
// EncryptOAEPWithCertificate 使用证书中的公钥加密
func EncryptOAEPWithCertificate(message string, certificate *x509.Certificate) (ciphertext string, err error)

// DecryptOAEP 使用私钥解密
func DecryptOAEP(ciphertext string, privateKey *rsa.PrivateKey) (message string, err error)
```

[rsa_crypto_test.go](utils/rsa_crypto_test.go) 中演示了如何使用以上函数做敏感信息加解密。

### 获取微信支付平台证书

请求的敏感信息,使用微信支付平台证书中的公钥加密。推荐 [使用平台证书下载管理器](FAQ.md#如何在更多地方使用平台证书下载管理器) 获取微信支付平台证书,或者 [下载平台证书](FAQ.md#如何下载微信支付平台证书)。

### 设置 `Wechatpay-Serial` 请求头

请求的敏感信息加密后,在 HTTP 请求头中添加微信支付平台证书序列号 `Wechatpay-Serial`。该序列号用于告知微信支付加密使用的证书。

使用 `core.Client` 的 `Request` 方法来传输自定义 HTTPHeader。

```go
// Request 向微信支付发送请求
//
// 相比于 Get / Post / Put / Patch / Delete 方法,本方法支持设置更多内容
// 特别地,如果需要为当前请求设置 Header,应使用本方法
func (client *Client) Request(
ctx context.Context,
method, requestPath string,
headerParams http.Header,
queryParams url.Values,
postBody interface{},
contentType string,
) (result *APIResult, err error)

// 示例代码
// 微信支付平台证书序列号,对应加密使用的私钥
header.Add("Wechatpay-Serial", "5157F09EFDC096DE15EBE81A47057A72*******")
result, err := client.Request(
ctx,
"POST",
"https://api.mch.weixin.qq.com/v3/profitsharing/receivers/add",
header,
nil,
body,
"application/json")

```

## 回调通知的验签与解密

1. 使用微信支付平台证书(验签)和商户 APIv3 密钥(解密)初始化 `notify.Handler`
Expand Down Expand Up @@ -368,6 +303,93 @@ if err != nil {
// 处理通知内容
fmt.Println(notifyReq.Summary)
fmt.Println(content)
```

## 敏感信息加解密

为了保证通信过程中敏感信息字段(如用户的住址、银行卡号、手机号码等)的机密性,

+ 微信支付要求加密上行的敏感信息
+ 微信支付会加密下行的敏感信息

详见 [接口规则 - 敏感信息加解密](https://wechatpay-api.gitbook.io/wechatpay-api-v3/qian-ming-zhi-nan-1/min-gan-xin-xi-jia-mi)。

### (推荐)使用敏感信息加解密器

敏感信息加解密器 `cipher.Cipher` 能根据 API 契约自动处理敏感信息:

+ 发起请求时,开发者设置原文,加密器自动加密敏感信息,并设置 `Wechatpay-Serial` 请求头
+ 收到应答时,解密器自动解密敏感信息,开发者得到原文

使用敏感信息加解密器,只需通过 `option.WithWechatPayCipher` 为 `core.Client` 添加加解密器:

```go
client, err := core.NewClient(
context.Background(),
// 一次性设置 签名/验签/敏感字段加解密,并注册 平台证书下载器,自动定时获取最新的平台证书
option.WithWechatPayAutoAuthCipher(mchID, mchCertificateSerialNumber, mchPrivateKey, mchAPIv3Key),
option.WithWechatPayCipher(
encryptors.NewWechatPayEncryptor(downloader.MgrInstance().GetCertificateVisitor(mchID)),
decryptors.NewWechatPayDecryptor(mchPrivateKey),
),
)
```

### 使用加解密算法工具包

#### 步骤一:获取微信支付平台证书

请求的敏感信息,使用微信支付平台证书中的公钥加密。推荐 [使用平台证书下载管理器](FAQ.md#如何使用平台证书下载管理器) 获取微信支付平台证书,或者 [下载平台证书](FAQ.md#如何下载微信支付平台证书)。

#### 步骤二:加解密

使用工具包 [utils](utils) 中的函数,手动对敏感信息加解密。

```go
package utils

// EncryptOAEPWithPublicKey 使用公钥加密
func EncryptOAEPWithPublicKey(message string, publicKey *rsa.PublicKey) (ciphertext string, err error)
// EncryptOAEPWithCertificate 使用证书中的公钥加密
func EncryptOAEPWithCertificate(message string, certificate *x509.Certificate) (ciphertext string, err error)

// DecryptOAEP 使用私钥解密
func DecryptOAEP(ciphertext string, privateKey *rsa.PrivateKey) (message string, err error)
```

[rsa_crypto_test.go](utils/rsa_crypto_test.go) 中演示了如何使用以上函数做敏感信息加解密。

#### 步骤三:设置 `Wechatpay-Serial` 请求头

请求的敏感信息加密后,在 HTTP 请求头中添加微信支付平台证书序列号 `Wechatpay-Serial`。该序列号用于告知微信支付加密使用的证书。

使用 `core.Client` 的 `Request` 方法来传输自定义 HTTPHeader。

```go
// Request 向微信支付发送请求
//
// 相比于 Get / Post / Put / Patch / Delete 方法,本方法支持设置更多内容
// 特别地,如果需要为当前请求设置 Header,应使用本方法
func (client *Client) Request(
ctx context.Context,
method, requestPath string,
headerParams http.Header,
queryParams url.Values,
postBody interface{},
contentType string,
) (result *APIResult, err error)

// 示例代码
// 微信支付平台证书序列号,对应加密使用的私钥
header.Add("Wechatpay-Serial", "5157F09EFDC096DE15EBE81A47057A72*******")
result, err := client.Request(
ctx,
"POST",
"https://api.mch.weixin.qq.com/v3/profitsharing/receivers/add",
header,
nil,
body,
"application/json")

```

Expand Down
129 changes: 111 additions & 18 deletions core/notify/notify.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,45 +6,107 @@ package notify
import (
"bytes"
"context"
"crypto/aes"
"crypto/cipher"
"encoding/base64"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
xy-peng marked this conversation as resolved.
Show resolved Hide resolved

"github.com/wechatpay-apiv3/wechatpay-go/core/auth"
"github.com/wechatpay-apiv3/wechatpay-go/core/auth/validators"
"github.com/wechatpay-apiv3/wechatpay-go/utils"
)

// Handler 微信支付通知 Handler
// Handler 通知处理器,使用前先设置验签和解密的算法套件
type Handler struct {
mchAPIv3Key string
validator validators.WechatPayNotifyValidator
cipherSuites map[string]CipherSuite
}

// CipherSuite 算法套件,包括验签和解密
type CipherSuite struct {
signatureType string
validator validators.WechatPayNotifyValidator
aeadAlgorithm string
aead cipher.AEAD
}

// NewEmptyHandler 创建一个不包含算法套件的空通知处理器
func NewEmptyHandler() *Handler {
h := &Handler{
cipherSuites: map[string]CipherSuite{},
}

return h
}

// AddCipherSuite 添加一个算法套件
func (h *Handler) AddCipherSuite(cipherSuite CipherSuite) *Handler {
h.cipherSuites[cipherSuite.signatureType] = cipherSuite
return h
}

// AddRSAWithAESGCM 添加一个 RSA + AES-GCM 的算法套件
func (h *Handler) AddRSAWithAESGCM(verifier auth.Verifier, aesgcm cipher.AEAD) *Handler {
v := CipherSuite{
signatureType: "WECHATPAY2-RSA2048-SHA256",
validator: *validators.NewWechatPayNotifyValidator(verifier),
aeadAlgorithm: "AEAD_AES_256_GCM",
aead: aesgcm,
}
return h.AddCipherSuite(v)
}

// ParseNotifyRequest 从 HTTP 请求(http.Request) 中解析 微信支付通知(notify.Request)
func (h *Handler) ParseNotifyRequest(ctx context.Context, request *http.Request, content interface{}) (
*Request, error,
) {
if err := h.validator.Validate(ctx, request); err != nil {
return nil, fmt.Errorf("not valid wechatpay notify request: %v", err)
func (h *Handler) ParseNotifyRequest(
ctx context.Context,
request *http.Request,
content interface{},
) (*Request, error) {
signType := request.Header.Get("Wechatpay-Signature-Type")
if signType == "" {
signType = "WECHATPAY2-RSA2048-SHA256"
}

suite, ok := h.cipherSuites[signType]
if !ok {
return nil, fmt.Errorf("unsupported Wechatpay-Signature-Type: %s", signType)
}

if err := suite.validator.Validate(ctx, request); err != nil {
return nil, fmt.Errorf("invalid notification, err: %v, request: %+v",
xy-peng marked this conversation as resolved.
Show resolved Hide resolved
err, request)
}

body, err := getRequestBody(request)
if err != nil {
return nil, err
}

return processBody(suite, body, content)
}

func processBody(suite CipherSuite, body []byte, content interface{}) (*Request, error) {
ret := new(Request)
if err = json.Unmarshal(body, ret); err != nil {
if err := json.Unmarshal(body, ret); err != nil {
return nil, fmt.Errorf("parse request body error: %v", err)
}

plaintext, err := utils.DecryptAES256GCM(
h.mchAPIv3Key, ret.Resource.AssociatedData, ret.Resource.Nonce, ret.Resource.Ciphertext,
if ret.Resource.Algorithm != suite.aeadAlgorithm {
return nil, fmt.Errorf(
"possible invalid notification, resource.algorithm %s is not the configured algorithm %s",
ret.Resource.Algorithm,
suite.aeadAlgorithm)
}

plaintext, err := doAEADOpen(
suite.aead,
ret.Resource.Nonce,
ret.Resource.Ciphertext,
ret.Resource.AssociatedData,
)
if err != nil {
return ret, fmt.Errorf("decrypt request error: %v", err)
return ret, fmt.Errorf("%s decrypt error: %v", ret.Resource.Algorithm, err)
}

ret.Resource.Plaintext = plaintext
Expand All @@ -56,6 +118,24 @@ func (h *Handler) ParseNotifyRequest(ctx context.Context, request *http.Request,
return ret, nil
}

func doAEADOpen(c cipher.AEAD, nonce, ciphertext, additionalData string) (string, error) {
data, err := base64.StdEncoding.DecodeString(ciphertext)
if err != nil {
return "", err
}
plaintext, err := c.Open(
nil,
[]byte(nonce),
data,
[]byte(additionalData),
)
if err != nil {
return "", err
}

return string(plaintext), nil
}

func getRequestBody(request *http.Request) ([]byte, error) {
body, err := ioutil.ReadAll(request.Body)
if err != nil {
Expand All @@ -68,10 +148,23 @@ func getRequestBody(request *http.Request) ([]byte, error) {
return body, nil
}

// NewNotifyHandler 创建通知处理器
func NewNotifyHandler(mchAPIv3Key string, verifier auth.Verifier) *Handler {
return &Handler{
mchAPIv3Key: mchAPIv3Key,
validator: *validators.NewWechatPayNotifyValidator(verifier),
// NewRSANotifyHandler 创建一个 RSA 的通知处理器,它包含 AES-GCM 解密能力
func NewRSANotifyHandler(apiV3Key string, verifier auth.Verifier) (*Handler, error) {
c, err := aes.NewCipher([]byte(apiV3Key))
if err != nil {
return nil, err
}
aesgcm, err := cipher.NewGCM(c)
if err != nil {
return nil, err
}

return NewEmptyHandler().AddRSAWithAESGCM(verifier, aesgcm), nil
}

// NewNotifyHandler 创建通知处理器
// Deprecated: Use NewRSANotifyHandler instead
func NewNotifyHandler(apiV3Key string, verifier auth.Verifier) *Handler {
h, _ := NewRSANotifyHandler(apiV3Key, verifier)
return h
}
Loading