鸿蒙系统调起支付宝免密签约的代码示例
结论
可以在鸿蒙应用中完成支付宝免密签约,但一般不能只靠一段客户端代码完成。完整流程至少包含三个部分:
- 商户服务端调用支付宝签约接口,生成经过 RSA2 签名的签约 URL。
- 鸿蒙客户端通过支付宝 URL Scheme 或浏览器打开该 URL。
- 服务端接收支付宝异步通知,并主动查询签约状态,确认签约是否成功。
签名私钥、支付宝开放平台接口调用和异步验签都必须放在服务端。鸿蒙客户端只负责请求签约地址、唤起支付宝和展示结果,不能根据客户端回跳直接判断签约成功。
推荐流程
鸿蒙客户端
│ 申请签约
▼
商户服务端
│ 调用支付宝签约接口
▼
返回签约 URL
│
▼
鸿蒙客户端唤起支付宝
│ 用户确认签约
▼
支付宝异步通知商户服务端
│ 验签并查询协议状态
▼
鸿蒙客户端查询最终签约结果
支付宝的具体签约接口、biz_content 参数和产品码,取决于商户开通的代扣产品。常见场景会使用支付宝用户协议页面签约能力,但不能直接照搬其他项目中的 personal_product_code、sign_scene 等参数。实际取值应以当前商户的产品配置和支付宝开放平台文档为准。
一、服务端生成签约链接
下面的 Java 示例给出了基本结构。接口名称和业务参数需要根据实际开通的产品调整,私钥和签名逻辑不能放进鸿蒙客户端。
import com.alipay.api.AlipayClient;
import com.alipay.api.DefaultAlipayClient;
import com.alipay.api.domain.AlipayUserAgreementPageSignModel;
import com.alipay.api.request.AlipayUserAgreementPageSignRequest;
import com.alipay.api.response.AlipayUserAgreementPageSignResponse;
public class AlipayAgreementService {
private final AlipayClient alipayClient;
public AlipayAgreementService(
String gateway,
String appId,
String merchantPrivateKey,
String alipayPublicKey) {
this.alipayClient = new DefaultAlipayClient(
gateway,
appId,
merchantPrivateKey,
"json",
"UTF-8",
alipayPublicKey,
"RSA2"
);
}
public String createAgreementUrl(
String externalAgreementNo,
String userIdentity) throws Exception {
AlipayUserAgreementPageSignRequest request =
new AlipayUserAgreementPageSignRequest();
request.setNotifyUrl(
"https://api.example.com/alipay/agreement/notify"
);
request.setReturnUrl(
"https://m.example.com/alipay/agreement/result"
);
AlipayUserAgreementPageSignModel model =
new AlipayUserAgreementPageSignModel();
// 商户侧唯一签约编号,必须持久化并保证不重复。
model.setExternalAgreementNo(externalAgreementNo);
/*
* 以下参数仅表示设置位置,不代表适用于所有产品。
* 参数值应从支付宝开放平台对应产品文档或商户配置中取得。
*/
model.setPersonalProductCode("YOUR_PERSONAL_PRODUCT_CODE");
model.setSignScene("YOUR_SIGN_SCENE");
/*
* 用户标识的类型和取值由实际接口要求决定。
* 不要把手机号等信息直接代入,除非接口明确支持。
*/
model.setExternalLogonId(userIdentity);
request.setBizModel(model);
AlipayUserAgreementPageSignResponse response =
alipayClient.pageExecute(request, "GET");
if (!response.isSuccess()) {
throw new IllegalStateException(
"生成签约链接失败,code=" + response.getCode()
+ ", subCode=" + response.getSubCode()
+ ", message=" + response.getSubMsg()
);
}
/*
* 不同版本支付宝 Java SDK 对 pageExecute 返回内容的处理方式
* 可能存在差异。这里预期 getBody() 为可访问的签约地址;
* 接入时应检查实际返回值,不能假定它一定是 URL。
*/
return response.getBody();
}
}
还需要向客户端提供一个业务接口,例如:
POST /api/agreement/create
Content-Type: application/json
{
"userId": "10001"
}
响应可以这样设计:
{
"agreementRequestId": "AGR202609090001",
"signUrl": "https://openapi.alipay.com/..."
}
agreementRequestId 应关联服务端保存的签约记录。对于客户端传入的用户编号、协议编号、金额等业务参数,服务端必须重新校验,不能直接信任。
二、鸿蒙客户端唤起支付宝
支付宝通常可以通过 URL Scheme 打开指定页面,常见格式如下:
alipays://platformapi/startapp?appId=20000067&url=ENCODED_SIGN_URL
其中,signUrl 必须经过 URL 编码。否则,签约地址中的 &、=、? 等字符会破坏外层 Scheme。
下面是 ArkTS 示例:
import { common, Want } from '@kit.AbilityKit';
export class AlipayAgreementLauncher {
private context: common.UIAbilityContext;
constructor(context: common.UIAbilityContext) {
this.context = context;
}
async open(signUrl: string): Promise<void> {
if (!this.isValidSignUrl(signUrl)) {
throw new Error('签约地址无效');
}
const encodedUrl = encodeURIComponent(signUrl);
const alipayUri =
`alipays://platformapi/startapp?appId=20000067&url=${encodedUrl}`;
const want: Want = {
action: 'ohos.want.action.viewData',
uri: alipayUri
};
try {
await this.context.startAbility(want);
} catch (error) {
console.error(`唤起支付宝失败: ${JSON.stringify(error)}`);
// 唤端失败时可降级到系统浏览器打开签约地址。
await this.openInBrowser(signUrl);
}
}
private async openInBrowser(url: string): Promise<void> {
const browserWant: Want = {
action: 'ohos.want.action.viewData',
uri: url
};
await this.context.startAbility(browserWant);
}
private isValidSignUrl(url: string): boolean {
return url.startsWith('https://');
}
}
在页面中调用:
const launcher = new AlipayAgreementLauncher(
getContext(this) as common.UIAbilityContext
);
async function startAgreement(): Promise<void> {
// signUrl 应通过 HTTPS 从商户服务端取得。
const signUrl: string = await requestAgreementUrl();
await launcher.open(signUrl);
}
网络请求可以沿用项目现有的 HTTP 封装:
interface AgreementResponse {
agreementRequestId: string;
signUrl: string;
}
async function requestAgreementUrl(): Promise<string> {
const response: AgreementResponse = await agreementApi.create();
if (!response.signUrl) {
throw new Error('服务端未返回签约地址');
}
return response.signUrl;
}
@kit.AbilityKit、Want 和 startAbility 的具体导入方式可能随鸿蒙 SDK/API 版本而变化。如果现有工程仍使用旧模块路径,应以当前 SDK 的类型定义为准。
三、处理签约结果
签约结果通常通过两条链路返回:
return_url:用户完成操作后的跳转地址,主要用于改善页面体验。notify_url:支付宝向商户服务端发送异步通知,是业务处理的主要依据。
客户端发生回跳,只能说明用户离开了支付宝签约页面,不能证明签约已经成功。用户可能取消操作、关闭页面或遇到网络中断,回跳 URI 也可能被伪造。
服务端异步通知处理
下面的代码展示了基本处理结构。具体字段名称应以实际接口的通知内容为准:
@PostMapping("/alipay/agreement/notify")
public String handleAgreementNotify(
HttpServletRequest request) throws Exception {
Map<String, String> parameters = extractParameters(request);
boolean verified = AlipaySignature.rsaCheckV1(
parameters,
alipayPublicKey,
"UTF-8",
"RSA2"
);
if (!verified) {
return "failure";
}
String externalAgreementNo =
parameters.get("external_agreement_no");
String agreementNo = parameters.get("agreement_no");
String status = parameters.get("status");
/*
* 1. 根据 externalAgreementNo 查询本地签约记录。
* 2. 校验 app_id、商户身份及业务关联关系。
* 3. 必要时调用支付宝协议查询接口二次确认状态。
* 4. 使用事务和唯一键保证重复通知不会重复处理。
*/
updateAgreementStatus(
externalAgreementNo,
agreementNo,
status
);
return "success";
}
生产环境还需要检查以下内容:
- 通知是否已通过支付宝公钥验签。
app_id、商户身份等信息是否与当前应用匹配。external_agreement_no是否属于当前用户和当前业务。- 通知状态是否符合当前状态机的转换规则。
- 相同通知重复到达时,处理结果能否保持幂等。
- 是否需要调用协议查询接口二次确认。
只有验签和业务校验全部通过后,才能返回支付宝要求的成功响应。
鸿蒙客户端查询结果
用户从支付宝回到应用后,客户端应使用之前保存的 agreementRequestId 查询商户服务端:
type AgreementStatus =
'PENDING' |
'SIGNED' |
'FAILED' |
'CANCELED' |
'UNKNOWN';
interface AgreementStatusResponse {
status: AgreementStatus;
agreementNo?: string;
}
async function refreshAgreementStatus(
agreementRequestId: string
): Promise<void> {
const result: AgreementStatusResponse =
await agreementApi.queryStatus(agreementRequestId);
switch (result.status) {
case 'SIGNED':
showToast('免密签约成功');
break;
case 'FAILED':
case 'CANCELED':
showToast('签约未完成');
break;
case 'PENDING':
case 'UNKNOWN':
showToast('签约结果确认中,请稍后刷新');
break;
}
}
如果需要回跳到应用内,可以为鸿蒙应用配置自定义 URI,例如:
myapp://alipay/agreement/result
支付宝是否允许把该 URI 直接配置为 return_url,取决于具体产品规则和跳转策略。更稳妥的做法是将 return_url 指向商户的 HTTPS 页面,再由该页面展示结果或尝试唤回应用。无论使用哪种方式,客户端返回后都要重新向服务端查询状态。
签约状态建议
服务端最好维护一套独立的签约状态机:
CREATED
│
▼
PENDING
├──► SIGNED
├──► FAILED
└──► CANCELED
生成签约链接不代表签约成功,因此不能在这一步把状态设为成功,也不能只依赖一次异步通知。对于长时间停留在 PENDING 状态的记录,可以通过定时任务调用支付宝协议查询接口,补偿确认最终状态。
注意事项
-
私钥只能保存在服务端
鸿蒙应用包可以被分析。如果把商户私钥、应用私钥或完整签名算法放进客户端,攻击者可能伪造签名。
-
签约链接应由服务端实时生成
服务端需要校验登录用户、业务资格和当前签约状态,并为每次请求生成新的签约记录,或者返回仍然有效的记录。
-
不要硬编码产品参数
personal_product_code、sign_scene、用户标识类型等参数与商户开通的代扣产品有关。参数不正确时,即使成功唤起支付宝,也可能无法进入对应的签约流程。 -
正确编码唤端参数
内层
signUrl需要执行一次encodeURIComponent。不要随意重复编码,否则支付宝可能无法解析目标地址。 -
准备未安装支付宝时的降级方案
startAbility失败后,可以用浏览器打开 HTTPS 签约地址,也可以提示用户安装支付宝。是否支持完全通过浏览器完成签约,要以对应产品的能力为准。 -
以服务端状态作为最终结果
应用回到前台、收到自定义 URI 或打开
return_url,都不能作为签约成功的凭证。最终状态必须来自通过验签的通知或支付宝协议查询接口。 -
处理好幂等和重复请求
用户可能连续点击签约按钮,支付宝也可能重复发送通知。建议为商户签约编号设置唯一约束,并让通知接口支持幂等更新。
-
记录必要的排查信息
可以记录商户签约编号、支付宝协议号、接口错误码和状态更新时间,但不要在日志中写入商户私钥、完整签名串或不必要的个人敏感信息。
上述代码可以作为服务端生成链接、鸿蒙端唤起支付宝以及服务端确认结果的实现骨架,但不能把示例中的产品参数和返回字段直接套用到所有商户。接入前还需确认当前开通的支付宝代扣产品、开放平台接口权限,以及鸿蒙工程使用的 SDK/API 版本。
备注:内容仅供参考。
