记得很早以前公司项目中添加过移动支付这一块, 包括微信,支付宝,银联等第三方的整合。 但是后来懒于总结就没留下什么, 最近公司项目打算添加,所以打算简单总结一下,记上一笔以备将来使用。 毕竟第三方的支付SDK , 一般定下了以后三五年不会改变。 闲话少说,开干。 集成第三方SDK没什么难度,只要我们用心阅读文档和开发引导,集成起来再留点神,一切都不是问题。
1. 申请流程(支付宝移动开发平台)
1.1 准备工作
1. 单位营业执照彩色扫描件或数码照片
2. 对公银行账户(基本账户、一般账户均可)
3. 法定代表人的身份证彩色扫描件或数码照片 若为代理人(即法人以外的公司代表)申请认证,需额外提供以下两项材料
4. 代理人的身份证彩色扫描件或数码照片
5. 委托书,委托书上必须盖有单位公章或财务专用章(合同专用章、业务专用章等无效)下载委托书模版
1.2 创建应用
这里就给个创建应用链接 ,再给个图 https://open.alipay.com/developmentAccess/developmentAccess.htm图上很清楚了, 一般就是需要使用哪种支付渠道就和支付宝签约哪种, 右上角有个立即签约, 上传材料审核时间一般半上午就行了。 应用创建审核通过之后会有一个APPID 端上需要进行配置。
1.3 配置RSA秘钥
这一部分很重要,因为牵涉到配置,签名,继续看。来个官网 , 根据不同系统下载了RSA生成工具,再截个图吧 ,
点击RSA签名验签名工具.command , 可能会有执行权限问题, 看txt文档的操作执行。
官网推荐的使用2048长度的, 本地已经生成公钥和私钥两个文件 , 按照操作流程 ,将公钥上传配置到设置应用公钥处即可。
2. SDK配置
将下载好的SDK解压找到Android部分,将jar 导入项目中。SDK地址 并且在我们的app/build.gradle里配置一下
compile files('libs/alipaySdk-20170922.jar')
配置一下Manfest文件
<!-- alipay sdk begin --> <activity android:name="com.alipay.sdk.app.H5PayActivity" android:configChanges="orientation|keyboardHidden|navigation|screenSize" android:exported="false" android:screenOrientation="behind" android:windowSoftInputMode="adjustResize|stateHidden"></activity> <activity android:name="com.alipay.sdk.app.H5AuthActivity" android:configChanges="orientation|keyboardHidden|navigation" android:exported="false" android:screenOrientation="behind" android:windowSoftInputMode="adjustResize|stateHidden"></activity> <!-- alipay sdk end -->
再来配置一下运行权限
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> <uses-permission android:name="android.permission.READ_PHONE_STATE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
项目中需要添加混淆的可以配一下
-optimizationpasses 7 -verbose -keepattributes Exceptions,InnerClasses -dontskipnonpubliclibraryclasses -dontskipnonpubliclibraryclassmembers -ignorewarnings -keep public class * extends android.app.Activity -keep public class * extends android.app.Application -keep public class * extends android.app.Service -keep public class * extends android.content.BroadcastReceiver -keep public class * extends android.content.ContentProvider -keep public class * extends java.lang.Throwable {*;} -keep public class * extends java.lang.Exception {*;}-keep class com.alipay.android.app.IAlixPay{*;} -keep class com.alipay.android.app.IAlixPay$Stub{*;} -keep class com.alipay.android.app.IRemoteServiceCallback{*;} -keep class com.alipay.android.app.IRemoteServiceCallback$Stub{*;} -keep class com.alipay.sdk.app.PayTask{ public *;} -keep class com.alipay.sdk.app.AuthTask{ public *;} -keep class com.alipay.sdk.app.H5PayCallback { <fields>; <methods>; } -keep class com.alipay.android.phone.mrpc.core.** { *; } -keep class com.alipay.apmobilesecuritysdk.** { *; } -keep class com.alipay.mobile.framework.service.annotation.** { *; } -keep class com.alipay.mobilesecuritysdk.face.** { *; } -keep class com.alipay.tscenter.biz.rpc.** { *; } -keep class org.json.alipay.** { *; } -keep class com.alipay.tscenter.** { *; } -keep class com.ta.utdid2.** { *;} -keep class com.ut.device.** { *;}
3. 支付接口调用
3.1 正式支付环境的接口调用
package com.greencheng.android.parent.alipay; import android.annotation.SuppressLint; import android.app.AlertDialog; import android.content.DialogInterface; import android.content.Intent; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.support.v4.app.FragmentActivity; import android.text.TextUtils; import android.util.Log; import android.view.View; import android.widget.Toast; import com.alipay.sdk.app.PayTask; import com.greencheng.android.parent.R; import java.util.Map; /** * 重要说明: * <p> * 这里只是为了方便直接向商户展示支付宝的整个支付流程;所以Demo中加签过程直接放在客户端完成; * 真实App里,privateKey等数据严禁放在客户端,加签过程务必要放在服务端完成; * 防止商户私密数据泄露,造成不必要的资金损失,及面临各种安全风险; */ public class PayDemoActivity extends FragmentActivity {/** * 支付宝支付业务:入参app_id */ public static final String APPID = "201803010d22f94394"; private static final int SDK_PAY_FLAG = 1; @SuppressLint("HandlerLeak")private Handler mHandler = new Handler() {@SuppressWarnings("unused")public void handleMessage(Message msg) {switch (msg.what) {case SDK_PAY_FLAG: {@SuppressWarnings("unchecked")PayResult payResult = new PayResult((Map<String, String>) msg.obj); /** 对于支付结果,请商户依赖服务端的异步通知结果。同步通知结果,仅作为支付结束的通知。 */ String resultInfo = payResult.getResult();// 同步返回需要验证的信息 String resultStatus = payResult.getResultStatus(); // 判断resultStatus 为9000则代表支付成功 if (TextUtils.equals(resultStatus, "9000")) {// 该笔订单是否真实支付成功,需要依赖服务端的异步通知。 Toast.makeText(PayDemoActivity.this, "支付成功", Toast.LENGTH_SHORT).show(); } else {// 该笔订单真实的支付结果,需要依赖服务端的异步通知。 Toast.makeText(PayDemoActivity.this, "支付失败", Toast.LENGTH_SHORT).show(); }break; }default:break; }}; }; @Override protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState); setContentView(R.layout.pay_external); }/** * 支付宝支付业务 * * @param v */ public void payV2(View v) {if (TextUtils.isEmpty(APPID)) {new AlertDialog.Builder(this).setTitle("警告").setMessage("需要配置APPID | RSA_PRIVATE").setPositiveButton("确定", new DialogInterface.OnClickListener() {public void onClick(DialogInterface dialoginterface, int i) {// finish(); }}).show(); return; }Runnable payRunnable = new Runnable() {/** * 这里只是为了方便直接向商户展示支付宝的整个支付流程;所以Demo中加签过程直接放在客户端完成; * 真实App里,privateKey等数据严禁放在客户端,加签过程务必要放在服务端完成; * 防止商户私密数据泄露,造成不必要的资金损失,及面临各种安全风险; * * orderInfo的获取必须来自服务端; * { "pay_params":"app_id=201221&biz_content=%7B%22out_trade_no%22%4104%2C%22subject%22 %xxx2C%22total_amount%22%3A0.02%2C%22product_code%22%3A%22QUICK_MSECURITY_PAY%22%7D& charset=utf-8&format=JSON&method=alipay.trade.app.pay¬ify_url=https%3A%2F%2Fpayment. test.xxx.com%2Fwechatnotify%2FappPay&sign=YwLzlQ0BfZCBZG%2FENu0NBQ%3D%3D&sign_typ e=RSA2×tamp=2018-03-09+09%3A24%3A20&version=1.0", "bill_id":"91" } */ 服务端会根据参数生成一个这样格式的字符串, 这是简化版 final String orderInfo = "app_id=201221&biz_content=%7B%22out_trade_no%22%4104%2C%22subject%22%xxx2C%22total_amount%22%3A0.02%2C%22product_code%22%3A%22QUICK_MSECURITY_PAY%22%7D&charset=utf-8&format=JSON&method=alipay.trade.app.pay¬ify_url=https%3A%2F%2Fpayment.test.xxx.com%2Fwechatnotify%2FappPay&sign=YwLzlQ0BfZCBZG%2FENu0NBQ%3D%3D&sign_type=RSA2×tamp=2018-03-09+09%3A24%3A20&version=1.0"; @Override public void run() {PayTask alipay = new PayTask(PayDemoActivity.this); Map<String, String> result = alipay.payV2(orderInfo, true); Log.i("msp", result.toString()); Message msg = new Message(); msg.what = SDK_PAY_FLAG; msg.obj = result; mHandler.sendMessage(msg); }}; /// 根据官方文档的要求这里必须使用子线程 Thread payThread = new Thread(payRunnable); payThread.start(); }/** * get the sdk version. 获取SDK版本号 */ public void getSDKVersion() {PayTask payTask = new PayTask(this); String version = payTask.getVersion(); Toast.makeText(this, version, Toast.LENGTH_SHORT).show(); }/** * 原生的H5(手机网页版支付切natvie支付) 【对应页面网页支付按钮】 * * @param v */ public void h5Pay(View v) {Intent intent = new Intent(this, H5PayDemoActivity.class); Bundle extras = new Bundle(); /** * url 是要测试的网站,在 Demo App 中会使用 H5PayDemoActivity 内的 WebView 打开。 * * 可以填写任一支持支付宝支付的网站(如淘宝或一号店),在网站中下订单并唤起支付宝; * 或者直接填写由支付宝文档提供的“网站 Demo”生成的订单地址 * (如 https://mclient.alipay.com/h5Continue.htm?h5_route_token=303ff0894cd4dccf591b089761dexxxx) * 进行测试。 * * H5PayDemoActivity 中的 MyWebViewClient.shouldOverrideUrlLoading() 实现了拦截 URL 唤起支付宝, * 可以参考它实现自定义的 URL 拦截逻辑。 */ String url = "http://www.samuelnotes.cn"; extras.putString("url", url); intent.putExtras(extras); startActivity(intent); }}
这里就不多说了,关键地方也就这几行
PayTask alipay = new PayTask(PayDemoActivity.this); Map<String, String> result = alipay.payV2(orderInfo, true); /// 调用SDK发起支付后 会在Handler里边进行接收,处理。 9000 支付成功
3.2 沙箱支付环境的接口调用
由于各种开发环境的需要,支付宝还有沙箱支付环境。 调用方法一样, 注意添加
/// 测试沙箱环境 上线必须注释 !!!! EnvUtils.setEnv(EnvUtils.EnvEnum.SANDBOX);
这里注意了 首先是测试端只支持Android端, 必须使用沙箱环境下的APP_ID , 特殊的沙箱环境支付钱包,账号, 密码等。
下载沙箱版的支付钱包需要支付宝扫码登录,界面就不再放了。 注意可以在后台设置沙箱登录账号,密码,充值。
再放一张图吧, 看到余额是不是很惊喜的样子, 这只是沙箱。
4. Server Api
沙箱调通之后端上基本上就没什么事情了, 等测试正式环境,记得把标注沙箱的那段代码注释即可。 接下来我们看一下Server端,先来张图
如果认真看官方Demo 就可以发现我们发起的orderinfo 的orderparams 和签名 都在客户端写得,莫名的脑袋一胀(这样安全吗)支付宝早想到了这里。 作为client只需要简单的把参数传给我们的server就行了。 下面我们来看一下server端的配置方法和处理逻辑。
4.1 JAVA服务端SDK生成APP支付订单
//实例化客户端 AlipayClient alipayClient = new DefaultAlipayClient("https://openapi.alipay.com/gateway.do", APP_ID, APP_PRIVATE_KEY, "json", CHARSET, ALIPAY_PUBLIC_KEY, "RSA2"); //实例化具体API对应的request类,类名称和接口名称对应,当前调用接口名称:alipay.trade.app.pay //创建请求对象 AlipayTradeAppPayRequest request = new AlipayTradeAppPayRequest(); model.setBody("我是测试数据"); //SDK已经封装掉了公共参数,这里只需要传入业务参数。以下方法为sdk的model入参方式(model和biz_content同时存在的情况下取biz_content)。 AlipayTradeAppPayModel model = new AlipayTradeAppPayModel(); AlipayTradeAppPayResponse response = alipayClient.sdkExecute(request); model.setSubject("App支付测试Java");model.setOutTradeNo(outtradeno); model.setTimeoutExpress("30m"); model.setTotalAmount("0.01"); model.setProductCode("QUICK_MSECURITY_PAY"); request.setBizModel(model);///request.setNotifyUrl("商户外网可以访问的异步地址"); }try { //这里和普通的接口调用不同,使用的是sdkExecute System.out.println(response.getBody());//就是orderString 可以直接给客户端请求,无需再做处理。 } catch (AlipayApiException e) {e.printStackTrace(); }
整个调用很简单, 因为大部分内容SDK已经帮我们做完了, 下载包里有源码,有兴趣的可以找找的看 肯定会有和端上签名一样的方法。 这这里注意的地方有request.setNotifyUrl("商户外网可以访问的异步地址"); 放置的参数是我们Server端暴露给支付宝回调的异步地址。
4.2 验证异步通知信息参数示例
我们知道在端上调用支付成功之后,支付宝会同步返回的数据,我们可以再端上根据RAS 生成的公钥进行验证支付结果。但有些时候会出现商户app在支付宝付款阶段被关闭(比如断网)导致无法正确收到同步结果,此时支付结果可以完全依赖服务端的异步通知。(当然这里对于用户来说,在离线状态下,支付宝会发短信告诉账户被扣款) 我们要做的当然是服务端进行验证支付宝给我们发送的支付结果请求,我们需要进行验证,然后再做处理。
以上代码很容易理解, 注意 传的publishkey是公钥, 还有签名类型。 如果乱码了 可以解注释使用(现成的,可以说很贴心了有木有)。
5. 整体流程分析
上边介绍完了端上和Server的流程和方法。 下边我们来看一下整体流图。我们引用了官网的一张整体的sequence图
看完图之后是不是有种高屋建瓴的感觉。 从用户的操作到APP调用我们Server生成订单,调起支付宝SDK, 以及支付宝服务端与我们server 的异步支付通知,一目了然。没什么说的, 不懂得评论区留言。
6. 注意事项
构造交易数据并签名必须在商户服务端完成,商户的应用私钥绝对不能保存在商户APP客户端中,也不能从服务端下发。
同步返回的数据,只是一个简单的结果通知,商户确定该笔交易付款是否成功需要依赖服务端收到支付宝异步通知的结果进行判断。
商户系统接收到通知以后,必须通过验签(验证通知中的sign参数)来确保支付通知是由支付宝发送的。建议使用支付宝提供的SDK来完成,详细验签规则参考异步通知验签。
支付宝支付的最小单位是1分, 格式是0.01 ,使用的是String格式。 这个和微信不一样,注意区分。
注意沙箱环境和真实环境,以及沙箱账号和各个参数的正确配置。
7. 总结
当时在集成时候测试一般使用的是一分,两分等,但是实际APP中支付额度要远远大的多,例如9800, 12650 , 这就牵涉到第三方银行的当日支付额度问题(上线后用户提我们支付有问题,支付不了,一帮人脸吓黑了),可以提前告知,或者贴各个银行的支付额度给用户。
自己集成的Demo 地址: https://github.com/samuelhehe/PayDemo 记得换成自己的包名,参数。