Apple Pay 商户验证
商户验证是 Apple Pay 支付流程的关键步骤,在用户点击支付后,Apple 会验证发起支付请求的商户身份是否合法。
本文档重点介绍:前端处理 onvalidatemerchant
事件、后端完成商户身份验证。
相关章节:支付请求创建
、Apple Pay Web 会话(Onerway 代理)
、支付授权
、完整支付流程
、会话初始化
、账号配置
、故障排查
流程概述
- 接收验证请求: 前端接收 Apple 提供的
validationURL
- 后端身份验证: 使用商户证书或通过 Onerway 代理向 Apple 验证身份
- 获取商户会话: 成功验证后获得
merchantSession
对象 - 完成验证: 前端调用
completeMerchantValidation()
继续支付流程
验证流程
商户验证流程
实现指南
前端实现
在前端处理onvalidatemerchant
事件:
/**
* 处理 Apple Pay 商户验证事件
* @param {ApplePaySession} session - 当前 Apple Pay 会话
* @param {ApplePayValidateMerchantEvent} event - Apple 返回的验证事件
* @param {Object} config - 商户配置信息
*/
async function handleMerchantValidation(session, event, config) {
// 构造发往商户后端的验证请求(仅保留关键参数)
const validationRequest = {
domainName: window.location.hostname, // 必须与已验证域名一致
validationURL: event.validationURL // 来自 Apple 的 onvalidatemerchant
};
try {
const resp = await fetch('/api/apple-pay/validate-merchant', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(validationRequest)
});
const resp = await resp.json();
if (resp.merchantSession) {
session.completeMerchantValidation(resp.data);
} else {
throw new Error(resp.error || '验证失败');
}
} catch (error) {
console.error('商户验证失败:', error);
session.abort();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
注意事项
获取到 session 对象后,请调用 completeMerchantValidation
方法完成商户验证。
后端实现
Apple Pay Web 会话(Onerway 代理)
通过 Onerway 代理完成商户验证。
API 请求参数
Parameter | Type | Length | Required | Signed | Description |
---|---|---|---|---|---|
appId | String | 20 | Yes | Yes | Merchant application ID assigned by Onerway for website identification. |
merchantNo | String | 20 | Yes | Yes | Merchant number assigned by |
requestId | String | 64 | Yes | Yes | Unique request identifier for tracking and deduplication. |
sign | String | / | Yes | No | Digital signature string for request verification and security. Please refer to Signature for signature generation method. |
verifyUrl | String | 256 | Yes | Yes | Apple Pay merchant validation URL (from |
website | String | 128 | Yes | Yes | Merchant domain name registered with Apple Pay (maps to Apple Pay |
响应
merchantSession
位于 data
内部:
Name | Type | Description |
---|---|---|
respCode | String | Response code from |
respMsg | String | Response message from |
data | Object | Response data. Refer to object data |
data
data
字段包含 Apple Pay 会话信息的 JSON 字符串:
Name | Type | Description |
---|---|---|
└data | String | Apple Pay merchant session data as JSON string. This data should be passed directly to the Apple Pay session completion handler. |
API 使用示例
{
"appId": "1727880846378401792",
"merchantNo": "800209",
"requestId": "ZYRDGXQSDEUMFFXVWEU",
"sign": "c46ea8b12bcc3392a6b8a58207a71f634442a8ea650d7cdb97494e1ec5f413c9",
"verifyUrl": "https://apple-pay-gateway-cert.apple.com/paymentservices/startSession",
"website": "rundown-beret.biz"
}
2
3
4
5
6
7
8
{
"respCode": "20000",
"respMsg": "Success",
"data": "{\"epochTimestamp\":1752027594903,\"expiresAt\":1752031194903,\"merchantSessionIdentifier\":\"SSHC70E8352BAB64DA1ABD5534864FB1D7D_916523AAED1343F5BC5815E12BEE9250AFFDC1A17C46B0DE5A943F0F94927C24\",\"nonce\":\"493ee68c\",\"merchantIdentifier\":\"A719B735608A23CC132E86CF3C67E22C446673B495D53A28FD2FA671B5B86C87\",\"domainName\":\"rundown-beret.biz\",\"displayName\":\"rundown-beret.biz\",\"signature\":\"308006092a864886f70d010702a0803080020101310d300b0609608648016503040201308006092a864886f70d0107010000a080308203e43082038ba003020102020859d8a1bcaaf4e3cd300a06082a8648ce3d040302307a312e302c06035504030c254170706c65204170706c69636174696f6e20496e746567726174696f6e204341202d20473331263024060355040b0c1d4170706c652043657274696669636174696f6e20417574686f7269747931133011060355040a0c0a4170706c6520496e632e310b3009060355040613025553301e170d3231303432303139333730305a170d3236303431393139333635395a30623128302606035504030c1f6563632d736d702d62726f6b65722d7369676e5f5543342d53414e44424f5831143012060355040b0c0b694f532053797374656d7331133011060355040a0c0a4170706c6520496e632e310b30090603550406130255533059301306072a8648ce3d020106082a8648ce3d030107034200048230fdabc39cf75e202c50d99b4512e637e2a901dd6cb3e0b1cd4b526798f8cf4ebde81a25a8c21e4c33ddce8e2a96c2f6afa1930345c4e87a4426ce951b1295a38202113082020d300c0603551d130101ff04023000301f0603551d2304183016801423f249c44f93e4ef27e6c4f6286c3fa2bbfd2e4b304506082b0601050507010104393037303506082b060105050730018629687474703a2f2f6f6373702e6170706c652e636f6d2f6f63737030342d6170706c65616963613330323082011d0603551d2004820114308201103082010c06092a864886f7636405013081fe3081c306082b060105050702023081b60c81b352656c69616e6365206f6e207468697320636572746966696361746520627920616e7920706172747920617373756d657320616363657074616e6365206f6620746865207468656e206170706c696361626c65207374616e64617264207465726d7320616e6420636f6e646974696f6e73206f66207573652c20636572746966696361746520706f6c69637920616e642063657274696669636174696f6e2070726163746963652073746174656d656e74732e303606082b06010505070201162a687474703a2f2f7777772e6170706c652e636f6d2f6365727469666963617465617574686f726974792f30340603551d1f042d302b3029a027a0258623687474703a2f2f63726c2e6170706c652e636f6d2f6170706c6561696361332e63726c301d0603551d0e041604140224300b9aeeed463197a4a65a299e4271821c45300e0603551d0f0101ff040403020780300f06092a864886f76364061d04020500300a06082a8648ce3d0403020347003044022074a1b324db4249430dd3274c5074c4808d9a1f480e3a85c5c1362566325fbca3022069369053abf50b5a52f9f6004dc58aad6c50a7d608683790e0a73ad01e4ad981308202ee30820275a0030201020208496d2fbf3a98da97300a06082a8648ce3d0403023067311b301906035504030c124170706c6520526f6f74204341202d20473331263024060355040b0c1d4170706c652043657274696669636174696f6e20417574686f7269747931133011060355040a0c0a4170706c6520496e632e310b3009060355040613025553301e170d3134303530363233343633305a170d3239303530363233343633305a307a312e302c06035504030c254170706c65204170706c69636174696f6e20496e746567726174696f6e204341202d20473331263024060355040b0c1d4170706c652043657274696669636174696f6e20417574686f7269747931133011060355040a0c0a4170706c6520496e632e310b30090603550406130255533059301306072a8648ce3d020106082a8648ce3d03010703420004f017118419d76485d51a5e25810776e880a2efde7bae4de08dfc4b93e13356d5665b35ae22d097760d224e7bba08fd7617ce88cb76bb6670bec8e82984ff5445a381f73081f4304606082b06010505070101043a3038303606082b06010505073001862a687474703a2f2f6f6373702e6170706c652e636f6d2f6f63737030342d6170706c65726f6f7463616733301d0603551d0e0416041423f249c44f93e4ef27e6c4f6286c3fa2bbfd2e4b300f0603551d130101ff040530030101ff301f0603551d23041830168014bbb0dea15833889aa48a99debebdebafdacb24ab30370603551d1f0430302e302ca02aa0288626687474703a2f2f63726c2e6170706c652e636f6d2f6170706c65726f6f74636167332e63726c300e0603551d0f0101ff0404030201063010060a2a864886f7636406020e04020500300a06082a8648ce3d040302036700306402303acf7283511699b186fb35c356ca62bff417edd90f754da28ebef19c815e42b789f898f79b599f98d5410d8f9de9c2fe0230322dd54421b0a305776c5df3383b9067fd177c2c216d964fc6726982126f54f87a7d1b99cb9b0989216106990f09921d00003182018830820184020101308186307a312e302c06035504030c254170706c65204170706c69636174696f6e20496e746567726174696f6e204341202d20473331263024060355040b0c1d4170706c652043657274696669636174696f6e20417574686f7269747931133011060355040a0c0a4170706c6520496e632e310b3009060355040613025553020859d8a1bcaaf4e3cd300b0609608648016503040201a08193301806092a864886f70d010903310b06092a864886f70d010701301c06092a864886f70d010905310f170d3235303730393032313935345a302806092a864886f70d010934311b3019300b0609608648016503040201a10a06082a8648ce3d040302302f06092a864886f70d010904312204206eea5fe324e35a0f4a87d568594a2334f3ad5e52ea2d1ad9b6de048605c83fda300a06082a8648ce3d04030204473045022100eff116376d607a5c43681ffe5ade91eafd5640723b8ff00a0a8f2b934f42d18e022059139c887992a14f6fa5fc61b3f3ba4ed454d1a295a6952d9068e561eb027f70000000000000\",\"operationalAnalyticsIdentifier\":\"rundown-beret.biz:A719B735608A23CC132E86CF3C67E22C446673B495D53A28FD2FA671B5B86C87\",\"retries\":0,\"pspId\":\"A719B735608A23CC132E86CF3C67E22C446673B495D53A28FD2FA671B5B86C87\"}"
}
2
3
4
5
代码示例
@PostMapping("/api/apple-pay/validate-merchant")
public ResponseEntity<Map<String, Object>> validateMerchant(@RequestBody Map<String, String> request) {
try {
String validationURL = request.get("validationURL");
String displayName = request.get("displayName");
String domainName = request.get("domainName");
// 构建 Onerway 请求体(仅保留关键业务参数)
Map<String, String> body = new HashMap<>();
body.put("verifyUrl", validationURL);
body.put("website", domainName);
// 调用 Onerway Apple Pay Web 会话 API
RestTemplate restTemplate = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<Map<String, String>> entity = new HttpEntity<>(body, headers);
ResponseEntity<Map> response = restTemplate.postForEntity(
ONERWAY_BASE_URL + "/txn/apiCheckApplePay",
entity,
Map.class
);
Map<String, Object> result = response.getBody();
if ("20000".equals(result.get("respCode"))) {
// 解析 merchantSession 数据
String sessionData = (String) result.get("data");
ObjectMapper mapper = new ObjectMapper();
Map<String, Object> merchantSession = mapper.readValue(sessionData, Map.class);
Map<String, Object> successResponse = new HashMap<>();
successResponse.put("merchantSession", merchantSession);
return ResponseEntity.ok(successResponse);
} else {
Map<String, Object> errorResponse = new HashMap<>();
errorResponse.put("error", result.get("respMsg"));
return ResponseEntity.status(500).body(errorResponse);
}
} catch (Exception e) {
logger.error("商户验证失败", e);
Map<String, Object> errorResponse = new HashMap<>();
errorResponse.put("error", "商户验证失败");
return ResponseEntity.status(500).body(errorResponse);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
app.post('/api/apple-pay/validate-merchant', async (req, res) => {
try {
const { validationURL, domainName } = req.body;
// 构建请求体(仅保留关键业务参数)
const body = {
verifyUrl: validationURL,
website: domainName
};
const response = await fetch(`${process.env.ONERWAY_BASE_URL}/txn/apiCheckApplePay`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
const result = await response.json();
if (result.respCode === '20000') {
// 解析 merchantSession 数据
const merchantSession = JSON.parse(result.data);
res.json({ merchantSession });
} else {
res.status(500).json({ error: result.respMsg || '验证失败' });
}
} catch (error) {
console.error('商户验证错误:', error);
res.status(500).json({ error: '商户验证失败' });
}
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
安全要点
证书管理(自有模式):
- 证书文件权限设置为 600
- 监控证书过期时间
- 定期轮换证书
请求安全:
- 验证 Apple 验证 URL 必须以
https://apple-pay-gateway
开头 - 所有通信使用 HTTPS
- 错误信息最小化暴露
详细实践参见 故障排查。
错误处理
基本原则:
- 验证失败时立即调用
session.abort()
终止支付 - 向用户显示友好错误提示
- 技术详情记录在服务端日志
常见错误:
- 网络超时:可重试 1-2 次
- 证书问题:检查证书配置
- URL 无效:验证域名白名单
完整错误处理策略参见 故障排查。
最佳实践
性能要求
- 设置合理的超时时间(5-10 秒)
- 缓存商户证书以提升响应速度
- 监控验证成功率和响应时间
安全规范
- 验证 Apple 验证 URL 的合法性
- 安全存储和管理商户证书
- 记录审计日志但避免敏感信息泄露
下一步
完成商户验证后,继续以下流程: