支付宝支付
1、申请签约
目的是得到开发使用的合作伙伴身份(PID)和MD5秘钥,申请地址(即时到账收款):https://b.alipay.com/order/productDetail.htm?productId=2015110218012942
申请方式在开放平台的文档上有详细说明,这里就不再赘述。
2、接入支付接口
在得到PID和秘钥后就可以接入接口了,首先在开放平台中下载官方的demo(java+MD5版本),支付宝的demo做的非常好,下载下来直接配置下jdk就可以运行了。如果遇到Java compiler level does not match错误,说明你用的eclipse或myeclipse的jdk编译版本与demo的JDK编译版本不一致,修改下jdk编译版本就可以了。其实就用到了4个类,如下图
可以选择把支付功能单独做一个项目,在其他项目调用接口就可以支付,也可以整合到自己的项目里,为了好维护我整合到自己的项目里了。把这四个类放到自己的项目中,引入相应的jar包
2.1、demo中类的说明
AlipayConfig.java类主要是配置参数信息的类
package com.fahai.pay.alipay;import com.fahai.utils.ProInfoUtil;/* * *类名:AlipayConfig *功能:基础配置类 *详细:设置帐户有关信息及返回路径 *版本:3.4 *修改日期:2016-03-08 *说明: *以下代码只是为了方便商户测试而提供的样例代码,商户可以根据自己网站的需要, 按照技术文档编写,并非一定要使用该代码。 *该代码仅供学习和研究支付宝接口使用,只是提供一个参考。 */public class AlipayConfig {//↓↓↓↓↓↓↓↓↓↓请在这里配置您的基本信息↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ // 合作身份者ID,签约账号,以2088开头由16位纯数字组成的字符串, 查看地址:https://b.alipay.com/order/pidAndKey.htm public static String partner = "你自己的PID"; // 收款支付宝账号,以2088开头由16位纯数字组成的字符串,一般情况下收款账号就是签约账号 public static String seller_id = partner; // MD5密钥,安全检验码,由数字和字母组成的32位字符串,查看地址:https://b.alipay.com/order/pidAndKey.htm public static String key = "你自己的MD5秘钥"; // 服务器异步通知页面路径 需http://格式的完整路径,不能加?id=123这类自定义参数, 必须外网可以正常访问//异步通知页面,就是接受支付宝支付结果返回信息的Controller,可以处理自己的支付后的逻辑 //测试环境 public static String notify_url = ProInfoUtil.getInstance(). getProperty("project_url")+"order/pay/aliPayOrder"; // 页面跳转同步通知页面路径 需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问 //同步跳转的页面,就是支付宝支付成功后页面跳转的url public static String return_url = ProInfoUtil.getInstance(). getProperty("project_url")+"order/pay/payResponse"; // 签名方式 public static String sign_type = "MD5"; // 调试用,创建TXT日志文件夹路径,见AlipayCore.java类中的logResult(String sWord)打印方法。 public static String log_path = "C:\\"; // 字符编码格式 目前支持 gbk 或 utf-8 public static String input_charset = "utf-8"; // 支付类型 ,无需修改 public static String payment_type = "1"; // 调用的接口名,无需修改 public static String service = "create_direct_pay_by_user"; //↑↑↑↑↑↑↑↑↑↑请在这里配置您的基本信息↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑//↓↓↓↓↓↓↓↓↓↓ 请在这里配置防钓鱼信息,如果没开通防钓鱼功能,为空即可 ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ // 防钓鱼时间戳 若要使用请调用类文件submit中的query_timestamp函数 public static String anti_phishing_key = ""; // 客户端的IP地址 非局域网的外网IP地址,如:221.0.0.1 public static String exter_invoke_ip = ""; //↑↑↑↑↑↑↑↑↑↑请在这里配置防钓鱼信息, 如果没开通防钓鱼功能,为空即可 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑}
AlipayCore.java是整理参数的工具类
package com.alipay.util;import java.io.File;import java.io.FileWriter;import java.io. IOException;import java.util.ArrayList;import java.util.Collections;import java.util.HashMap;import java.util.List;import java.util.Map;import org.apache.commons.codec. digest.DigestUtils;import org.apache.commons.httpclient.methods.multipart.FilePartSource;import org. apache.commons.httpclient.methods.multipart.PartSource;import com.alipay.config.AlipayConfig;/* * *类名:AlipayFunction *功能:支付宝接口公用函数类 *详细:该类是请求、通知返回两个文件所调用的公用函数核心处理文件,不需要修改 *版本:3.3 *日期:2012-08-14 *说明: *以下代码只是为了方便商户测试而提供的样例代码,商户可以根据自己网站的需要, 按照技术文档编写,并非一定要使用该代码。 *该代码仅供学习和研究支付宝接口使用,只是提供一个参考。 */public class AlipayCore { /** * 除去数组中的空值和签名参数 * @param sArray 签名参数组 * @return 去掉空值与签名参数后的新签名参数组 */ public static Map<String, String> paraFilter(Map<String, String> sArray) { Map<String, String> result = new HashMap<String, String>(); if (sArray == null || sArray.size() <= 0) { return result; } for (String key : sArray.keySet()) { String value = sArray.get(key); if (value == null || value.equals("") || key.equalsIgnoreCase("sign") || key.equalsIgnoreCase("sign_type")) { continue; } result.put(key, value); } return result; } /** * 把数组所有元素排序,并按照“参数=参数值”的模式用“&”字符拼接成字符串 * @param params 需要排序并参与字符拼接的参数组 * @return 拼接后字符串 */ public static String createLinkString(Map<String, String> params) { List<String> keys = new ArrayList<String>(params.keySet()); Collections.sort(keys); String prestr = ""; for (int i = 0; i < keys.size(); i++) { String key = keys.get(i); String value = params.get(key); if (i == keys.size() - 1) {//拼接时,不包括最后一个&字符 prestr = prestr + key + "=" + value; } else { prestr = prestr + key + "=" + value + "&"; } } return prestr; } /** * 写日志,方便测试(看网站需求,也可以改成把记录存入数据库) * @param sWord 要写入日志里的文本内容 */ public static void logResult(String sWord) { FileWriter writer = null; try { writer = new FileWriter(AlipayConfig.log_path + "alipay_log_" + System.currentTimeMillis()+".txt"); writer.write(sWord); } catch (Exception e) { e.printStackTrace(); } finally { if (writer != null) { try { writer.close(); } catch (IOException e) { e.printStackTrace(); } } } } /** * 生成文件摘要 * @param strFilePath 文件路径 * @param file_digest_type 摘要算法 * @return 文件摘要结果 */ public static String getAbstract(String strFilePath, String file_digest_type) throws IOException { PartSource file = new FilePartSource(new File(strFilePath)); if(file_digest_type.equals("MD5")){ return DigestUtils.md5Hex(file.createInputStream()); } else if(file_digest_type.equals("SHA")) { return DigestUtils.sha256Hex(file.createInputStream()); } else { return ""; } } }
AlipayNotify.java是验证签名的类
package com.alipay.util;import java.io.BufferedReader;import java. io.InputStreamReader;import java.net.HttpURLConnection;import java.net.URL;import java.util. Map;import com.alipay.config.AlipayConfig;import com.alipay.sign.MD5;/* * *类名:AlipayNotify *功能:支付宝通知处理类 *详细:处理支付宝各接口通知返回 *版本:3.3 *日期:2012-08-17 *说明: *以下代码只是为了方便商户测试而提供的样例代码,商户可以根据自己网站的需要, 按照技术文档编写,并非一定要使用该代码。 *该代码仅供学习和研究支付宝接口使用,只是提供一个参考 *************************注意************************* *调试通知返回时,可查看或改写log日志的写入TXT里的数据,来检查通知返回是否正常 */public class AlipayNotify { /** * 支付宝消息验证地址 */ private static final String HTTPS_VERIFY_URL = "https://mapi.alipay.com/gateway.do?service=notify_verify&"; /** * 验证消息是否是支付宝发出的合法消息 * @param params 通知返回来的参数数组 * @return 验证结果 */ 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); //写日志记录(若要调试,请取消下面两行注释) //String sWord = "responseTxt=" + responseTxt + "\n isSign=" + isSign + "\n 返回回来的参数:" + AlipayCore.createLinkString(params); //AlipayCore.logResult(sWord); if (isSign && responseTxt.equals("true")) { return true; } else { return false; } } /** * 根据反馈回来的信息,生成签名结果 * @param Params 通知返回来的参数数组 * @param sign 比对的签名结果 * @return 生成的签名结果 */ private static boolean getSignVeryfy(Map<String, String> Params, String sign) { //过滤空值、sign与sign_type参数 Map<String, String> sParaNew = AlipayCore.paraFilter(Params); //获取待签名字符串 String preSignStr = AlipayCore.createLinkString(sParaNew); //获得签名验证结果 boolean isSign = false; if(AlipayConfig.sign_type.equals("MD5") ) { isSign = MD5.verify(preSignStr, sign, AlipayConfig.key, AlipayConfig.input_charset); } return isSign; } /** * 获取远程服务器ATN结果,验证返回URL * @param notify_id 通知校验ID * @return 服务器ATN结果 * 验证结果集: * invalid命令参数不对 出现这个错误,请检测返回处理中partner和key是否为空 * true 返回正确信息 * false 请检查防火墙或者是服务器阻止端口问题以及验证时间是否超过一分钟 */ private static String verifyResponse(String notify_id) { //获取远程服务器ATN结果,验证是否是支付宝服务器发来的请求 String partner = AlipayConfig.partner; String veryfy_url = HTTPS_VERIFY_URL + "partner=" + partner + "¬ify_id=" + notify_id; return checkUrl(veryfy_url); } /** * 获取远程服务器ATN结果 * @param urlvalue 指定URL路径地址 * @return 服务器ATN结果 * 验证结果集: * invalid命令参数不对 出现这个错误,请检测返回处理中partner和key是否为空 * true 返回正确信息 * false 请检查防火墙或者是服务器阻止端口问题以及验证时间是否超过一分钟 */ private static String checkUrl(String urlvalue) { String inputLine = ""; try { URL url = new URL(urlvalue); HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection(); BufferedReader in = new BufferedReader(new InputStreamReader(urlConnection .getInputStream())); inputLine = in.readLine().toString(); } catch (Exception e) { e.printStackTrace(); inputLine = ""; } return inputLine; } }
AlipaySubmit.java模拟form表单请求支付宝支付接口的类
package com.alipay.util;import java.io.IOException;import java.net.MalformedURLException;import java.net. URL;import java.util.ArrayList;import java.util.List;import java.util.Map; import org.dom4j.Document; import org.dom4j.DocumentException;import org.dom4j.Node;import org.dom4j.io. SAXReader;import com.alipay.config.AlipayConfig;import com.alipay.sign.MD5;/* * *类名:AlipaySubmit *功能:支付宝各接口请求提交类 *详细:构造支付宝各接口表单HTML文本,获取远程HTTP数据 *版本:3.3 *日期:2012-08-13 *说明: *以下代码只是为了方便商户测试而提供的样例代码,商户可以根据自己网站的需要, 按照技术文档编写,并非一定要使用该代码。 *该代码仅供学习和研究支付宝接口使用,只是提供一个参考。 */public class AlipaySubmit { /** * 支付宝提供给商户的服务接入网关URL(新) */ private static final String ALIPAY_GATEWAY_NEW = "https://mapi.alipay.com/gateway.do?"; /** * 生成签名结果 * @param sPara 要签名的数组 * @return 签名结果字符串 */ public static String buildRequestMysign(Map<String, String> sPara) { String prestr = AlipayCore.createLinkString(sPara); //把数组所有元素,按照“参数=参数值”的模式用“&”字符拼接成字符串 String mysign = ""; if(AlipayConfig.sign_type.equals("MD5") ) { mysign = MD5.sign(prestr, AlipayConfig.key, AlipayConfig.input_charset); } return mysign; } /** * 生成要请求给支付宝的参数数组 * @param sParaTemp 请求前的参数数组 * @return 要请求的参数数组 */ private static Map<String, String> buildRequestPara(Map<String, String> sParaTemp) { //除去数组中的空值和签名参数 Map<String, String> sPara = AlipayCore.paraFilter(sParaTemp); //生成签名结果 String mysign = buildRequestMysign(sPara); //签名结果与签名方式加入请求提交参数组中 sPara.put("sign", mysign); sPara.put("sign_type", AlipayConfig.sign_type); return sPara; } /** * 建立请求,以表单HTML形式构造(默认) * @param sParaTemp 请求参数数组 * @param strMethod 提交方式。两个值可选:post、get * @param strButtonName 确认按钮显示文字 * @return 提交表单HTML文本 */ public static String buildRequest(Map<String, String> sParaTemp, String strMethod, String strButtonName) { //待请求参数数组 Map<String, String> sPara = buildRequestPara(sParaTemp); List<String> keys = new ArrayList<String>(sPara.keySet()); StringBuffer sbHtml = new StringBuffer(); sbHtml.append("<form id=\"alipaysubmit\" name=\"alipaysubmit\" action=\"" + ALIPAY_GATEWAY_NEW + "_input_charset=" + AlipayConfig.input_charset + "\" method=\"" + strMethod + "\">"); for (int i = 0; i < keys.size(); i++) { String name = (String) keys.get(i); String value = (String) sPara.get(name); sbHtml.append("<input type=\"hidden\" name=\"" + name + "\" value=\"" + value + "\"/>"); } //submit按钮控件请不要含有name属性 sbHtml.append("<input type=\"submit\" value=\"" + strButtonName + "\" style=\"display:none;\"></form>"); sbHtml.append("<script>document.forms['alipaysubmit'].submit();</script>"); return sbHtml.toString(); } /** * 用于防钓鱼,调用接口query_timestamp来获取时间戳的处理函数 * 注意:远程解析XML出错,与服务器是否支持SSL等配置有关 * @return 时间戳字符串 * @throws IOException * @throws DocumentException * @throws MalformedURLException */ public static String query_timestamp() throws MalformedURLException, DocumentException, IOException { //构造访问query_timestamp接口的URL串 String strUrl = ALIPAY_GATEWAY_NEW + "service=query_timestamp&partner=" + AlipayConfig.partner + "&_input_charset" +AlipayConfig.input_charset; StringBuffer result = new StringBuffer(); SAXReader reader = new SAXReader(); Document doc = reader.read(new URL(strUrl).openStream()); List<Node> nodeList = doc.selectNodes("//alipay/*"); for (Node node : nodeList) { // 截取部分不需要解析的信息 if (node.getName().equals("is_success") && node.getText().equals("T")) { // 判断是否有成功标示 List<Node> nodeList1 = doc.selectNodes("//response/timestamp/*"); for (Node node1 : nodeList1) { result.append(node1.getText()); } } } return result.toString(); } }
这几个类调用支付宝接口的是AlipaySubmit,在网页选好购买的商品时,在系统中生成订单,然后进行支付,浏览器跳转到支付宝支付网站,Controller中代码为:
/** * 支付宝支付页面 * * @return * @throws IOException */ @RequestMapping(value = "/aliPay") public void aliPay(HttpServletRequest request, HttpServletResponse response) throws IOException { LOGGER.info("支付宝支付页面"); //商户订单号,商户网站订单系统中唯一订单号,必填 String orderNo = request.getParameter("orderNo"); //订单名称,必填 String subjectName = request.getParameter("subjectName"); //付款金额,必填 String total_fee = request.getParameter("fee"); //商品描述,可空 String body = "法海风控 " + subjectName; if ("money".equals(body)) { body = "法海风控 余额充值"; } //把请求参数打包成map Map<String, String> sParaTemp = new HashMap<String, String>(); sParaTemp.put("service", AlipayConfig.service); sParaTemp.put("partner", AlipayConfig.partner); sParaTemp.put("seller_id", AlipayConfig.seller_id); sParaTemp.put("_input_charset", AlipayConfig.input_charset); sParaTemp.put("payment_type", AlipayConfig.payment_type); sParaTemp.put("notify_url", AlipayConfig.notify_url); sParaTemp.put("return_url", AlipayConfig.return_url); sParaTemp.put("anti_phishing_key", AlipayConfig.anti_phishing_key); sParaTemp.put("exter_invoke_ip", AlipayConfig.exter_invoke_ip); sParaTemp.put("out_trade_no", orderNo); sParaTemp.put("subject", subjectName); sParaTemp.put("total_fee", total_fee); sParaTemp.put("body", body); //其他业务参数根据在线开发文档, 添加参数.文档地址:https://doc.open.alipay.com/doc2/detail.htm?spm=a219a.7629140.0.0. O9yorI&treeId=62&articleId=103740&docType=1 //如sParaTemp.put("参数名","参数值"); //建立请求 String sHtmlText = AlipaySubmit.buildRequest(sParaTemp,"get","确认"); response.setHeader("Content-Type", "text/html; charset=UTF-8"); response.setCharacterEncoding("UTF-8"); PrintWriter out = response.getWriter(); System.out.println(sHtmlText); out.println(sHtmlText); }
请求aliPay会跳转到支付宝支付页面:
3、接收支付结果通知
在配置好notify_url之后,支付结果会请求相应的接口,我的是order/pay/aliPayOrder,代码如下
/** * 支付宝支付订单 * @return * @throws IOException */ @ResponseBody @RequestMapping(value = "pay/aliPayOrder", method = RequestMethod.POST) public void aliPayOrder(HttpServletRequest request,HttpServletResponse response) throws IOException { LOGGER.info("支付订单"); //从request中获得参数Map,并返回可读的Map Map<String, String> params = RequestUtil.getParameterMap(request); LOGGER.info(params.toString()); //验证支付宝签名 boolean aliSign = AlipayNotify.verify(params); if (aliSign) {//验证成功 //交易状态 String tradeStatus = params.get("trade_status"); //订单编号 String orderNo = params.get("out_trade_no"); //支付单号 String payNo = params.get("trade_no"); //支付账号 String payAccount = params.get("buyer_email"); //支付金额 String totalFee = params.get("total_fee"); //收款支付宝账号 String sellerId = params.get("seller_id"); if (Constant.ALIPAY_TRADE_SUCCESS.equals(tradeStatus)) {//支付宝支付状态为成功 //验证支付宝返回信息与请求信息一致 if (ProInfoUtil.getInstance().getProperty("alipay_partner").equals(sellerId)) { //订单处理状态 String orderHandleStatus = "error"; //验证订单未做支付处理 Order order = orderService.queryOrderByOrderNo(orderNo); //订单已支付 if (Constant.DEALSTATUS_PAY.equals(order.getDealStatus())) { response.getWriter().print("success"); return; } if (null != order && Double.parseDouble(totalFee) == order.getDealPrice() && Constant.DEALSTATUS_NOT_PAY.equals(order.getDealStatus())) {//验证金额是否和订单一致 //更新订单为已支付、更新用户套餐余额、添加用户充值记录、添加用户余额支出记录 order.setDealStatus(Constant.DEALSTATUS_PAY); order.setPayNo(payNo); order.setPayType(Constant.ALIPAY); order.setPayAccount(payAccount); try { //支付成功处理支付业务 boolean result = orderService.payOrder(order); if (result) { //成功后向支付宝返回成功标志 LOGGER.info("支付宝支付成功"); orderHandleStatus = "success"; response.getWriter().print("success"); } } catch (Exception e) { e.printStackTrace(); LOGGER.info("支付宝支付失败"); response.getWriter().print("fail"); } } //添加支付信息 Map<String, Object> map = new HashMap<String, Object>(); map.put("params", params.toString()); map.put("payType", Constant.ALIPAY); map.put("orderNo", orderNo); map.put("handleStatus", orderHandleStatus); orderService.addPayInfo(map); } } } else {//验证失败 LOGGER.info("支付宝返回验证失败"); response.getWriter().print("fail"); } } /** * 从request中获得参数Map,并返回可读的Map * * @param request * @return */ @SuppressWarnings("unchecked") public static Map getParameterMap(HttpServletRequest request) { // 参数Map Map properties = request.getParameterMap(); // 返回值Map Map<String, String> returnMap = new HashMap<String, String>(); Iterator entries = properties.entrySet().iterator(); Map.Entry entry; String name = ""; String value = ""; while (entries.hasNext()) { entry = (Map.Entry) entries.next(); name = (String) entry.getKey(); Object valueObj = entry.getValue(); if(null == valueObj){ value = ""; }else if(valueObj instanceof String[]){ String[] values = (String[])valueObj; for(int i=0;i<values.length;i++){ value = values[i] + ","; } value = value.substring(0, value.length()-1); }else{ value = valueObj.toString(); } returnMap.put(name, value); } return returnMap; }
至此,支付宝支付功能已经做完了,其中有几个细节需要添加,比如支付时查询订单状态是否已经支付,是否过期等等,可以根据自己的需求去完善。
支付宝的接入还是很顺利的,如果熟练的话一两天就可以完成了,刚开始写博客,有错误或者不明白的地方欢迎大家指出一起交流学习,共同进步。
由于篇幅问题,我在下一章介绍微信支付的接入。