项目集成支付宝支付,并进行沙箱测试

因为项目需要对接支付宝,进行扫描支付,同时生成订单进入系统处理具体的业务逻辑,所以就记录一下开发过程。

一、配置沙箱环境

1、入驻支付宝开发平台

访问https://open.alipay.com,入驻支付宝开发平台。

image.png

点击立即入驻
image.png

信息填写完成后,即可进入开发平台。

2、进入沙箱环境

进入支付宝开发平台>控制台>开发服务,点击研发服务即可进入沙箱环境

二、设置RSA2(SHA256)密钥

注:本次开发主要是应用了普通密钥方式。

image.png

image.png

参考文档:https://opendocs.alipay.com/open/291/105971

1、基本说明

支付宝开放平台开发助手提供了一键生成密钥功能,便于开发者生成一对 RSA 密钥(应用公钥、应用私钥)以及公钥证书申请 CSR 文件(在线申请应用公钥证书需要)。加密过程:使用公钥(public key)为系统进行加密,并将密文发送给解密者,解密者使用私钥(private key)解密将密文解码为明文。公钥证书模式中上传的文件,无论是 CSR 文件或者开发者自己申请的公钥证书文件,必须和用户本地代码中加密的应用私钥是匹配的,否则会导致支付宝开放平台验签失败。

支付宝开放平台支持使用普通公钥、公钥证书两种签名方式。下面分别向您介绍两种方式的工具操作步骤,包括如何使用密钥生成工具生成应用公钥(public key)、应用私钥(private key)和公钥证书申请 CSR 文件。

说明:
(1)应用公钥(public key)需提供给支付宝账号管理者上传到支付宝开放平台。
(2)应用私钥(private key)由开发者自己保存,需填写到代码中供签名时使用。
(3)生成的私钥需妥善保管,避免遗失,不要泄露。
(4)密钥和应用(APPID)一一对应,即开发者需要为名下的每个应用分别设置密钥,且不同应用的密钥不能混用。

工具下载:
WINDOWS(windows 版本工具请不要安装在含有空格的目录路径下,否则会导致公私钥乱码的问题)
MAC_OSX

普通公钥与公钥证书区别:
(1)企业开发者若涉及资金类支出接口接入,必须使用公钥证书模式。
(2)个人开发者不涉及到资金类接口,建议使用公钥方式进行加签。
(3)在报文签名场景下,报文接受方使用发送方的公钥进行报文验签,该功能两种签名方式都可以实现。
(4)公钥证书签名方式引入了 CA 机构对公钥持有者进行身份识别,保证该证书所属实体的真实性,以实现报文的抗抵赖。
(5)公钥证书签名方式下,开放平台支持通过上传 CSR 文件的方式给开发者在线签发应用公钥证书,新的开放平台 RSA 验签和签名工具支持生成 CSR 文件。

前提条件:
(1)已完成开发者入驻以及实名认证。详情请参见个人支付宝账号实名认证指南支付宝账号实名认证指南
(2)已下载密钥生成工具(支付宝开放平台开发助手)。详情请参见简介

2、普通公钥方式

下载相应环境工具并安装后即可使用,本步骤指引以 WINDOWS界面为例。


image.png

(1)根据开发语言选择密钥格式和密钥长度。

说明:
①新建应用请务必使用 RSA2 密钥长度 即 2048 位。详情请参见开放平台证书升级指南
②目前已使用 RSA 密钥长度即 1024 位密钥长度的应用仍然可以正常调用接口。
③接口中的 sign_type 参数应与上传密钥的加签方式一致。例如接口参数中 sign_type=RSA2,请求时就会使用此处设置的 RSA2(SHA256) 公钥验签。

(2)点击 生成密钥 后,工具会自动生成商户应用公钥(public key)和应用私钥(private key)。

image.png

(3)点击工具界面下方的打开文件位置,在支付宝开放平台开发助手文件夹下选择RSA 密钥,即可找到生成的公私钥文件。

image.png

(4)复制上一步生成的公钥,点击保存设置,即可完成公钥的设置。

image.png

3、公钥证书方式

同普通公钥方式一样,下载相应环境工具并安装后即可使用,本步骤指引以 MAC_OSX 界面为例:

(1)点击获取CSR文件后的点击获取,生成应用公钥证书 CSR 申请文件。

说明:
① 密钥长度选择 RSA2
② 密钥格式选择 PKCS8(Java适用)

image.png

(2)在弹出的获取CSR对话框中根据提示填写相关信息,点击生成 CSR 文件。

说明:
①组织/公司名称一定要和开发者中心门户账号信息的公司名称保持一致,否则会导致后续步骤中上传 CSR 证书文件校验失败。
②沙箱环境下组织/公司名称应填写为沙箱环境。

image.png

(3)在生成 CSR 文件后,点击打开密钥文件路径,在对应的文件夹里可以看到三个文件:应用公钥 key 串、应用私钥 key 串,以及 csr 格式的应用公钥证书文件。

image.png

(4)进入支付宝开放平台并打开对应的应用,在应用的开发配置页面进行接口加签方式设置。点击设置后,输入手机验证码。

image.png

(5)加签模式选择公钥证书 ,上传证书文件选择上传 CSR 文件在线生成证书或者上传已申请证书,即可完成公钥证书的设置。上传证书文件。即可完成公钥证书的设置。

①选择上传 CSR 文件在线生成证书并点击 上传 CSR 文件在线生成证书。

image.png

②选择上传已申请证书,点击,选择上一步骤生成的 .csr 文件上传。上传完成证书后,系统会自动识别证书的加密方式。证书必须由权威 CA 签发,详情请参见 当前支持的CA列表,且仅支持 X.509 格式的证书,详情请参见 证书说明
image.png

三、项目集成支付宝支付接口

1、集成并配置SDK

参考文档:https://opendocs.alipay.com/open/54/103419

image.png

2、支付宝支付参数配置

public class AlipayConfig {
    /**
     * APPID即创建应用后生成
     */
    public static String APPID = "2021000118641985";

    /**
     * 开发者私钥,由开发者自己生成
     */
    public static String APP_PRIVATE_KEY = "MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCXOnhEM7n606PpsE25ItCMCyLVIL4b6dYLzMl2rT7NT9n62xSnlQDjbQ22pHNBEgRqFXqT1TeBqi61MGBjCwC9Uyz4E4CcTyjhoToeWlrwXSc/GJ4Goww5rZKdnaqtf2fspdGDwHxib6D5bZBPOOMenMCdA+vRlaZyKxDwVRsUdFTLuZT1eLaXXOTA6f6LkZvXntIC9oHNwgpWiMzQwdnfJij18IGYhLMUjlQKJUIeGEanf1R6vzgPZZlmmHrVhDrx0LD9JJYYNlN0a+NFgrZfuLv0jiJoMtCsqOFFOJHBp3BtNJQt200yp8idefZu7IPaSLYmxy2yPh6s2be4455dAgMBAAECggEAXkt28g/Oxzdv3SaxT98Fak0HSx0/bOhBLtpiRD2CC0LfCCvSlSuzghtdkaS4uLojRoJeDA/GrHMQ7KldcGRL8cELKSP/7XbuZsHBG2v7iCSNdCpFXp4L4Wr5II5O/h+TDVvXZ+99n2M7XEuUz9EIzO2wrDbls6k8P7PavABVAksQiwiPftUG507xAUUyjAZparqKJF232DiprwFePqeJnp1vh3EmE+YA4f8W4mz6GiJpgtPV8ZGeqjCV7X3gauU9hiqPbiW+rR+VEeShiZA1G/O1XbweY5BF0v4xgUwbQaRSxXHeUvpjHaEvCsKqeeKVFDGMZzybjTgCOjE6cnFlgQKBgQDY4ChitPuSSiJISoqcJxoKx4PxWuQw5+q+r3k29IPSu1ApDaeOP/+GhDREwQV0so5/kxBbeuZGXobtdp66UYEWBASOk6cRmc4dy0r70NRQs28t2Qxo8sRRBQttPynRwolOfhyuwm8s6H1aQwLBR9VkW3Jdi7gygsSmUUc6nbsk1wKBgQCygpBtvsGqo7HwW5It9kC0PPxGgIedVwDH5VZ3/Xvr1mkOT4XPB3RnpwFL0WtmvCYfDaJWlcBilOixw1jjV6xJvm+VIGL5poMPD5uroHkUm2SCkkacMS4Wyd7HOMqcoLL0KmdCNPCy4hhPNKqYaClFpHvuM49DsahIiCe1nZb76wKBgFxJhuX5/dOSmGQK1FD+kqZjoFHkS5ZEGjBqmzo3cqEJ9GKD3Pk7YpDrURKw0JGIKfs/qYZEFhl7wA7smz7N0BB+RTImwsFKodsr1wyxIKf2syjfY9iE9eVEMEicyD7qeWNdZvc25fhGNpFiUpnM55F9GH2WJxvXabccfyMCW9ChAoGAOkU2gix/qYUP46bwm8JDstIpg5YXLrwkzBvH0xlSp1RxLLO2uTL0w5UXbjlpNrr6Mq7PrDXr/AIhx00+KdAHtHbOk75jsJyzMWpl5WtXuutSrvCyze+b3OJ+r0eRk/k9EUj6Nfl0DOCTEN/fRCrUNiCQN9xqyq0mgq63T6imjYsCgYBeiegO0YAs0hYwNRyUREsG5yHLix9sQT85SGHj8XPL+DnRwlMybPxleS6qU5lI6UN7Yt4RRfYYwb+a4EI5hf5DVcAWl0vanojymveqJRvyn40DiXdX3uR2tD3ujYSacm8ATdyaFqfvlwFlzwLGh6OwfmuxIGPMvBnU4BC1GguMEg==";

    /**
     * 支付宝公钥,查看地址:https://openhome.alipay.com/platform/keyManage.htm 对应APPID下的支付宝公钥。
     */
    public static String ALIPAY_PUBLIC_KEY = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2KKzch2l2g7Sx0TpZRtyk2+dQUxzKWMgK61p3qa5USF8WN50sSlCGRNy7FXbh7wp+KgQIUWc8IAMKqzpl0JfyB4axbalElB/jsijRwteiIQajPZAzAsxa82OMPVDty+XxCqiEmV1Dc0eAPPEsjd5zyqYxMhUGSnmWB6l8BYSsQCY5A92ep6+caDKQkvPUGB8Az7oP3aiuRjYfPe3YyhOxI0G02yej4jJ+jMOAuMCjLv2RnNcpu1I6XAEFIpCR2ru5ffx4ccFl+PR3w0yG4f2WYFXJYyFWmR9fdVq2rD5ZHVfcKlCAUhEVhqqZaxxSNJOTrxDP7x5U7jCerBHsqXFzQIDAQAB";

    /**
     * 服务器异步通知页面路径  需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
     */
    public static String NOTIFY_URL = "http://c68b2c1735f7.ngrok.io/supplier/notify_url";

    /**
     * 页面跳转同步通知页面路径 需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
     */
    public static String RETURN_URL = "http://127.0.0.1:8081/supplier/return_url";

    /**
     * 商户生成签名字符串所使用的签名算法类型,目前支持 RSA2 和 RSA,推荐使用 RSA2
     */
    public static String SIGN_TYPE = "RSA2";

    /**
     * 编码集,支持 GBK/UTF-8
     */
    public static String CHARSET = "utf-8";

    /**
     * 支付宝网关(固定)
     * 沙箱环境:https://openapi.alipaydev.com/gateway.do
     * 生产环境:https://openapi.alipay.com/gateway.do
     */
    public static String URL = "https://openapi.alipaydev.com/gateway.do";

    /**
     * 参数返回格式,只支持 json
     */
    public static String FORMAT = "json";

}

关键参数说明:

① URL:支付宝支付网关(固定)

沙箱环境:https://openapi.alipaydev.com/gateway.do

生产环境:https://openapi.alipay.com/gateway.do

② APPID:APPID 即创建应用后生成,沙箱环境进入沙箱应用中查看

③ APP_PRIVATE_KEY:开发者私钥,由开发者自己生成,从这里进行查看,从本地支付宝开发平台开发助手生成的应用私钥获取。


image.png

④FORMAT:参数返回格式,只支持 json
⑤CHARSET:编码集,支持 GBK/UTF-8
⑥ALIPAY_PUBLIC_KEY:支付宝公钥,由支付宝生成


image.png

⑦SIGN_TYPE:商户生成签名字符串所使用的签名算法类型,目前支持 RSA2 和 RSA,推荐使用 RSA2
⑧RETURN_URL:页面跳转同步通知页面路径 需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问

⑨NOTIFY_URL:服务器异步通知页面路径 需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问。

3、支付宝支付工具类

public class AlipayUtil {

    //获得初始化的AlipayClient
    private final static AlipayClient alipayClient = new DefaultAlipayClient(AlipayConfig.URL, AlipayConfig.APPID,
            AlipayConfig.APP_PRIVATE_KEY, AlipayConfig.FORMAT, AlipayConfig.CHARSET, AlipayConfig.ALIPAY_PUBLIC_KEY, AlipayConfig.SIGN_TYPE);

    /**
     * @annotation: alipay.trade.page.pay(统一收单下单并支付页面接口)
     */
    public static AlipayTradePagePayResponse createOrder(String outTradeNo, String totalAmount, String subject, String body, HttpServletResponse rep) {
        //设置请求参数
        //实例化具体API对应的request类,类名称和接口名称对应,当前调用接口名称:alipay.trade.page.pay
        AlipayTradePagePayRequest alipayRequest = new AlipayTradePagePayRequest();
        alipayRequest.setReturnUrl(AlipayConfig.RETURN_URL);
        alipayRequest.setNotifyUrl(AlipayConfig.NOTIFY_URL);
        AlipayTradeAppPayModel model = new AlipayTradeAppPayModel();
        model.setOutTradeNo(outTradeNo);//订单编号 商户订单号,64个字符以内、可包含字母、数字、下划线;需保证在商户端不重复
        model.setProductCode("FAST_INSTANT_TRADE_PAY"); //销售产品码,与支付宝签约的产品码名称。 注:目前仅支持FAST_INSTANT_TRADE_PAY
        model.setTotalAmount(totalAmount);//订单总金额,单位为元,精确到小数点后两位,取值范围[0.01,100000000]。
        model.setSubject(subject);//订单标题
        model.setBody(body);//订单描述
        alipayRequest.setBizModel(model);
        //请求
        String result = null;
        AlipayTradePagePayResponse alipayTradePagePayResponse = null;
        try {
            alipayTradePagePayResponse = alipayClient.pageExecute(alipayRequest);
            if (alipayTradePagePayResponse.isSuccess()){
                System.out.println("创建订单成功!");
            }else {
                System.out.println("创建订单失败!");
            }
            result = alipayTradePagePayResponse.getBody();
            rep.setContentType("text/html;charset=" + AlipayConfig.CHARSET);
            rep.getWriter().write(result);//直接将完整的表单html输出到页面
            rep.getWriter().flush();
            rep.getWriter().close();
        } catch (AlipayApiException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return alipayTradePagePayResponse;
    }
    
    /**
     * @annotation: 调用统一收单线下交易查询接口 alipay.trade.query
     */
    public static String findOrder(String outTradeNo) throws AlipayApiException {
        AlipayTradeQueryRequest request =  new AlipayTradeQueryRequest();
        AlipayTradeAppPayModel model = new AlipayTradeAppPayModel();
        model.setOutTradeNo(outTradeNo);
        request.setBizModel(model);
        AlipayTradeQueryResponse response = alipayClient.execute(request);
        if (response.isSuccess()){
            System.out.println( "交易查询调用成功" );
        }  else {
            System.out.println("交易查询调用失败");
        }
        return response.getBody();
    }
}

4、实际业务处理

@Controller
@RequestMapping("/supplier")
public class SupplierController extends BaseController {

    /**
     * @annotation: 获得订单编号
     */
    public static synchronized String getOrderNumber() {
        Date date = new Date();
        return Long.valueOf(date.getTime()).toString();
    }

    /**
     * @annotation: 支付宝支付
     */
    @RequestMapping("/alipay")
    @ResponseBody
    public void alipay(Long supplierId, String loginName, String companyName, String expiredTime, String projectSign, HttpServletResponse rep) {
        BidProject bidProject = new BidProject();
        bidProject.setProjectSign(projectSign);
        bidProject.setDelFlag(ProjectConstants.DEL_FLAG_NO);
        List<BidProject> bidProjects = bidProjectService.selectBidProjectList(bidProject);
        BigDecimal totalAmount = bidProjects.get(0).getUkPrice();
        String subject = "";
        if (ProjectConstants.PROJECT_SIGN_TN.equals(projectSign)) {
            subject = "支付宝扫码支付测试";
        }
        String body = supplierId + "-" + loginName + "-" + companyName;
        String outTradeNo = getOrderNumber();
        //创建订单
        AliPayOrder aliPayOrder = new AliPayOrder();
        aliPayOrder.setTradeNo(outTradeNo);
        aliPayOrder.setSubject(subject);
        aliPayOrder.setBody(body);
        aliPayOrder.setTotalAmount(totalAmount);
        aliPayOrder.setProjectSign(projectSign);
        aliPayOrder.setSupplierId(supplierId);
        aliPayOrder.setOldExpiredTime(expiredTime);
        //续费年限先默认一年
        aliPayOrder.setRenewYear(1);
        AlipayTradePagePayResponse tradePagePayResponse = AlipayUtil.createOrder(outTradeNo, totalAmount.toString(), subject, body, rep);
        aliPayOrder.setCode(tradePagePayResponse.getCode());
        aliPayOrder.setMsg(tradePagePayResponse.getMsg());
        aliPayOrder.setSubCode(tradePagePayResponse.getSubCode());
        aliPayOrder.setSubMsg(tradePagePayResponse.getSubMsg());
        if (tradePagePayResponse.isSuccess()) {//成功
            aliPayOrder.setTradeNo(tradePagePayResponse.getTradeNo());
            aliPayOrder.setSellerId(tradePagePayResponse.getSellerId());
            aliPayOrder.setMerchantOrderNo(tradePagePayResponse.getMerchantOrderNo());
        }
        //保存订单信息
        aliPayService.saveAliPayOrder(aliPayOrder);
    }

    /**
     * @annotation: 支付宝页面跳转同步通知页面路径
     */
    @GetMapping("/return_url")
    public String return_url(HttpServletRequest request, ModelMap modelMap) throws UnsupportedEncodingException, AlipayApiException {
        //获取支付宝GET过来反馈信息
        Map<String, String> params = new HashMap<String, String>();
        Map<String, String[]> requestParams = request.getParameterMap();
        for (Iterator<String> iter = requestParams.keySet().iterator(); iter.hasNext(); ) {
            String name = (String) iter.next();
            String[] values = (String[]) requestParams.get(name);
            String valueStr = "";
            for (int i = 0; i < values.length; i++) {
                valueStr = (i == values.length - 1) ? valueStr + values[i]
                        : valueStr + values[i] + ",";
            }
            //乱码解决,这段代码在出现乱码时使用
            valueStr = new String(valueStr.getBytes("ISO-8859-1"), "utf-8");
            params.put(name, valueStr);
        }
        String outTradeNo = params.get("out_trade_no");
        AliPayOrder aliPayOrder = aliPayService.selectAliPayOrderByOutTradeNo(outTradeNo);
        modelMap.put("aliPayOrder", aliPayOrder);
        return "supplier/alipay/return_url";
    }

    /**
     * @annotation: 服务器异步通知页面路径
     */
    @RequestMapping("/notify_url")
    @ResponseBody
    public void notify_url(HttpServletRequest request, HttpServletResponse response) throws IOException, AlipayApiException, ParseException {
        //获取支付宝POST过来反馈信息
        Map<String, String> params = new HashMap<String, String>();
        Map<String, String[]> requestParams = request.getParameterMap();
        for (Iterator<String> iter = requestParams.keySet().iterator(); iter.hasNext(); ) {
            String name = (String) iter.next();
            String[] values = (String[]) requestParams.get(name);
            String valueStr = "";
            for (int i = 0; i < values.length; i++) {
                valueStr = (i == values.length - 1) ? valueStr + values[i]
                        : valueStr + values[i] + ",";
            }
            //乱码解决,这段代码在出现乱码时使用
            // valueStr = new String(valueStr.getBytes("ISO-8859-1"), "utf-8");
            params.put(name, valueStr);
        }
        boolean signVerified = AlipaySignature.rsaCheckV1(params, AlipayConfig.ALIPAY_PUBLIC_KEY, AlipayConfig.CHARSET, AlipayConfig.SIGN_TYPE); //调用SDK验证签名
        if (signVerified) {//验证成功
            /**签名验证成功后,需要进行通知数据的验证。
             1、商户需要验证该通知数据中的 out_trade_no 是否为商户系统中创建的订单号;
             2、判断 total_amount 是否确实为该订单的实际金额(即商户订单创建时的金额);
             3、校验通知中的seller_id(或者seller_email) 是否为out_trade_no这笔单据的对应的操作方(有的时候,一个商户可能有多个seller_id/seller_email)
             4、验证app_id是否为该商户本身。
             上述 1、2、3、4 有任何一个验证不通过,则表明本次通知是异常通知,务必忽略。
             */
            //商户订单号
            String out_trade_no = new String(request.getParameter("out_trade_no").getBytes("ISO-8859-1"), "UTF-8");
            AliPayOrder aliPayOrder = aliPayService.selectAliPayOrderByOutTradeNo(out_trade_no);
            if (aliPayOrder == null) {
                return;
            }
            //订单的实际金额
            String totalAmount = new String(request.getParameter("total_amount").getBytes("ISO-8859-1"), "UTF-8");
            if (!aliPayOrder.getTotalAmount().equals(new BigDecimal(totalAmount))) {
                return;
            }
            //商品卖方id
            String sellerIid = new String(request.getParameter("seller_id").getBytes("ISO-8859-1"), "UTF-8");
            if (!aliPayOrder.getSellerId().equals(sellerIid)) {
                return;
            }
            //appid
            String app_id = new String(request.getParameter("app_id").getBytes("ISO-8859-1"), "UTF-8");
            if (!AlipayConfig.APPID.equals(app_id)) {
                return;
            }

            //buyer_id
            String buyer_id = new String(request.getParameter("buyer_id").getBytes("ISO-8859-1"), "UTF-8");
            aliPayOrder.setBuyerId(buyer_id);


            SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

            //交易创建时间
            String gmt_create = new String(request.getParameter("gmt_create").getBytes("ISO-8859-1"), "UTF-8");
            if (gmt_create != null) {
                Date gmtCreate = simpleDateFormat.parse(gmt_create);
                aliPayOrder.setGmtCreate(gmtCreate);
            }

            //交易付款时间
            String gmt_payment = new String(request.getParameter("gmt_create").getBytes("ISO-8859-1"), "UTF-8");
            if (gmt_payment != null) {
                Date gmtPayment = simpleDateFormat.parse(gmt_payment);
                aliPayOrder.setGmtPayment(gmtPayment);
            }

            //交易结束时间
            String gmt_close = new String(request.getParameter("gmt_close").getBytes("ISO-8859-1"), "UTF-8");
            if (gmt_close != null) {
                Date gmtClose = simpleDateFormat.parse(gmt_close);
                aliPayOrder.setGmtClose(gmtClose);
            }

            //交易状态
            String trade_status = new String(request.getParameter("trade_status").getBytes("ISO-8859-1"), "UTF-8");
            aliPayOrder.setTradeStatus(trade_status);
            aliPayService.updateAliPayOrder(aliPayOrder);
            if (trade_status.equals("TRADE_SUCCESS")) { //默认开启
                //调用各个系统的接口
                String url = "";
                if (ProjectConstants.PROJECT_SIGN_TN.equals(aliPayOrder.getProjectSign())) {//泰能热电电子招投标系统
                    url = DictUtils.getDictValue("tn_project_interface_address", "supplier_ukey_renew");
                }
                JSONObject jsonObject = new JSONObject();
                jsonObject.put("supplierId", aliPayOrder.getSellerId());
                jsonObject.put("expiredTime", aliPayOrder.getNowExpiredTime());
                HttpUtils.sendPost(url, JSONObject.toJSONString(jsonObject));
            }
            response.setContentType("text/html;charset=" + AlipayConfig.CHARSET);
            response.getWriter().write("success");
            response.getWriter().flush();
            response.getWriter().close();
        } else {//验证失败
            response.setContentType("text/html;charset=" + AlipayConfig.CHARSET);
            response.getWriter().write("fail");
            response.getWriter().flush();
            response.getWriter().close();
        }
    }
}

四、接口调用

1、支付

电脑网站支付的支付接口alipay.trade.page.pay调用时序图如下:

image.png

调用顺序如下:

1. 商户系统请求支付宝接口alipay.trade.page.pay,支付宝对商户请求参数进行校验,而后重新定向至用户登录页面。

2. 用户确认支付后,支付宝通过 get 请求 returnUrl(商户入参传入),返回同步返回参数。

3. 交易成功后,支付宝通过 post 请求 notifyUrl(商户入参传入),返回异步通知参数。

4. 若由于网络等问题异步通知没有到达,商户可自行调用交易查询接口 alipay.trade.query进行查询,根据查询接口获取交易以及支付信息(商户也可以直接调用查询接口,不需要依赖异步通知)。

注意:

①由于同步返回的不可靠性,支付结果必须以异步通知或查询接口返回为准,不能依赖同步跳转。

②商户系统接收到异步通知以后,必须通过验签(验证通知中的 sign 参数)来确保支付通知是由支付宝发送的。详细验签规则参考异步通知验签

③接收到异步通知并验签通过后,一定要检查通知内容,包括通知中的 app_id、out_trade_no、total_amount 是否与请求中的一致,并根据 trade_status 进行后续业务处理。

④在支付宝端,partnerId 与 out_trade_no 唯一对应一笔单据,商户端保证不同次支付 out_trade_no 不可重复;若重复,支付宝会关联到原单据,基本信息一致的情况下会以原单据为准进行支付。

五、支付结果异步通知

对于 PC 网站支付的交易,在用户支付完成之后,支付宝会根据 API 中商户传入的 notify_url,通过 POST 请求的形式将支付结果作为参数通知到商户系统。

1、异步通知参数

公共参数

image.png

image.png

2、服务器异步通知页面特性

①必须保证服务器异步通知页面(notify_url)上无任何字符,如空格、HTML 标签、开发系统自带抛出的异常提示信息等;

②支付宝是用 POST 方式发送通知信息,因此该页面中获取参数的方式,如:request.Form(“out_trade_no”)、$_POST[‘out_trade_no’];

③支付宝主动发起通知,该方式才会被启用;

④只有在支付宝的交易管理中存在该笔交易,且发生了交易状态的改变,支付宝才会通过该方式发起服务器通知(即时到账交易状态为“等待买家付款”的状态默认是不会发送通知的);

⑤服务器间的交互,不像页面跳转同步通知可以在页面上显示出来,这种交互方式是不可见的;

⑥第一次交易状态改变(即时到账中此时交易状态是交易完成)时,不仅会返回同步处理结果,而且服务器异步通知页面也会收到支付宝发来的处理结果通知;

⑦程序执行完后必须打印输出“success”(不包含引号)。如果商户反馈给支付宝的字符不是 success 这7个字符,支付宝服务器会不断重发通知,直到超过24小时22分钟。一般情况下,25小时以内完成8次通知(通知的间隔频率一般是:4m,10m,10m,1h,2h,6h,15h);

⑧程序执行完成后,该页面不能执行页面跳转。如果执行页面跳转,支付宝会收不到 success 字符,会被支付宝服务器判定为该页面程序运行出现异常,而重发处理结果通知;

⑨cookies、session 等在此页面会失效,即无法获取这些数据;

⑩该方式的调试与运行必须在服务器上,即互联网上能访问;

该方式的作用主要防止订单丢失,即页面跳转同步通知没有处理订单更新,它则去处理;

当商户收到服务器异步通知并打印出 success 时,服务器异步通知参数 notify_id 才会失效。也就是说在支付宝发送同一条异步通知时(包含商户并未成功打印出 success 导致支付宝重发数次通知),服务器异步通知参数 notify_id 是不变的。

3、异步返回结果的验签

某商户设置的通知地址为 https://商家网站通知地址,对应接收到通知的示例如下:

https://商家网站通知地址?voucher_detail_list=[{"amount":"0.20","merchantContribute":"0.00","name":"5折券","otherContribute":"0.20","type":"ALIPAY_DISCOUNT_VOUCHER","voucherId":"2016101200073002586200003BQ4"}]&fund_bill_list=[{"amount":"0.80","fundChannel":"ALIPAYACCOUNT"},{"amount":"0.20","fundChannel":"MDISCOUNT"}]&subject=PC网站支付交易&trade_no=2016101221001004580200203978&gmt_create=2016-10-12 21:36:12&notify_type=trade_status_sync&total_amount=1.00&out_trade_no=mobile_rdm862016-10-12213600&invoice_amount=0.80&seller_id=2088201909970555&notify_time=2016-10-12 21:41:23&trade_status=TRADE_SUCCESS&gmt_payment=2016-10-12 21:37:19&receipt_amount=0.80&passback_params=passback_params123&buyer_id=2088102114562585&app_id=2016092101248425&notify_id=7676a2e1e4e737cff30015c4b7b55e3kh6& sign_type=RSA2&buyer_pay_amount=0.80&sign=***&point_amount=0.00

第一步: 在通知返回参数列表中,除去 sign、sign_type 两个参数外,凡是通知返回回来的参数皆是待验签的参数。

第二步: 将剩下参数进行 url_decode,然后进行字典排序,组成字符串,得到待签名字符串:

 app_id=2016092101248425&buyer_id=2088102114562585&buyer_pay_amount=0.80&fund_bill_list=[{"amount":"0.80","fundChannel":"ALIPAYACCOUNT"},{"amount":"0.20","fundChannel":"MDISCOUNT"}]&gmt_create=2016-10-12 21:36:12&gmt_payment=2016-10-12 21:37:19&invoice_amount=0.80&notify_id=7676a2e1e4e737cff30015c4b7b55e3kh6&notify_time=2016-10-12 21:41:23&notify_type=trade_status_sync&out_trade_no=mobile_rdm862016-10-12213600&passback_params=passback_params123&point_amount=0.00&receipt_amount=0.80&seller_id=2088201909970555&subject=PC网站支付交易&total_amount=1.00&trade_no=2016101221001004580200203978&trade_status=TRADE_SUCCESS&voucher_detail_list=[{"amount":"0.20","merchantContribute":"0.00","name":"5折券","otherContribute":"0.20","type":"ALIPAY_DISCOUNT_VOUCHER","voucherId":"2016101200073002586200003BQ4"}]

第三步: 将签名参数(sign)使用 base64 解码为字节码串。

第四步: 使用 RSA 的验签方法,通过签名字符串、签名参数(经过 base64 解码)及支付宝公钥验证签名。

第五步:需要严格按照如下描述校验通知数据的正确性

1、商户需要验证该通知数据中的 out_trade_no 是否为商户系统中创建的订单号;

2、判断 total_amount 是否确实为该订单的实际金额(即商户订单创建时的金额);

3、校验通知中的 seller_id(或者 seller_email) 是否为 out_trade_no 这笔单据的对应的操作方(有的时候,一个商户可能有多个 seller_id/seller_email);

4、验证 app_id 是否为该商户本身。

上述 1、2、3、4 有任何一个验证不通过,则表明本次通知是异常通知,务必忽略。 在上述验证通过后商户必须根据支付宝不同类型的业务通知,正确的进行不同的业务处理,并且过滤重复的通知结果数据。在支付宝的业务通知中,只有交易通知状态为 TRADE_SUCCESS 或 TRADE_FINISHED 时,支付宝才会认定为买家付款成功。

注意:

1、状态 TRADE_SUCCESS 的通知触发条件是商户签约的产品支持退款功能的前提下,买家付款成功;

2、交易状态 TRADE_FINISHED 的通知触发条件是商户签约的产品不支持退款功能的前提下,买家付款成功;或者,商户签约的产品支持退款功能的前提下,交易已经成功并且已经超过可退款期限。

异步通知验签:

Map<String, String> paramsMap = ... //将异步通知中收到的所有参数都存放到map中
 boolean signVerified = AlipaySignature.rsaCheckV1(paramsMap, ALIPAY_PUBLIC_KEY, CHARSET, SIGN_TYPE) //调用SDK验证签名
 if(signVerfied){
     // TODO 验签成功后,按照支付结果异步通知中的描述,对支付结果中的业务内容进行二次校验,校验成功后在response中返回success并继续商户自身业务处理,校验失败返回failure
 }else{
     // TODO 验签失败则记录异常日志,并在response中返回failure.
 }

六、将内网ip映射成公网ip

使用ngrok工具
参考文档:https://www.jianshu.com/p/571fdbc98d25

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 162,306评论 4 370
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,657评论 2 307
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 111,928评论 0 254
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,688评论 0 220
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,105评论 3 295
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 41,024评论 1 225
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,159评论 2 318
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,937评论 0 212
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,689评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,851评论 2 254
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,325评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,651评论 3 263
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,364评论 3 244
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,192评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,985评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,154评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,955评论 2 279

推荐阅读更多精彩内容