最近閑來無事,看到網上有一些免簽支付回調的服務商,當時感覺很新奇,於是自己動手看看怎么玩的,先看成果
App上監聽通知並向服務器POST支付信息

服務端的支付訂單表

下面說原理及流程
1.App上使用NotificationListenerService監聽通知欄通知,一旦微信支付或者支付寶收款收到消息,讀取消息的內容,然后使用正則匹配金額
2.App讀取到金額后,構造支付訂單,支付訂單包含:訂單號(App自己生成,不是真實的支付方訂單號),金額,App端標識,支付方式,簽名(保證數據不被篡改)
3.App將訂單POST到填寫的URL中
4.服務端收到訂單信息,先校驗簽名是否相符,再查看訂單是否存在(防止重放攻擊),驗證通過后存入數據庫,並向指定的回調地址發起請求
5.服務端如果向指定的回調地址發起請求失敗,使用定時任務重復發起回調,直到回調成功或達到指定次數
以上就是全部過程,服務端使用springboot,可以很快速搭建
當然為了保證可靠性需要給App加固,防止退出,還有這種只能讀取到金額,其他信息一無所知,有些局限性
2019-03-14補充:
代碼很簡單,上傳github完全是小題大做,下面貼出關鍵代碼
App部分
繼承NotificationListenerService重寫onNotificationPosted方法
1 //來通知時的調用
2 @Override
3 public void onNotificationPosted(StatusBarNotification sbn) {
4 Notification notification = sbn.getNotification();
5 if (notification == null) {
6 return;
7 }
8 Bundle extras = notification.extras;
9 if (extras != null) {
10 //包名
11 String pkg = sbn.getPackageName();
12 // 獲取通知標題
13 String title = extras.getString(Notification.EXTRA_TITLE, "");
14 // 獲取通知內容
15 String content = extras.getString(Notification.EXTRA_TEXT, "");
16 Log.i(TAG, String.format("收到通知,包名:%s,標題:%s,內容:%s", pkg, title, content));
17 //處理
18 processOnReceive(pkg, title, content);
19 }
20 }
21
22 /**
23 * 消息來時處理
24 *
25 * @param pkg
26 * @param title
27 * @param content
28 */
29 private void processOnReceive(String pkg, String title, String content) {
30 if (!AppConstants.LISTEN_RUNNING) {
31 return;
32 }
33 if ("com.eg.android.AlipayGphone".equals(pkg)) {
34 //支付寶
35 if (checkMsgValid(title,content,"alipay") && StringUtils.isNotBlank(parseMoney(content))) {
36 TreeMap<String, String> paramMap = new TreeMap<>();
37 paramMap.put("title", title);
38 paramMap.put("content", content);
39 paramMap.put("identifier", AppConstants.CLIENT_IDENTIFIER);
40 paramMap.put("orderid", CommonUtils.randomCharSeq());
41 paramMap.put("gateway", "alipay");
42 String sign = CommonUtils.calcSign(paramMap, AppConstants.SIGN_KEY);
43 if (StringUtils.isBlank(sign)) {
44 Log.e(TAG, "簽名錯誤");
45 return;
46 }
47 HttpTask task = new HttpTask();
48 task.setOnAsyncResponse(this);
49 String json = new Gson().toJson(paramMap);
50 task.execute(AppConstants.POST_URL, "sign=" + sign, json);
51 }
52 } else if ("com.tencent.mm".equals(pkg)) {
53 //微信
54 if (checkMsgValid(title, content, "wxpay") && StringUtils.isNotBlank(parseMoney(content))) {
55 TreeMap<String, String> paramMap = new TreeMap<>();
56 paramMap.put("title", title);
57 paramMap.put("content", content);
58 paramMap.put("identifier", AppConstants.CLIENT_IDENTIFIER);
59 paramMap.put("orderid", CommonUtils.randomCharSeq());
60 paramMap.put("gateway", "wxpay");
61 String sign = CommonUtils.calcSign(paramMap, AppConstants.SIGN_KEY);
62 if (StringUtils.isBlank(sign)) {
63 Log.e(TAG, "簽名錯誤");
64 return;
65 }
66 HttpTask task = new HttpTask();
67 task.setOnAsyncResponse(this);
68 String json = new Gson().toJson(paramMap);
69 task.execute(AppConstants.POST_URL, "sign=" + sign, json);
70 }
71 }
72 }
73
74 /**
75 * 解析內容字符串,提取金額
76 *
77 * @param content
78 * @return
79 */
80 private static String parseMoney(String content) {
81 Pattern pattern = Pattern.compile("收款(([1-9]\\d*)|0)(\\.(\\d){0,2})?元");
82 Matcher matcher = pattern.matcher(content);
83 if (matcher.find()) {
84 String tmp = matcher.group();
85 Pattern patternnum = Pattern.compile("(([1-9]\\d*)|0)(\\.(\\d){0,2})?");
86 Matcher matchernum = patternnum.matcher(tmp);
87 if (matchernum.find())
88 return matchernum.group();
89 }
90 return null;
91 }
92
93 /**
94 * 驗證消息的合法性,防止非官方消息被處理
95 *
96 * @param title
97 * @param content
98 * @param gateway
99 * @return
100 */
101 private static boolean checkMsgValid(String title, String content, String gateway) {
102 if ("wxpay".equals(gateway)) {
103 //微信支付的消息格式
104 //1條:標題:微信支付,內容:微信支付收款0.01元(朋友到店)
105 //多條:標題:微信支付,內容:[4條]微信支付: 微信支付收款1.01元(朋友到店)
106 Pattern pattern = Pattern.compile("^((\\[\\+?\\d+條])?微信支付:|微信支付收款)");
107 Matcher matcher = pattern.matcher(content);
108 return "微信支付".equals(title) && matcher.find();
109 } else if ("alipay".equals(gateway)) {
110 //支付寶的消息格式,標題:支付寶通知,內容:支付寶成功收款1.00元。
111 return "支付寶通知".equals(title);
112 }
113 return false;
114 }
服務端接收代碼
/**
* 接受App發送的通知內容
* @param content 通知內容json, {"title": "標題", "content": "內容", "identifier": "app端標識", "orderid": "app生成的唯一訂單號", "gateway": "wxpay或alipay"}
* @param sign 簽名,簽名方式按照content對應的key1=vaule1&key2=value2...&SECKEY計算md5,key的順序按字母表的順序
* @return
*/
@RequestMapping(value = "/c/post/notification", method = { RequestMethod.POST })
@ResponseBody
public String receiveAppNotification(@RequestBody Map<String, Object> content, String sign) {
logger.debug("請求參數,content=>{}, sign=>{}", JSON.toJSONString(content), sign);
if (StringUtils.isBlank(sign) || CollectionUtils.isEmpty(content)) {
return APIUtil.getReturn(APIConst.PARAM_ERROR);
}
//再次驗證字段
String contenttext = (String) content.get("content");
String identifier = (String) content.get("identifier");
String orderid = (String) content.get("orderid");
String gateway = (String) content.get("gateway");
if (StringUtils.isAnyBlank(contenttext, identifier, orderid, gateway) || !ImmutableList.of("alipay",
"wxpay").contains(gateway)) {
return APIUtil.getReturn(APIConst.PARAM_ERROR);
}
//讀取金額(單位元)
Pattern pattern = Pattern.compile("([1-9]\\d*\\.\\d*|0\\.\\d*[1-9]\\d*)");
Matcher matcher = pattern.matcher(contenttext);
if (!matcher.find()) {
return APIUtil.getReturn(APIConst.PARAM_ERROR);
}
String amountStr = matcher.group(1);
logger.debug("解析的金額:{}", amountStr);
BigDecimal amount = null;
try {
amount = new BigDecimal(amountStr);
} catch (NumberFormatException e) {
logger.error("金額格式錯誤: {}", amountStr);
return APIUtil.getReturn(APIConst.PARAM_ERROR);
}
//驗證簽名
TreeMap<String, Object> paramMap = new TreeMap<>(content);
Iterator<Map.Entry<String, Object>> it = paramMap.entrySet().iterator();
StringBuilder sb = new StringBuilder();
while (it.hasNext()) {
Map.Entry<String, Object> entry = it.next();
sb.append(entry.getKey());
sb.append("=");
sb.append(entry.getValue());
sb.append("&");
}
sb.append(SIGN_KEY);
//計算簽名
String calcSign = MD5Util.MD5Encode(sb.toString(), "UTF-8");
if (!calcSign.equalsIgnoreCase(sign)) {
return APIUtil.getReturn(1, "簽名錯誤");
}
//查詢訂單號是否已經存在
boolean exist = orderService.checkOrderExist(orderid);
if (exist) {
logger.error("訂單號:{}已存在", orderid);
return APIUtil.getReturn(1, "訂單號已存在");
}
//訂單寫入數據庫
String account = "";
if (gateway.equals("wxpay")) {
account = "W" + identifier;
} else if (gateway.equals("alipay")) {
account = "A" + identifier;
}
MqOrder order = new MqOrder();
order.setAccount(account);
order.setAmount(amount);
order.setGateway(gateway);
order.setOrderId(orderid);
order.setStatus(0);
order.setNotifyCount(0);
order.setCreateTime(new Date());
orderService.save(order);
return APIUtil.getReturn(APIConst.OK);
}
歡迎學習交流,qq:3168598325

