4-4 实现JSAPI接口获取prepay_id:请求头证书RSA签名+验证
签名工具方法实现
在 PayService 中添加签名相关方法,为每个微信支付 API 请求自动生成 Authorization 头。
安装依赖
pnpm add randomstring
pnpm add -D @types/randomstring
bash
getSignHeaders 方法
import * as crypto from 'crypto';
import * as fs from 'fs';
import * as path from 'path';
import * as randomstring from 'randomstring';
// 在 PayService 类中添加
/**
* 生成微信支付 APIv3 签名请求头
* @param method HTTP 方法(GET/POST)
* @param url 请求 URL 路径
* @param body 请求体(GET 为空字符串)
*/
private getSignHeaders(method: string, url: string, body: string = '') {
const timestamp = Math.floor(Date.now() / 1000).toString();
const nonceStr = randomstring.generate(32);
const mchid = this.configService.get<string>('WECHAT_MCHID');
const serialNo = this.configService.get<string>('WECHAT_SERIAL_NO');
// 1. 构造签名串
const message = `${method}\n${url}\n${timestamp}\n${nonceStr}\n${body}\n`;
// 2. RSA-SHA256 签名
const signature = this.sign(message);
// 3. 组装 Authorization 头
const authorization =
`WECHATPAY2-SHA256-RSA2048 ` +
`mchid="${mchid}",` +
`nonce_str="${nonceStr}",` +
`signature="${signature}",` +
`timestamp="${timestamp}",` +
`serial_no="${serialNo}"`;
return { Authorization: authorization };
}
typescript
sign 方法
/**
* RSA-SHA256 签名
* @param message 待签名字符串
* @returns Base64 编码的签名值
*/
private sign(message: string): string {
const privateKeyPath = path.join(
__dirname,
'..',
'..',
'ssl',
'keys',
'apiclient_key.pem',
);
const privateKey = fs.readFileSync(privateKeyPath, 'utf-8');
const sign = crypto.createSign('RSA-SHA256');
sign.update(message);
sign.end();
return sign.sign(privateKey, 'base64');
}
typescript
改造 wechatJsPay 方法
在发起 JSAPI 下单请求时,自动添加签名头:
async wechatJsPay(params: WxPayParams) {
try {
const urlPath = '/v3/pay/transactions/jsapi';
const wxParams: WechatPayOrder = {
...params,
appid: this.configService.get<string>('WECHAT_APPID'),
mchid: this.configService.get<string>('WECHAT_MCHID'),
notify_url: this.configService.get<string>('WECHAT_NOTIFY_PAY'),
out_trade_no: this.getTradeNumber(),
time_expire: dayjs().add(30, 'minute').format(),
};
const body = JSON.stringify(wxParams);
// 生成签名头
const headers = this.getSignHeaders('POST', urlPath, body);
const result = await this.axiosInstance.post(
`https://api.mch.weixin.qq.com${urlPath}`,
wxParams,
{ headers }, // 传入签名头
);
return result.data;
} catch (error) {
this.logger.error('微信JSAPI下单失败', error.message);
throw error;
}
}
typescript
OpenSSL 命令行验证签名
签名实现后,可以通过 OpenSSL 命令行验证签名是否正确:
# 验证签名(使用商户公钥证书)
echo -n "签名串内容" | openssl dgst -sha256 -sign apiclient_key.pem | base64
bash
或者验证签名值:
# 验证签名(使用公钥)
echo -n "签名串内容" | openssl dgst -sha256 -verify <(openssl x509 -in apiclient_cert.pem -pubkey -noout) -signature <(echo "签名值" | base64 -d)
bash
Windows 用户注意
Windows 环境下可能遇到 OpenSSL 命令行问题,可以使用 Docker 容器替代:
# 使用 Docker 验证
docker run --rm -v $(pwd)/ssl:/ssl alpine/openssl \
dgst -sha256 -verify <(openssl x509 -in /ssl/certs/apiclient_cert.pem -pubkey -noout) \
-signature <(echo "签名值" | base64 -d)
bash
成功获取 prepay_id
签名正确后,请求微信 JSAPI 下单接口将返回 prepay_id:
{
"prepay_id": "wx201410272009395542657a690389285100"
}
json
prepay_id 是预付单标识,有效期 2 小时,用于后续调起小程序支付。
.env 完整配置
# 微信支付配置
WECHAT_APPID=wx1234567890
WECHAT_MCHID=1234567890
WECHAT_APIV3_KEY=your_apiv3_key_here
WECHAT_SERIAL_NO=5A6F2E3D4C5B6A7F8E9D0C1B2A3F4E5
WECHAT_NOTIFY_PAY=https://wechat-dev.domain.com/api/v1/pay/notify
bash
常见错误排查
| 错误 | 原因 | 解决方案 |
|---|---|---|
| 401 签名错误 | 签名串格式不对 | 检查换行符和末尾 \n |
| 401 证书序列号错误 | serial_no 不匹配 | 到商户平台确认序列号 |
| 401 商户号错误 | mchid 配置错误 | 检查 .env 中的 WECHAT_MCHID |
| 400 参数错误 | 请求体字段有误 | 对照微信文档检查参数 |
参考资源
- 微信支付签名验证 - 签名验证方法
- Node.js crypto.createSign - 签名 API
- randomstring - 随机字符串生成
↑