因爲公司項目須要,安排我負責接入微信、支付寶支付功能。從最開始的申請帳號到最後的功能完成,全程參與其中。如今功能完成了,正好寫篇總結文檔。順便寫了個Android端的demo,把整個功能都整合了一下。裏面包括獲取訂單,簽名,驗證,調起支付,支付完成同步回調整個流程。配合總結文檔食用最佳,歡迎star ~php
支付寶接入相對而言比較簡單,按照官方文檔和demo基本沒什麼大問題。先看下支付寶支付的流程圖。前端
若是你已經按照官方教程完成了接入支付寶的準備工做,那麼用申請的appid和生成的公鑰私鑰替換demo中PayConstants
的相關屬性,就能夠直接體驗支付寶支付了。這其中須要注意RSA_PRIVATE 和 RSA2_PRIVATE 都是支付寶私鑰,分別對應着RSA和RSA2簽名算法。 目前新建應用只能使用RSA2簽名算法,老應用仍是可使用RSA簽名算法,不過在調用支付時須要標註是不是使用RSA2簽名算法。java
/**
* RSA_PRIVATE 和支付寶私鑰
*/
public static final String RSA2_PRIVATE = "XXXXXXX";
/**
* 支付寶APP_ID
*/
public static final String ALI_APP_ID = "2018083061113973";
複製代碼
下載最新版的支付SDK,將其複製到你項目的libs文件夾中。在項目的build.gradle中添加以下代碼:android
allprojects {
repositories {
// 添加下面的內容
flatDir {
dirs 'libs'
}
// ... jcenter() 等其餘倉庫
}
}
複製代碼
在APP的build.gradle中,添加支付SDK的aar包依賴:git
compile (name: 'alipaySdk-15.5.9-20181123210601', ext: 'aar')
複製代碼
在AndroidManifest中添加如下權限:github
<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.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
複製代碼
以上就完成了接入SDK的工做。這其中須要注意,WRITE_EXTERNAL_STORAGE和READ_PHONE_STATE權限須要在代碼中動態獲取。算法
按照接入流程圖所示,第一步是先獲取簽名訂單。爲了防止應用的支付寶私鑰暴露,這一步都是在服務器中完成。demo中爲了方便方便體驗,直接寫在了客戶端中。若是是開發線上APP,不建議這麼作。支付寶獲取簽名訂單須要如下幾個步驟:express
按照官方的請求參數說明,demo中的代碼示例以下:api
/**
* 生成map格式訂單
*
* @param app_id 支付平臺中你APP的app_id
* @param rsa2 是不是使用RSA2簽名算法
* @param payBean 包括訂單金額,訂單描述,訂單詳細內容,建立訂單時間戳
* @param version 接入的支付寶SDK版本號
* @return
*/
public static Map<String, String> buildOrderParamMap(String app_id, boolean rsa2, PayBean payBean, String version) {
Map<String, String> keyValues = new HashMap<>();
//支付寶的amount單位爲元
float amount = (payBean.getAmount()) / 100;
keyValues.put("app_id", app_id);
/**
* biz_content參數包括全部的訂單信息
*/
keyValues.put("biz_content", "{\"timeout_express\":\"30m\",\"product_code\":\"QUICK_MSECURITY_PAY\",\"total_amount\":\"" + amount + "\",\"subject\":\"" + payBean.getBody() + "\",\"body\":\"我是測試數據\",\"out_trade_no\":\"" + getOutTradeNo() + "\"}");
keyValues.put("charset", "utf-8");
keyValues.put("method", "alipay.trade.app.pay");
keyValues.put("sign_type", rsa2 ? "RSA2" : "RSA");
keyValues.put("notify_url",payBean.getNotify_url());
keyValues.put("timestamp", getDateToString(payBean.getTime(), "yyyy-MM-dd HH:mm:ss"));
keyValues.put("version", version);
return keyValues;
}
/**
* 將map轉換爲string,構造原始支付訂單參數信息
*
* @param map 支付訂單參數
* @return
*/
public static String buildOrderParam(Map<String, String> map) {
List<String> keys = new ArrayList<String>(map.keySet());
StringBuilder sb = new StringBuilder();
for (int i = 0; i < keys.size() - 1; i++) {
String key = keys.get(i);
String value = map.get(key);
sb.append(buildKeyValue(key, value, true));
sb.append("&");
}
String tailKey = keys.get(keys.size() - 1);
String tailValue = map.get(tailKey);
sb.append(buildKeyValue(tailKey, tailValue, true));
return sb.toString();
}
複製代碼
先根據用戶支付的訂單參數,如訂單價格、訂單描述等,構建原始map格式訂單。這其中須要注意,全部的訂單參數(total_amount,timeout_express,subject)都一塊兒存入biz_content中。而後將map格式原始訂單參數,進行排序,轉換爲string格式。這其中還須要傳入一個notify_url參數,後面會用到。安全
/**
* 將訂單信息簽名,encode
*
* @param map 待簽名的訂單信息
* @param rsaKey 支付寶私鑰
* @param rsa2 是否使用RSA2簽名算法
* @return
*/
public static String getSign(Map<String, String> map, String rsaKey, boolean rsa2) {
List<String> keys = new ArrayList<String>(map.keySet());
// key排序
Collections.sort(keys);
StringBuilder authInfo = new StringBuilder();
for (int i = 0; i < keys.size() - 1; i++) {
String key = keys.get(i);
String value = map.get(key);
authInfo.append(buildKeyValue(key, value, false));
authInfo.append("&");
}
String tailKey = keys.get(keys.size() - 1);
String tailValue = map.get(tailKey);
//簽名的數據須要沒有通過encode處理
authInfo.append(buildKeyValue(tailKey, tailValue, false));
//將排序後的序列進行簽名
String oriSign = sign(authInfo.toString(), rsaKey, rsa2);
String encodedSign = "";
try {
encodedSign = URLEncoder.encode(oriSign, "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return "sign=" + encodedSign;
}
/**
* 簽名
*
* @param content 待簽名信息
* @param privateKey 支付寶私鑰
* @param rsa2 是否使用RSA2簽名算法
* @return
*/
public static String sign(String content, String privateKey, boolean rsa2) {
try {
PKCS8EncodedKeySpec priPKCS8 = new PKCS8EncodedKeySpec(
Base64.decode(privateKey));
KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM, "BC");
PrivateKey priKey = keyFactory.generatePrivate(priPKCS8);
java.security.Signature signature = java.security.Signature
.getInstance(getAlgorithms(rsa2));
signature.initSign(priKey);
signature.update(content.getBytes(DEFAULT_CHARSET));
byte[] signed = signature.sign();
return Base64.encode(signed);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
複製代碼
而後將排序後的訂單信息進行簽名和encode,獲取最終請求支付的字符串。
支付寶請求支付,只須要調用alipay.payV2方法。
Runnable payRunnable = new Runnable() {
@Override
public void run() {
PayTask alipay = new PayTask(MainActivity.this);
String orderInfo = AliPayUtil.getKidoOrderInfo(getPayBean(),alipay.getVersion());
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();
複製代碼
其中,同步支付結果經過message傳遞出來。這其中須要注意,支付結果必須經過服務器端返回的異步結果來進行最終確認。不能僅根據本地結果來顯示給用戶。
@SuppressLint("HandlerLeak")
private Handler mHandler = new Handler() {
@SuppressWarnings("unused")
@Override
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")) {
// 該筆訂單是否真實支付成功,須要依賴服務端的異步通知。
paymentSucceed();
} else {
// 該筆訂單真實的支付結果,須要依賴服務端的異步通知。
paymentFailed();
}
break;
}
default:
break;
}
}
};
複製代碼
前面傳入的支付參數中,有一個notify_url。支付寶服務器會把異步支付結果經過調用這個notify_url來返回給咱們服務器,而後經過服務器返回給APP,APP經過與本地結果對比,來反饋給用戶支付結果。
以上,整個支付流程就走完了。
微信支付接入起來稍微有點複雜。官方文檔上面也有相應介紹。先看下官方流程圖。
總結下就是:
首先在官網上下載SDK,並將其複製到libs文件夾中。在APP的build.gradle中添加以下代碼(通常Android studio會自動生成這段代碼):
implementation fileTree(dir: 'libs', include: ['*.jar'])
複製代碼
微信支付的本地結果回調,須要在項目中新建wxapi文件夾,在其中添加WXPayEntryActivity,並繼承IWXAPIEventHandler接口。當支付完成後,結果會同步到WXPayEntryActivity的onResp(BaseResp baseResp)方法中。
public class WXPayEntryActivity extends Activity implements IWXAPIEventHandler {
private IWXAPI api;
private static WXPayListenter mListener;
public static void setmListener(WXPayListenter listener) {
mListener = listener;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
api = WXAPIFactory.createWXAPI(this, PayConstants.WX_APP_ID, false);
try {
api.handleIntent(getIntent(), this);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
setIntent(intent);
api.handleIntent(intent, this);
}
@Override
public void onReq(BaseReq baseReq) {
}
@Override
public void onResp(BaseResp baseResp) {
int result = 0;
//支付結果還須要發送給服務器確認支付狀態
if (baseResp.getType() == ConstantsAPI.COMMAND_PAY_BY_WX) {
if (mListener != null) {
if (baseResp.errCode == 0) {
mListener.paymentSucceed();
} else if (baseResp.errCode == -2) {
mListener.paymentCanceled();
} else {
mListener.paymentFailed();
}
}
finish();
}
}
public interface WXPayListenter {
void paymentSucceed();
void paymentCanceled();
void paymentFailed();
}
}
複製代碼
在AndroidManifest中添加WXPayEntryActivity
<activity
android:name=".wxapi.WXPayEntryActivity"
android:exported="true"
android:launchMode="singleTop" />
複製代碼
以上就完成了接入SDK的工做。若是已經接入過微信分享SDK,那麼下載SDK和依賴這一步就能夠跳過了,只須要再添加WXPayEntryActivity就好了。
自我感受,這一步是最麻煩的。首先咱們須要本地訂單信息進行排序和簽名。
/**
* 生成訂單信息
*
* @param ip 當前手機IP
* @param orderId 當前生成的外部訂單號
* @return
*/
public static SortedMap<String, Object> prepareOrder(String ip, String orderId,PayBean payBean) {
Map<String, Object> oparams = new LinkedHashMap<String, Object>();
oparams.put("appid", PayConstants.WX_APP_ID);// 服務號的應用號
oparams.put("body",payBean.getDescribe());// 商品描述
oparams.put("mch_id", PayConstants.WX_CHD_ID);// 商戶號
oparams.put("nonce_str", CreateNoncestr());// 16隨機字符串(大小寫字母加數字)
oparams.put("out_trade_no", orderId);// 商戶訂單號
oparams.put("total_fee", payBean.getAmount());// 支付金額 單位分 注意:前端負責傳入分
oparams.put("spbill_create_ip", ip);// IP地址
oparams.put("notify_url", payBean.getNotify_url()); // 微信回調地址
oparams.put("trade_type", PayConstants.TRADE_TYPE);// 支付類型 app
return sortMap(oparams);
}
/**
* 對map根據key進行排序 ASCII 順序
*
* @param
* @return
*/
public static SortedMap<String, Object> sortMap(Map<String, Object> map) {
List<Map.Entry<String, Object>> infoIds = new ArrayList<Map.Entry<String, Object>>(
map.entrySet());
// 排序
Collections.sort(infoIds, new Comparator<Map.Entry<String, Object>>() {
@Override
public int compare(Map.Entry<String, Object> o1,
Map.Entry<String, Object> o2) {
// return (o2.getValue() - o1.getValue());//value處理
return (o1.getKey()).toString().compareTo(o2.getKey());
}
});
// 排序後
SortedMap<String, Object> sortmap = new TreeMap<String, Object>();
for (int i = 0; i < infoIds.size(); i++) {
String[] split = infoIds.get(i).toString().split("=");
sortmap.put(split[0], split[1]);
}
return sortmap;
}
/**
* 簽名工具
*
* @param characterEncoding 編碼格式 UTF-8
* @param parameters 請求參數
* @return
*/
public static String createSign(String characterEncoding,
Map<String, Object> parameters) {
StringBuffer sb = new StringBuffer();
Iterator<Map.Entry<String, Object>> it = parameters.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<String, Object> entry = (Map.Entry<String, Object>) it.next();
String key = (String) entry.getKey();
Object value = entry.getValue();//去掉帶sign的項
if (null != value && !"".equals(value) && !"sign".equals(key)
&& !"key".equals(key)) {
sb.append(key + "=" + value + "&");
}
}
sb.append("key=" + PayConstants.WX_KEY);
return MD5Encode(sb.toString(), characterEncoding).toUpperCase();
}
/**
* MD5
*
* @param origin
* @param charsetname
* @return
*/
public static String MD5Encode(String origin, String charsetname) {
String resultString = null;
try {
resultString = new String(origin);
MessageDigest md = MessageDigest.getInstance("MD5");
if (charsetname == null || "".equals(charsetname)) {
resultString = byteArrayToHexString(md.digest(resultString
.getBytes()));
} else {
resultString = byteArrayToHexString(md.digest(resultString
.getBytes(charsetname)));
}
} catch (Exception exception) {
}
return resultString;
}
複製代碼
這一部分,上面代碼中已經備註的很詳細,就不展開解釋了。簽名以後,將本地下單信息進行xml格式化。
/**
* 將請求參數轉換爲xml格式的string
*
* @param parameters 請求參數
* @return
*/
public static String getRequestXml(SortedMap<String, Object> parameters) {
StringBuffer sb = new StringBuffer();
sb.append("<xml>");
Iterator<Map.Entry<String, Object>> iterator = parameters.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, Object> entry = (Map.Entry<String, Object>) iterator.next();
String key = (String) entry.getKey();
String value = (String) entry.getValue();
if ("attach".equalsIgnoreCase(key) || "body".equalsIgnoreCase(key)
|| "sign".equalsIgnoreCase(key)) {
sb.append("<" + key + ">" + "<![CDATA[" + value + "]]></" + key + ">");
} else {
sb.append("<" + key + ">" + value + "</" + key + ">");
}
}
sb.append("</xml>");
return sb.toString();
}
複製代碼
這其中須要注意,attach,body,sign三個參數加了CDATA標籤,用於說明數據不被XML解析器解析。以上咱們就準備好了本地下單信息,能夠直接調用微信統一下單接口,獲取統一下單訂單信息了。
先調用接口獲取統一下單信息。我這裏使用的是最基本的httpsRequest。
/**
* 發送https請求
*
* @param requestUrl 請求地址
* @param requestMethod 請求方式(GET、POST)
* @param outputStr 提交的數據
* @return 返回微信服務器響應的信息
* @throws Exception
*/
public static String httpsRequest(String requestUrl, String requestMethod,
String outputStr) throws Exception {
try {
URL url = new URL(requestUrl);
HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
conn.setDoOutput(true);
conn.setDoInput(true);
conn.setUseCaches(false);
// 設置請求方式(GET/POST)
conn.setRequestMethod(requestMethod);
conn.setRequestProperty("content-type",
"application/x-www-form-urlencoded");
// 當outputStr不爲null時向輸出流寫數據
if (null != outputStr) {
OutputStream outputStream = conn.getOutputStream();
// 注意編碼格式
outputStream.write(outputStr.getBytes("UTF-8"));
outputStream.close();
}
// 從輸入流讀取返回內容
InputStream inputStream = conn.getInputStream();
InputStreamReader inputStreamReader = new InputStreamReader(
inputStream, "UTF-8");
BufferedReader bufferedReader = new BufferedReader(
inputStreamReader);
String str = null;
StringBuffer buffer = new StringBuffer();
while ((str = bufferedReader.readLine()) != null) {
buffer.append(str);
}
// 釋放資源
bufferedReader.close();
inputStreamReader.close();
inputStream.close();
inputStream = null;
conn.disconnect();
return buffer.toString();
} catch (ConnectException ce) {
Log.e(TAG, "鏈接超時:{}" + ce);
throw new RuntimeException("連接異常" + ce);
} catch (Exception e) {
Log.e(TAG, "https請求異常:{}" + e);
throw new RuntimeException("https請求異常" + e);
}
}
複製代碼
服務器返回的下單信息是xml格式,咱們須要先進行格式轉換。
/**
* @param xml
* @return Map
* @description 將xml字符串轉換成map
*/
public static Map<String, Object> readStringXmlOut(String xml) {
Map<String, Object> map = new HashMap<String, Object>();
Document doc = null;
try {
doc = DocumentHelper.parseText(xml); // 將字符串轉爲XML
Element rootElt = doc.getRootElement(); // 獲取根節點
@SuppressWarnings("unchecked")
List<Element> list = rootElt.elements();// 獲取根節點下全部節點
for (Element element : list) { // 遍歷節點
map.put(element.getName(), element.getText()); // 節點的name爲map的key,text爲map的value
}
} catch (DocumentException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
return map;
}
複製代碼
檢驗API返回的數據裏面的簽名是否合法,避免數據在傳輸的過程當中被第三方篡改。
/**
* 檢驗API返回的數據裏面的簽名是否合法,避免數據在傳輸的過程當中被第三方篡改
*
* @param map API返回的下單數據
* @return API簽名是否合法
* @throws
* @throws
* @throws
*/
public static boolean checkIsSignValidFromResponseString(Map<String, Object> map) {
try {
String signFromAPIResponse = map.get("sign").toString();
if ("".equals(signFromAPIResponse) || signFromAPIResponse == null) {
Log.d(TAG, "API返回的數據簽名數據不存在,有可能被第三方篡改!!!");
return false;
}
//清掉返回數據對象裏面的Sign數據(不能把這個數據也加進去進行簽名),而後用簽名算法進行簽名
map.put("sign", "");
//將API返回的數據根據用簽名算法進行計算新的簽名,用來跟API返回的簽名進行比較
String signForAPIResponse = createSign(Charsets.UTF_8.toString(), map);
Log.d(TAG, "服務器回包裏面的簽名是:" + signFromAPIResponse + "==服務器回包數據簽名是:" + signForAPIResponse);
if (!signForAPIResponse.equals(signFromAPIResponse)) {
//簽名驗不過,表示這個API返回的數據有可能已經被篡改了
Log.d(TAG, "API返回的數據簽名驗證不經過,有可能被第三方篡改!!!");
return false;
}
Log.d(TAG, "恭喜,API返回的數據簽名驗證經過!!!");
return true;
} catch (Exception e) {
return false;
}
}
複製代碼
最後,還須要對返回的訂單信息進行整理,來獲得咱們須要的最終調用微信支付所須要的數據。
/**
* 提取微信服務器返回的下單數據,組合成調用微信支付所需的map
* 對map進行排序簽名
*
* @param resultMap
* @return
*/
private static Map<String, Object> getPayResultMap(Map<String, Object> resultMap) {
Map<String, Object> map = new LinkedHashMap<String, Object>();
map.put("appid", resultMap.get("appid"));
map.put("partnerid", resultMap.get("mch_id"));
map.put("prepayid", resultMap.get("prepay_id"));
map.put("package", "Sign=WXPay");
map.put("noncestr", resultMap.get("nonce_str"));
map.put("timestamp", getTimeStamp());
//對map進行排序
SortedMap<String, Object> sortedMap = sortMap(map);
//對map進行簽名,並將簽名加入map
sortedMap.put("sign", createSign(Charsets.UTF_8.toString(), sortedMap));
return sortedMap;
}
複製代碼
這其中須要注意,咱們仍是須要將最終的訂單信息進行從新排序,而後從新簽名。這裏的簽名方式必定要與統一下單接口使用的一致。具體能夠參照官方簽名文檔。
總算是走到這一步了,維信調用支付須要先判斷當前手機是否有安裝維信,維信版本是否支持支付功能。
/**
* 判斷當前手機是否支持維信支付
* @return
*/
private boolean wxCanPay(){
try{
if(!iwxapi.isWXAppInstalled()){
Toast.makeText(MainActivity.this,"請安裝微信客戶端", Toast.LENGTH_SHORT).show();
return false;
}else if(!iwxapi.isWXAppSupportAPI()){
Toast.makeText(MainActivity.this, "當前微信版本不支持支付", Toast.LENGTH_SHORT).show();
return false;
}
}catch (Exception e){
e.printStackTrace();
Toast.makeText(MainActivity.this, "請安裝最新微信客戶端", Toast.LENGTH_SHORT).show();
return false;
}
return true;
}
複製代碼
調用微信支付
Runnable wxPayRunnable = new Runnable() {
@Override
public void run() {
try{
Map<String,Object> orderInfo = WXPayUtil.getWXOrderInfo(getPayBean(),MainActivity.this);
PayReq req = new PayReq();
req.appId = (String)orderInfo.get("appid");
req.partnerId = (String)orderInfo.get("partnerid");
req.prepayId = (String)orderInfo.get("prepayid");
req.nonceStr = (String)orderInfo.get("noncestr");
req.timeStamp = (String)orderInfo.get("timestamp");
req.packageValue = (String)orderInfo.get("package");
req.sign = (String)orderInfo.get("sign");
iwxapi.sendReq(req);
}catch (Exception e ){
e.printStackTrace();
}
}
};
Thread payThread = new Thread(wxPayRunnable);
payThread.start();
複製代碼
按照第一步接入準備中所說,支付結果會返回到WXPayEntryActivity的onResp(BaseResp baseResp)。咱們須要獲取到結果後,finish掉這個activity。而後在支付結果頁面等待服務器返回異步支付結果。
接入兩個支付,收穫仍是有的。設計到支付相關,安全是最重要的。因此簽名加密等全部操做都是在服務器端進行的,最終落實到APP端,也只是寫個界面,調個接口而已。可是相關的流程和邏輯仍是有必要理一理,總結一下的。最後再附上demo的地址,歡迎你們star