安卓 app 如何接入支付宝sdk实现免密支付流程?
结论
安卓 App 可以通过支付宝 App 支付 SDK 拉起签约页面,但免密支付无法只靠客户端完成。完整流程需要 App、业务服务端和支付宝共同配合:
- App 向业务服务端申请签约参数。
- 服务端调用支付宝签约接口,生成 SDK 所需的
orderStr。 - App 使用支付宝 SDK 拉起签约页面,服务端根据异步通知确认签约结果。
- 后续扣款由服务端使用签约号调用支付宝扣款接口,App 不能在本地直接发起免密扣款。
应用私钥、签名生成、签约状态确认、扣款和异步通知验签都必须放在服务端。客户端返回的 resultStatus 只能用于界面提示,不能作为签约或扣款成功的最终依据。
一、接入前需要准备什么
先在支付宝开放平台创建应用并完成产品签约。通常需要准备:
- 支付宝开放平台应用的
app_id - 应用私钥
- 应用公钥证书或支付宝公钥
- 支付宝公钥证书、根证书等证书文件,具体取决于所选签名模式
- 签约异步通知地址
- 支付异步通知地址
- 业务系统中的用户、协议和订单数据
- 已申请并获准使用的免密支付或周期扣款产品
免密支付不是普通 App 支付中的一个客户端开关。商户必须先取得相应的产品权限。可用的 product_code、personal_product_code、签约规则和扣款接口,应以该应用在支付宝开放平台实际开通的产品文档为准。
二、Android 接入支付宝 SDK
在应用模块中添加支付宝 SDK 依赖。SDK 版本号和仓库配置可能发生调整,应从支付宝开放平台当前的 Android 接入文档或下载页面获取,不建议直接复制旧版本号。
如果使用本地 AAR,可以按照 Android Gradle 的方式引入:
dependencies {
implementation files('libs/alipaysdk.aar')
}
如果官方当前提供 Maven 坐标,优先使用官方文档中的依赖声明。
还要检查应用是否声明了网络权限:
<uses-permission android:name="android.permission.INTERNET" />
支付宝 SDK 可能还要求配置特定组件或混淆规则,不同版本之间会有差异。Activity、Service 或 Provider 等配置应以当前 SDK 的官方接入说明为准,不要自行猜测。
三、整体签约和扣款流程
签约和扣款可以按下面的流程处理:
用户点击开通免密支付
↓
App 请求业务服务端创建签约
↓
服务端调用支付宝签约接口并生成 orderStr
↓
App 使用支付宝 SDK 拉起支付宝签约页面
↓
支付宝返回客户端结果,同时通知业务服务端
↓
服务端验签并保存 agreement_no
↓
需要扣款时,由业务服务端创建扣款订单
↓
服务端携带 agreement_no 调用支付宝扣款接口
↓
服务端接收支付通知或主动查询支付结果
流程中有两个重要的业务标识:
external_agreement_no:商户生成的外部协议号,必须唯一。agreement_no:支付宝在签约成功后返回的协议号,后续扣款和解约通常都需要使用。
建议为签约信息建立独立的记录表,至少保存以下字段:
user_id
external_agreement_no
agreement_no
sign_status
product_code
sign_time
invalid_time
last_notify_time
四、服务端生成签约参数
签约通常使用 alipay.user.agreement.page.sign 接口。服务端通过支付宝服务端 SDK 构造请求,再调用 sdkExecute,生成交给 Android SDK 的 orderStr。
下面是 Java 服务端示例。代码只展示典型字段,实际必填项取决于商户开通的免密产品。
AlipayClient alipayClient = new DefaultAlipayClient(
"https://openapi.alipay.com/gateway.do",
APP_ID,
APP_PRIVATE_KEY,
"json",
"UTF-8",
ALIPAY_PUBLIC_KEY,
"RSA2"
);
AlipayUserAgreementPageSignRequest request =
new AlipayUserAgreementPageSignRequest();
request.setNotifyUrl("https://api.example.com/alipay/agreement/notify");
request.setReturnUrl("https://www.example.com/alipay/agreement/result");
JSONObject bizContent = new JSONObject();
bizContent.put("external_agreement_no", externalAgreementNo);
bizContent.put("personal_product_code", "以实际开通产品为准");
bizContent.put("sign_scene", "以实际业务场景为准");
JSONObject accessParams = new JSONObject();
accessParams.put("channel", "ALIPAYAPP");
bizContent.put("access_params", accessParams);
request.setBizContent(bizContent.toJSONString());
AlipayUserAgreementPageSignResponse response =
alipayClient.sdkExecute(request);
if (!response.isSuccess()) {
throw new IllegalStateException(
response.getCode() + ": " + response.getSubMsg()
);
}
String orderStr = response.getBody();
服务端生成参数后,将 orderStr 返回给 App,例如:
{
"code": 0,
"data": {
"orderStr": "app_id=...&method=alipay.user.agreement.page.sign&..."
}
}
不要让 Android 客户端自行拼接请求参数,也不要在客户端使用应用私钥签名。否则,私钥很容易因 APK 被反编译而泄露。
如果开通的是周期扣款产品,签约请求可能还要包含周期规则、单次金额限制等参数。这些字段因产品而异,必须按照支付宝为对应产品提供的接口文档填写。
五、Android 拉起支付宝签约
App 从服务端取得完整的 orderStr 后,在工作线程中调用支付宝 SDK。
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.execute(() -> {
PayTask payTask = new PayTask(activity);
Map<String, String> result =
payTask.payV2(orderStr, true);
activity.runOnUiThread(() -> {
String resultStatus = result.get("resultStatus");
String memo = result.get("memo");
String rawResult = result.get("result");
if ("9000".equals(resultStatus)) {
// 这里只表示客户端流程看起来已完成。
// 仍然必须向业务服务端查询最终签约状态。
queryAgreementStatus();
} else if ("6001".equals(resultStatus)) {
showMessage("用户取消了签约");
} else {
showMessage(
memo == null ? "签约未完成" : memo
);
}
});
});
Kotlin 写法如下:
lifecycleScope.launch(Dispatchers.IO) {
val payTask = PayTask(this@AgreementActivity)
val result = payTask.payV2(orderStr, true)
withContext(Dispatchers.Main) {
when (result["resultStatus"]) {
"9000" -> queryAgreementStatus()
"6001" -> showMessage("用户取消了签约")
else -> showMessage(result["memo"] ?: "签约未完成")
}
}
}
部分支付宝产品或 SDK 版本可能采用不同的客户端拉起方式。如果当前产品文档明确要求使用授权任务、URL Scheme 或其他方式,应按对应文档接入,不能直接套用普通 App 支付示例中的 API。
六、以异步通知确认签约结果
签约完成后,支付宝会向签约请求中的 notify_url 发送通知。服务端收到通知后,应完成以下检查和处理:
- 使用支付宝公钥或证书验签。
- 检查通知中的
app_id是否属于当前应用。 - 检查
external_agreement_no是否存在,以及是否对应当前用户。 - 检查协议状态和产品类型。
- 保存支付宝返回的
agreement_no。 - 以幂等方式更新签约状态。
- 按照支付宝的要求返回成功响应。
验签示例:
Map<String, String> params = new HashMap<>();
request.getParameterMap().forEach((key, values) -> {
if (values != null && values.length > 0) {
params.put(key, values[0]);
}
});
boolean verified = AlipaySignature.rsaCheckV1(
params,
ALIPAY_PUBLIC_KEY,
"UTF-8",
"RSA2"
);
if (!verified) {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
return;
}
如果使用证书模式,应调用对应的证书验签方法,不能混用支付宝公钥模式和证书模式。
支付宝可能重复发送同一通知,因此更新逻辑必须具备幂等性。例如,可以为 external_agreement_no 和 agreement_no 建立唯一索引,避免重复创建协议记录。
即使客户端显示签约成功,只要服务端还没有确认,就不能立即允许免密扣款。App 可以调用业务接口查询签约状态,必要时再由业务服务端向支付宝查询协议状态。
七、服务端发起免密扣款
签约成功后,服务端保存 agreement_no。需要扣款时,由服务端创建商户订单,并调用支付宝提供的签约扣款接口。
典型调用可能基于 alipay.trade.pay,同时在请求中传入协议参数。下面只展示常见结构:
AlipayTradePayRequest request = new AlipayTradePayRequest();
request.setNotifyUrl("https://api.example.com/alipay/payment/notify");
JSONObject bizContent = new JSONObject();
bizContent.put("out_trade_no", outTradeNo);
bizContent.put("total_amount", "19.90");
bizContent.put("subject", "会员服务费");
bizContent.put("product_code", "以实际开通产品为准");
JSONObject agreementParams = new JSONObject();
agreementParams.put("agreement_no", agreementNo);
bizContent.put("agreement_params", agreementParams);
request.setBizContent(bizContent.toJSONString());
AlipayTradePayResponse payResponse =
alipayClient.execute(request);
if (!payResponse.isSuccess()) {
// 记录 code、sub_code、sub_msg,按错误类型决定查询、
// 重试、关闭订单或提示用户重新签约。
throw new IllegalStateException(
payResponse.getCode() + ": " +
payResponse.getSubCode() + ": " +
payResponse.getSubMsg()
);
}
具体使用哪个接口,以及 product_code 和 agreement_params 中需要哪些字段,应以商户实际开通的产品为准。周期扣款和一般代扣的准入条件、扣款时间、金额限制及接口参数可能不同。
同步响应也不能单独用来判断扣款是否成功。服务端需要结合以下信息确认最终结果:
- 支付宝异步支付通知
- 主动订单查询
- 商户订单状态
- 支付宝交易号
八、扣款通知和订单幂等
处理支付通知时,至少要校验:
- 通知签名
app_idout_trade_notrade_nototal_amountseller_id- 交易状态
- 订单当前状态
金额校验应使用 BigDecimal,不要使用 double:
BigDecimal notifiedAmount =
new BigDecimal(params.get("total_amount"));
BigDecimal orderAmount =
order.getAmount();
if (notifiedAmount.compareTo(orderAmount) != 0) {
throw new IllegalStateException("支付金额不一致");
}
同一条通知可能会被多次投递。更新订单状态时,建议使用数据库条件更新,只允许以下状态转换:
WAIT_PAY -> SUCCESS
WAIT_PAY -> CLOSED
已经成功的订单再次收到成功通知时,应直接返回成功,不能重复发货、充值或增加会员时长。
九、解约和失效处理
业务系统还需要提供关闭免密支付的入口。解约通常由服务端调用支付宝的协议解约接口,并通过 agreement_no 或商户协议号定位协议。
完整流程包括:
- 用户在 App 中申请关闭免密支付。
- 服务端查询本地协议状态。
- 服务端调用支付宝解约接口。
- 根据同步结果、异步通知或主动查询确认解约结果。
- 将本地协议状态更新为已解约。
- 后续扣款前再次检查协议状态。
用户也可能直接在支付宝客户端解约。因此,本地保存了 agreement_no 并不表示协议会一直有效。扣款失败时,应识别协议失效类错误并停止自动重试,再提示用户重新签约。
十、常见错误
在 Android 中保存应用私钥
这是严重的安全问题。APK 可以被反编译,私钥泄露后,攻击者可能伪造商户请求。所有签名操作都应由业务服务端完成。
把 resultStatus=9000 当成最终签约成功
客户端结果可能受到网络中断、进程终止或回调数据异常的影响。最终状态必须由服务端通过异步通知或主动查询确认。
签约成功后由客户端直接扣款
免密扣款涉及商户私钥、协议号、订单金额和交易状态控制,必须在可信的服务端执行。
重复使用外部协议号或订单号
external_agreement_no 和 out_trade_no 都应在业务范围内保持唯一。重复使用可能引发请求冲突或状态混乱。
对所有失败请求直接重试
免密扣款失败可能由余额不足、协议失效、额度限制、风控或系统繁忙引起。只有明确可以重试的系统类错误,才适合进行有限次数的重试。遇到协议失效或业务拒绝时,不应自动连续扣款。
忽略用户授权范围
实际扣款必须符合签约时向用户展示的金额、周期、服务内容和解约规则。签约后不能任意改变扣款用途或频率。
十一、推荐的服务端接口设计
Android App 不必直接处理支付宝的全部参数,只需对接业务服务端:
POST /api/payment/agreement/create
GET /api/payment/agreement/status
POST /api/payment/agreement/terminate
服务端内部处理以下回调:
POST /callbacks/alipay/agreement
POST /callbacks/alipay/payment
扣款接口通常只供可信的业务服务调用,不建议直接暴露给移动客户端:
POST /internal/payment/alipay/deduct
采用这种设计后,支付宝参数、签名逻辑、订单幂等和风控都集中在服务端。Android 端只负责展示签约入口、拉起支付宝并查询最终状态。
备注:内容仅供参考。
