手拼 alipay.trade.page.pay 签名:不用 SDK 时的六类高频报错与修复
TypeScript/Node.js 自研商城对接支付宝电脑网站支付,不引 SDK,自己拼待签串和 sign。下面是六类反复出现的报错及其修复,附可复现的函数。
相关文档:
- 接口 alipay.trade.page.pay:https://opendocs.alipay.com/open/common/105901
- 加签原理与签名工具:https://opendoc.alipay.com/open/02khjm
- 验签出错 charset 说明:https://opendocs.alipay.com/support/01ravv
- 时间戳非法说明:https://opendocs.alipay.com/
- 正式网关
https://openapi.alipay.com/gateway.do,沙箱网关https://openapi.alipaydev.com/gateway.do
1. 时间戳:toISOString 生成的一定被拒
报错 isv.invalid-timestamp。原因是用 new Date().toISOString() 生成时间,自带 UTC 时区、毫秒和 T/Z,形如 2026-08-14T21:14:42.477Z,不符合支付宝规范;服务器时区不在东八区时还会整体偏 8 小时。
timestamp 必须是北京时间字符串 yyyy-MM-dd HH:mm:ss,无毫秒、无时区后缀:
function formatAlipayTimestamp(): string {
const now = new Date()
const utc8Time = new Date(now.getTime() + 8 * 60 * 60 * 1000)
const pad = (num: number) => String(num).padStart(2, '0')
return `${utc8Time.getUTCFullYear()}-${pad(utc8Time.getUTCMonth() + 1)}-${pad(utc8Time.getUTCDate())} ${pad(utc8Time.getUTCHours())}:${pad(utc8Time.getUTCMinutes())}:${pad(utc8Time.getUTCSeconds())}`
}
不要用字符串截取 toISOString() 的方式糊:毫秒不是 000 时会残留小数点。这个函数封装成公共方法,全局统一调用。
2. charset 只认 form action 的网关 URL
验签失败原文:验签出错,请确认charset参数放在了URL查询字符串中且各参数值使用charset参数指示的字符集编码。
最高发的根因是网关 URL 没带 charset=utf-8。支付宝 POST 表单提交时,只从 form 的 action 网关 URL 读取 charset,表单 body 里传的 charset 不生效——所以下面这种写法必错:
正确做法是在 action 上拼:
const gatewayUrl = 'https://openapi.alipay.com/gateway.do'
const sep = gatewayUrl.includes('?') ? '&' : '?'
const formAction = `${gatewayUrl}${sep}charset=utf-8`
兼容方案是表单里保留 charset 隐藏域,同时 URL 上追加一份。另外网关地址本身不要带 ?,参数值末尾也不要带 ?,这两处都会引发同类校验失败。
3. 待签串必须剔除空值参数
支付宝规范要求拼接待签串时过滤掉值为空字符串的参数,比如未配置的 return_url。直接遍历全部参数排序拼接就会签错。标准函数:
function buildAlipaySignContent(params: Record, excludeKeys: string[] = []): string {
return Object.keys(params)
.filter(key => !excludeKeys.includes(key) && params[key] !== '')
.sort()
.map(key => `${key}=${params[key]}`)
.join('&')
}
4. 下单签名剔 sign,回调验签剔 sign 和 sign_type
两个场景的剔除规则不一样,这也是容易串味的地方:
- 下单签名:保留
sign_type=RSA2,只剔除sign,调用buildAlipaySignContent(params, ['sign'])。 - 回调验签:同时剔除
sign和sign_type,调用buildAlipaySignContent(params, ['sign', 'sign_type'])。
不要写一套统一过滤逻辑给两处复用。
5. 参数按 ASCII 升序排列
官方定义:取所有 POST 内容,排除字节类型参数,剔除 sign 字段,剔除值为空的参数;按参数名第一个字符的 ASCII 码递增排序,首字符相同则比第二个字符,以此类推;用 参数=参数值 组合后以 & 连接,得到待签名字符串。
使用公钥证书模式签名时,待签名字符串里还要加上应用公钥证书 SN(app_cert_sn)和支付宝根证书 SN(alipay_root_cert_sn)。
电脑网站支付的待签串示例:
app_id=2016101800718925&biz_content={"out_trade_no":"...","product_code":"FAST_INSTANT_TRADE_PAY","total_amount":88.88,"subject":"Iphone6 16G","body":"Iphone6 16G"}&charset=utf-8&format=json&method=alipay.trade.page.pay&sign_type=RSA2×tamp=2020-10-13 09:58:50&version=1.0
拼接请求 URL 时:biz_content 的值做 url_encode,sign 的值做 url_encode,然后拼成 网关?待签名字符串&sign=url_encode后的sign。
6. 中文编码统一 UTF-8
前端表单自动提交时,浏览器可能按 GBK 编码发送,中文字段乱码会导致两端签名原文不一致。表单上加:
page.pay 公共参数
app_id必选method=alipay.trade.page.pay必选format=JSON可选charset=utf-8必选sign_type=RSA2必选sign必选timestamp必选,格式yyyy-MM-dd HH:mm:ssversion=1.0必选notify_url、return_url可选biz_content必选,内含out_trade_no、product_code=FAST_INSTANT_TRADE_PAY、total_amount、subject等
total_amount 的单位是「元」,字符串形式,保留两位小数。
其他线上问题
密钥解析异常:后台存的是脱敏占位符 ****;公钥只粘贴了裸 Base64,缺 PEM 头尾;换行被转义成字面量 \n;公私钥粘贴错位。
后台配置不生效:notify_url 输入框只读,改了不回写数据库。
僵尸订单:订单过期只在支付下单接口里做懒更新,列表查询不同步,15 分钟超时单仍显示待支付。前端支付入口不展示也是同类配置/字段问题。
安全与联调
- 不要在正式环境调试支付,高频失败会触发风控,还会批量生成无效待支付订单。
- 私钥禁止打印进日志,禁止传到前端。
- 区分沙箱与生产的 AppID 和密钥。
- 回调验签要做幂等,防重复。
- 桌面端内嵌 WebView 打开支付时,建议改用系统浏览器。
排查签名类问题时,把待签名字符串、sign、完整请求 URL 都打出来,和支付宝开发助手生成的结果逐字段对比。