移动端集成支付,似乎是每个App都可能面临的一件事。所有项目都在谈盈利模式,而从C端获取现金流是盈利中最重要的一个途径之一。
当前大家主要采用微信支付和阿里支付集成到自己的移动应用,虽然官方提供一些文档和Demo,但是文档的不完整性,信息分散,集成的时候,依然会有很多困惑。本文从Android的Client集成,到服务器集成对阿里支付集成做介绍。
从网上找到的文章里,大多数只说了如何集成客户端,但是很少提及服务器。虽然如此,服务器集成是一定要做的。如果仅凭客服端返的数据就更改订单状态是极为危险的,因为任何人都可以通过技术手段伪造数据,发数据给服务器修改订单状态,会造成极大的经济损失。并且强烈建议,订单状态的修改不要依赖客户端,客户端的返回,可以作为校验辅助。
下面是集成阿里支付的数据交互流,可以在这里找到:
https://doc.open.alipay.com/doc2/detail?treeId=59&articleId=103563&docType=1
用户在客户端发起支付后,打开支付宝。需要解释的一点是,无论是完成支付,还是按掉叉叉取消支付,阿里支付的服务器端都会给应用服务器(商户服务器,“我们的服务器”)同步一条数据,通知服务器用户的详细支付结果。所以客户端集成和服务器集成都必不可少。
本文分别从Android客户端和Java服务器端集成,介绍集成阿里支付的详细过程。
其中,Android端支付,主要参考了官方Demo和博文:
http://my.oschina.net/daniels/blog/597356?fromerr=CxXSKO5S
服务器端集成主要是在官方Demo基础上自己实践,服务器端可运行代码在我的Github上
https://github.com/hopeztm7500/AlipayServerDemo
1. RSA算法概述
如果不先说点关于数据校验到事情,肯定对配置支付环境出现的各种密钥十分困惑。在数据流图中,我们发现有两次跨应用的数据交互,前端我们吊起了支付宝,而支付宝发送同步数据给应用服务器。特别是服务器同步数据,原理再简单不过,就是一个POST请求,用Shell严格仿照格式任何程序员都能构造出来,那么应用服务器如何辨别这条数据是来自支付宝服务器而不是伪造的呢。这里就涉及到数据签名校验。
简单可以理解RSA校验按照如下到方式工作:
(1)私钥用来进行解密和签名,是给自己用的。
(2)公钥由本人公开,用于加密和验证签名,是给别人用的。
(3)当该用户发送文件时,用私钥签名,别人用他给的公钥验证签名,可以保证该信息是由他发送的。当该用户接受文件时,别人用他的公钥加密,他用私钥解密,可以保证该信息只能由他接收到。
所以在两处数据交互的地方,我们都用到了RSA算法,在前端调用支付宝的时候,我们自己生成一对公钥和私钥(商户公钥和私钥),公钥我们要填写到支付宝后台,而私钥要自己写在客户端程序中。这里我推荐把私钥存储在SO文件中,防止被反编译获取利用。
而服务器交互数据的时候,支付宝服务器用自己持有的私钥对数据做签名,应用服务器使用支付宝提供的公钥对数据做校验。所以在集成中,你会用在应用客户端使用商户私钥,在支付宝后台配置商户公钥,在应用服务器使用支付宝公钥。
2. Android 客户端集成
目录[-]
- 一、申请移动支付权限
- 二、阿里支付DEMO
- 1、概述
- (1)支付调用页面及测试
- (2)客户端与服务器
- 2、配置几个变量
- (1)PID
- (2)、APPID、APP SECRET和支付宝公钥
- (3)、生成商户私钥【windows生成方法】
- (4)、生成商户私钥【MAC生成方法】
- (5)、生成用户公钥及网页填充
- 3、配置DEMO
- 4、代码讲解
- 第一步:构造定单信息:
- 第二步:对订单字符串做RSA签名
- 第三步: 构造完成的请求字符串
- 第四步:请求与结果返回
一、申请移动支付权限
首先登录【支付宝开放平台】http://open.alipay.com/platform/home.htm,添加应用,申请移动支付权限。申请开通支付,是需要公司文件的,个人是不允许开始支付的。
具体细节就不再详聊了,下面就讲讲如何将阿里给出的demo运行起来。
二、阿里支付DEMO
1、概述
(1)支付调用页面及测试
支付宝在调用时,会首先看本地是不是存在支付宝客户端,如果有的话,就直接调用客户端进行支付,如果没有,则调用jar包中的H5页面支付。
所以在测试时,需要有测试两种情境:有支付宝客户端和没有支付宝客户端的情况。
(2)、客户端与服务器
在demo中大家可以看到,有客户端的demo也有服务端的demo,大家可能觉得需要服务端写好之后,客户端才能集成,其实并不是。整个流程是这样的:
1,APP客户端通过SDK发送支付请求 (客户端处理)
2,SDK支付成功并同步返回支付结果(客户端处理)
3,支付宝服务器向我们的服务器发送支付结果字符串(服务端处理)
客户端:从上面的流程可以看出,服务端只是用来接出异步返回的支付结果的。而支付与同步结果返回都是在客户端可以直接看得到的。所以在集成支付宝支付接口时,主要功能是在客户端,即便服务端没有做集成,也是可能付款成功的。
服务端:服务端只需要添加一个功能:接口支付结果返回
下面几张图显示了整个demo的运行过程,由于没办法在真机上录制gif,所以只能用图片来代替了。
初始化界面:
点击支付后,跳出确认付款界面:
点击确认付款后,跳出输入密码界面:
最后是支付成功界面:
在看DEMO的代码之前,我们需要先配置几个变量:
2、配置几个变量
这部分会对代码中用到的几个变量的找到方法或生成方法进行讲述,部分资料引自支付宝开放平台。
(1)PID
合作者身份ID(PID)是商户与支付宝签约后,商户获得的支付宝商户唯一识别码。当商户把支付宝功能接入商户网站时会用到PID,以便让支付宝认证商户。
查看PID步骤如下:
1、登录支付宝官方网站b.alipay.com
2、点击导航栏中“商家服务”
3、点击“查询PID、Key”
(2)、APPID、APP SECRET和支付宝公钥
在https://openhome.alipay.com/platform/createApp.htm页面,创建一个应用
完成之后:在我的应用中是可以看得到的:
然后转到帐户基本信息页面:https://openhome.alipay.com/platform/keyManage.htm
在开放平台密钥栏,可以找到APPID,APP SECRET,和支付宝密钥
这三个数据,都是在应用创建后,支付宝为我们生成好的,无法更改!
(3)、生成商户私钥【windows生成方法】
(有关mac的生成方法,下面会再补充)
1、下载DEMO及SDK
到文档中心,查看移动支付对应的文档,文档地址:http://doc.open.alipay.com/doc2/detail?treeId=59&articleId=103563&docType=1
然后,点击(SDK&DEMO下载)下载代码
2、得到原始私钥
在代码中的DEMO/openssl/bin目录下,有openssl.exe文件
打开openssl.exe
输入
genrsa -out rsa_private_key.pem 1024
此时,我们可以在bin文件夹中看到一个文件名为rsa_private_key.pem的文件
用记事本方式打开它,可以看到—–BEGIN RSA PRIVATE KEY—–开头,—–END RSA PRIVATE KEY—–结尾的没有换行的字符串,这个就是原始的私钥。
但这段原始私钥代码中是用不到的,我们需要将它转化为PKCS8格式
3、转换为PKCS8格式
在openssl.exe中输入:并回车
pkcs8 -topk8 -inform PEM -in rsa_private_key.pem -outform PEM -nocrypt
得到生成功的结果,这个结果就是PKCS8格式的私钥,如下图:
注意,私钥是红框包括的那部分,是不包含BEGIN PRIVATE KEY和END PRIVATE KEY这两行的。
右键点击openssl窗口上边边缘,选择编辑→标记,选中要复制的文字(如上图),
此时继续右键点击openssl窗口上边边缘,选择编辑→复制,
把复制的内容粘土进一个新的记事本中,可随便命名,只要知道这个是PKCS8格式的私钥即可。
(4)、生成商户私钥【MAC生成方法】
这里来讲一下mac端如何生成用户私钥的,由于mac系统是自带openssl的,所以只需要打开终端,利用cd 命令切到任意一个想存放生成Key的文件夹下:
比如,切到下载目录下
然后运行下面的命令来生成私钥原始密钥
openssl genrsa -out rsa_private_key.pem 1024
然后运行下面的命令来生成转换的PCKS8格式的命令。
openssl pkcs8 -topk8 -inform PEM -in rsa_private_key.pem -outform PEM -nocrypt
然后将生成的私钥复制保存起来。
从上面的命令可以看出,与windows相比,mac上需要在前面添加openssl指定运行的是openssl命令。其它命令是完全一致的。
(5)、生成用户公钥及网页填充
1、生成公钥
同样对于windows用户而言,直接在openssl.exe中输入下面的命令:
rsa -in rsa_private_key.pem -pubout -out rsa_public_key.pem
同样,如果是Mac的同学,输入的命令应该是如下:
openssl rsa -in rsa_private_key.pem -pubout -out rsa_public_key.pem
此时,我们可以在bin文件夹中看到一个文件名为rsa_public_key.pem的文件,用记事本方式打开它,可以看到—–BEGIN PUBLIC KEY—–开头,
—–END PUBLIC KEY—–结尾的没有换行的字符串,这个就是公钥。
在生成网页以后,复制—-BEGIN PUBLIC KEY—–和—–END PUBLIC KEY—–之间的部分,即那段纯代码,不要把—-BEGIN PUBLIC KEY—–和—–END PUBLIC KEY—–给复制进去了。中间的这部分就是公钥。
2、网页填充
然后到https://openhome.alipay.com/platform/keyManage.htm?keyType=partner(需要登录)中,左侧找到合作伙伴密钥栏,再到右侧的RSA加密中,将公钥粘贴进去。由于,我们已经粘贴进去了,所以这里显示查看开发者公钥,在没填之前写的是“添加开发者公钥”
到这里,所有的准备工作都已经结束了。下面就是配置DEMO的过程了
3、配置DEMO
在刚才下载的sdk&demo的源码中,打开DEMO/客户端demo/支付宝Android 15.0.1/alipay_demo工程
路径如下:
在PayDemoActivity中配置几个变量:
//PID
public static final String PARTNER = "";
在这里填上我们上面找到的PID;
// 商户收款账号
public static final String SELLER = "76949XXXX@qq.com";
然后在SELLER上写上我们支付宝的登录帐户,即那个你申请移动支付的支付宝账号
// 支付宝公钥
public static final String RSA_PUBLIC ="";
然后在RSA_PUBLIC这里填上支付宝公钥
// 商户私钥,pkcs8格式
public static final String RSA_PRIVATE = "";
最后是填上RSA_PRIVATE对应的商户私钥,注意是PKCS8格式的。
私钥这部分,注意是—-BEGIN PUBLIC KEY—–和—–END PUBLIC KEY—–之间的部分,即那段纯代码,不要把—-BEGIN PUBLIC KEY—–和—–END PUBLIC KEY—–给复制进去了。中间的这部分就是公钥。
现在运行demo就直接可以支付了。
本文中对应的DEMO在文章底部给出。
4、代码讲解
通过上面的配置,demo应该就直接可以运行了,但这里所涉及的代码,我们再仔细看看
主要的支付与结果返回就是pay()这个函数,这里完成了支付所需要的所有功能。代码如下:
public void pay(View v) {
…………
// 订单信息
String orderInfo = getOrderInfo("测试的商品", "该测试商品的详细描述", "0.01");
// 对订单做RSA 签名
String sign = sign(orderInfo);
try {
// 仅需对sign 做URL编码
sign = URLEncoder.encode(sign, "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
// 完整的符合支付宝参数规范的订单信息
final String payInfo = orderInfo + "&sign=\"" + sign + "\"&"
+ getSignType();
Runnable payRunnable = new Runnable() {
@Override
public void run() {
// 构造PayTask 对象
PayTask alipay = new PayTask(PayDemoActivity.this);
// 调用支付接口,获取支付结果
String result = alipay.pay(payInfo);
Message msg = new Message();
msg.what = SDK_PAY_FLAG;
msg.obj = result;
mHandler.sendMessage(msg);
}
};
// 必须异步调用
Thread payThread = new Thread(payRunnable);
payThread.start();
}
这里总是分了四步来完成支付与结果接收。
第一步:构造定单信息:
String orderInfo = getOrderInfo("测试的商品", "该测试商品的详细描述", "0.01");
主要是这句,即在getOrderInfo()函数中完成定单信息的构造:(这里对getOrderInfo函数做的精减,更多字段及意义参考源码)
有关paymethod的方法使用,参考:https://cshall.alipay.com/support/help_detail.htm?help_id=476935
各个字段的意义及取值参考:http://doc.open.alipay.com/doc2/detail?treeId=59&articleId=103663&docType=1
public String getOrderInfo(String subject, String body, String price) {
// 签约合作者身份ID
String orderInfo = "partner=" + "\"" + PARTNER + "\"";
// 签约卖家支付宝账号
orderInfo += "&seller_id=" + "\"" + SELLER + "\"";
// 商户网站唯一订单号
orderInfo += "&out_trade_no=" + "\"" + getOutTradeNo() + "\"";
// 商品名称
orderInfo += "&subject=" + "\"" + subject + "\"";
// 商品详情
orderInfo += "&body=" + "\"" + body + "\"";
// 商品金额
orderInfo += "&total_fee=" + "\"" + price + "\"";
// 服务器异步通知页面路径
orderInfo += "¬ify_url=" + "\"" + "http://notify.msp.hk/notify.htm"
+ "\"";
…………
return orderInfo;
}
这里就是通过我们的提供的商家ID,产品信息,价格等信息来构造定单及回调页面,这里需要非常注意的一个地方:
// 服务器异步通知页面路径
orderInfo += "&noify_url=" + "\"" + "http://notify.msp.hk/notify.htm"
+ "\"";
服务器异步通知页面路径,首先我们用支付宝支付之后,支付宝会返回给我们两个通知,一个是同步的,就是我们点击支付后支付宝直接反馈给我们客户端的信息,我们可以直接拿到,根据反馈的结果可以初步判定该次交易是否成功,第二个就是服务器异步的通知,这个异步的通知是支付宝的服务器端发给我们服务器端的信息,我们在客户端是直接获取不了的,那支付宝的服务器怎么知道我们服务器的路径呢,那就是这参数的作用了,我们给支付宝服务器一个路径,它就会在订单状态改变的时候给我们服务器端一个反馈,告诉服务器这次交易的状态,如果服务器结果判定该次交易成功了,就必须返给支付宝服务器一个success,要不服务器会一直给我们异步通知,因为它不知道该次交易是否完成了(一般情况下25小时内8次通知,频率一般是2m 10m 10m 1h 2h 6h 15h),我们一般会在收到异步通知时,对订单的状态进行更新。
其它的就不讲了,通过看源码都能看得懂,比如构造订单号啥的。
第二步:对订单字符串做RSA签名
为什么要签名呢?当然是防止传输出错了,这可是跟钱相关的,如果orderInfo传输过程中出错了,那怎么样来校验它是不是出错了呢,只有通过签名算法来了。所以这里就需要对订单字符串做签名。
具体签名算法就不讲了,直接应用到项目中就行,不需要理解,如果想看看怎么实现的,里面有对应的源码,可以去研究一下。
// 对订单做RSA 签名
String sign = sign(orderInfo);
try {
// 仅需对sign 做URL编码
sign = URLEncoder.encode(sign, "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
第三步:构造完成的请求字符串
在订单字符串和签名做完以后,就可以用他们来构造完整的请求字符串了:
// 完整的符合支付宝参数规范的订单信息
final String payInfo = orderInfo + "&sign=\"" + sign + "\"&"
+ getSignType();
第四步:请求与结果返回
最后是发送请求,代码如下:
Runnable payRunnable = new Runnable() {
@Override
public void run() {
// 构造PayTask 对象
PayTask alipay = new PayTask(PayDemoActivity.this);
// 调用支付接口,获取支付结果
String result = alipay.pay(payInfo);
Message msg = new Message();
msg.what = SDK_PAY_FLAG;
msg.obj = result;
mHandler.sendMessage(msg);
}
};
// 必须异步调用
Thread payThread = new Thread(payRunnable);
payThread.start();
最关键的部分在这里:
PayTask alipay = new PayTask(PayDemoActivity.this);
// 调用支付接口,获取支付结果
String result = alipay.pay(payInfo);
Message msg = new Message();
msg.what = SDK_PAY_FLAG;
msg.obj = result;
mHandler.sendMessage(msg);
在String result = alipay.pay(payInfo);中,就直接获得了支付结果;
然后通过handler将结果发送出去。
3. 服务器集成
集成了客户端,工作才刚刚完成一半,支付宝客户端返回了支付结果后,应用服务器的数据库中,订单的状态并没有改变,如果依赖客户端通知服务器改变订单状态,那是十分危险的,就好像刚刚说过的一样,任何程序员经过一定的努力,都可能发送数据给你的应用服务器,改变你订单的状态,这样,即使没有支付,订单的状态也会变成已支付,所以,订单状态的改变,一定要依赖服务器的数据交互。
那么当前端完成支付以后,数据如何交互呢?notify_url !
还记得前端构造的订单数据中的notify_url么,支付宝服务器就是利用这个配置,将服务器数据POST到这个地址上的,所以在进行服务器集成之前,你要保证你的服务器已经能够接收到请求。
一份完整的代码可以参考:
https://github.com/hopeztm7500/AlipayServerDemo
1. 服务器同步的订单数据
支付宝服务器发送给我们的数据,可以用如下的Bean来描述。
package com.wenxi.alipay.bean;
import java.io.Serializable;
import java.util.Date;
import com.alibaba.fastjson.annotation.JSONField;
public class AlipayNotification implements Serializable{
/**
*
*/
private static final long serialVersionUID = -8638199167144867399L;
private Integer alipayNoticeId;
private String notifyId;
private String notifyType;
@JSONField(format="yyyy-MM-dd HH:mm:ss")
private Date notifyTime;
private String signType;
private String sign;
private String outTradeNo;
private String subject;
private String paymentType;
private String tradeNo;
private String tradeStatus;
private String sellerId;
private String sellerEmail;
private String buyerId;
private String buyerEmail;
private Double totalFee;
private Integer quantity;
private Double price;
private String body;
@JSONField(format="yyyy-MM-dd HH:mm:ss")
private Date gmtCreate;
@JSONField(format="yyyy-MM-dd HH:mm:ss")
private Date gmtPayment;
private String isTotalFeeAdjust;
private String userCoupon;
private String discount;
private String refundStatus;
@JSONField(format="yyyy-MM-dd HH:mm:ss")
private Date gmtRefund;
private Boolean verifyResult;
public String getNotifyId() {
return notifyId;
}
public void setNotifyId(String notifyId) {
this.notifyId = notifyId == null ? null : notifyId.trim();
}
public String getNotifyType() {
return notifyType;
}
public void setNotifyType(String notifyType) {
this.notifyType = notifyType == null ? null : notifyType.trim();
}
public Date getNotifyTime() {
return notifyTime;
}
public void setNotifyTime(Date notifyTime) {
this.notifyTime = notifyTime;
}
public String getSignType() {
return signType;
}
public void setSignType(String signType) {
this.signType = signType == null ? null : signType.trim();
}
public String getSign() {
return sign;
}
public void setSign(String sign) {
this.sign = sign == null ? null : sign.trim();
}
public String getOutTradeNo() {
return outTradeNo;
}
public void setOutTradeNo(String outTradeNo) {
this.outTradeNo = outTradeNo == null ? null : outTradeNo.trim();
}
public String getSubject() {
return subject;
}
public void setSubject(String subject) {
this.subject = subject == null ? null : subject.trim();
}
public String getPaymentType() {
return paymentType;
}
public void setPaymentType(String paymentType) {
this.paymentType = paymentType == null ? null : paymentType.trim();
}
public String getTradeNo() {
return tradeNo;
}
public void setTradeNo(String tradeNo) {
this.tradeNo = tradeNo == null ? null : tradeNo.trim();
}
public String getTradeStatus() {
return tradeStatus;
}
public void setTradeStatus(String tradeStatus) {
this.tradeStatus = tradeStatus == null ? null : tradeStatus.trim();
}
public String getSellerId() {
return sellerId;
}
public void setSellerId(String sellerId) {
this.sellerId = sellerId == null ? null : sellerId.trim();
}
public String getSellerEmail() {
return sellerEmail;
}
public void setSellerEmail(String sellerEmail) {
this.sellerEmail = sellerEmail == null ? null : sellerEmail.trim();
}
public String getBuyerId() {
return buyerId;
}
public void setBuyerId(String buyerId) {
this.buyerId = buyerId == null ? null : buyerId.trim();
}
public String getBuyerEmail() {
return buyerEmail;
}
public void setBuyerEmail(String buyerEmail) {
this.buyerEmail = buyerEmail == null ? null : buyerEmail.trim();
}
public Double getTotalFee() {
return totalFee;
}
public void setTotalFee(Double totalFee) {
this.totalFee = totalFee;
}
public Integer getQuantity() {
return quantity;
}
public void setQuantity(Integer quantity) {
this.quantity = quantity;
}
public Double getPrice() {
return price;
}
public void setPrice(Double price) {
this.price = price;
}
public String getBody() {
return body;
}
public void setBody(String body) {
this.body = body == null ? null : body.trim();
}
public Date getGmtCreate() {
return gmtCreate;
}
public void setGmtCreate(Date gmtCreate) {
this.gmtCreate = gmtCreate;
}
public Date getGmtPayment() {
return gmtPayment;
}
public void setGmtPayment(Date gmtPayment) {
this.gmtPayment = gmtPayment;
}
public String getIsTotalFeeAdjust() {
return isTotalFeeAdjust;
}
public void setIsTotalFeeAdjust(String isTotalFeeAdjust) {
this.isTotalFeeAdjust = isTotalFeeAdjust == null ? null : isTotalFeeAdjust.trim();
}
public String getUserCoupon() {
return userCoupon;
}
public void setUserCoupon(String userCoupon) {
this.userCoupon = userCoupon == null ? null : userCoupon.trim();
}
public String getDiscount() {
return discount;
}
public void setDiscount(String discount) {
this.discount = discount == null ? null : discount.trim();
}
public String getRefundStatus() {
return refundStatus;
}
public void setRefundStatus(String refundStatus) {
this.refundStatus = refundStatus == null ? null : refundStatus.trim();
}
public Date getGmtRefund() {
return gmtRefund;
}
public void setGmtRefund(Date gmtRefund) {
this.gmtRefund = gmtRefund;
}
public Integer getAlipayNoticeId() {
return alipayNoticeId;
}
public void setAlipayNoticeId(Integer alipayNoticeId) {
this.alipayNoticeId = alipayNoticeId;
}
public Boolean getVerifyResult() {
return verifyResult;
}
public void setVerifyResult(Boolean verifyResult) {
this.verifyResult = verifyResult;
}
}
当然获取这些数据需要一些额外的工作,首先它们存在Request对象中,其次它们是underscore case,所以Sample代码中,先把这些数据用
AsynAlipayNotifyController
接收到,并且转化成我们能够处理的Bean格式。
其中,outTradeNo可能我们最关心的业务数据,是应用服务器中的订单ID,状态的改变几乎全凭它。
2. 数据校验
数据校验的代码,基本上就是支付宝Java服务器Demo中的代码,需要配置的只有PID,代码中已经提供了支付宝公钥,这里不需要修改。
数据校验的核心代码在这里:
public static boolean verify(Map<String, String> params) { // 判断responsetTxt是否为true,isSign是否为true // responsetTxt的结果不是true,与服务器设置问题、合作身份者ID、notify_id一分钟失效有关 // isSign不是true,与安全校验码、请求时的参数格式(如:带自定义参数等)、编码格式有关 String responseTxt = "false"; if (params.get("notify_id") != null) { String notify_id = params.get("notify_id"); responseTxt = verifyResponse(notify_id); } String sign = ""; if (params.get("sign") != null) { sign = params.get("sign"); } boolean isSign = getSignVeryfy(params, sign); if (isSign && responseTxt.equals("true")) { return true; } else { return false; } }
首先校验通知ID,这个ID的是有有效期的,所以如果用测试数据POST到服务器,可能还要注意ID是不是过期,然后就是签名校验,签名校验使用了支付宝公钥。然后根据校验结果,就可以实现相应的业务逻辑了。
写到这里, 第三方支付-支付宝的应用集成,基本就理清了。不过,总结起来,技术都不是难点,如何让用户塞钱到你的支付宝,才是我们的最终目标~ 祝每个集成后的小伙伴,都有现金流进来!