支付宝支付开票

完成 接入准备 后,商家/服务商可根据本文内容快速接入 支付开票

1 简介

消费者使用支付宝完成支付后,可在该笔交易的对应的页面直接发起开票请求,用户之后进入开票页面选择抬头,申请开具发票。商家开票平台开具发票,将发票插入到支付宝发票管家,发票管家会将发票交付并通知消费者。

1.1 名词解释

PID:partner_id 的简称,合作伙伴身份,支付宝账号对应的支付宝唯一用户号(支付宝账号的唯一标识),以2088开头的16位纯数字组成。获取步骤参见 查看 PID

SMID:针对间连支付的商家,商家支付是委托网商银行完成的(网商银行提供一层接口给商家或服务商,网商银行再和支付宝支付接口对接,这样会导致支付宝端能识别的支付商家是网商银行无法确认实际的商家),所以针对这类的交易商家,在交易中会有一个 SMID 字段来标识实际商家。

1.2 应用示例

支付开票的交互流程如下图所示。

1.3 开票入口

在支付宝端内和交易相关的页面提供开票入口,方便用户快速便捷的发起开票,具体的入口有如下三处,用户点击 开发票 按钮会跳转至支付宝标准开票页(服务商申请表中填入服务器地址即可,每个服务商有唯一地址,和商家无关)。

透出位置选择及信息配置:

  1. 默认三个通道全部开通,如果对投放渠道有需求可以在邮件申请权限时列出具体需求;
  2. 以上三个入口开发者可自由选择,其中 支付结果页 可单独选择是否透出、支付助手页及账单详情页是一体控制的,只能同时选择透出或同时不透出;
  3. logo 和显示名称支持配置,通过《配置申请表》提交由商家决定具体的 logo 和名称,该表会在开发者申请功能成功后邮件发送。申请权限的邮件模板见下文 前置条件 > 申请接入功能

1.4 模式介绍

支付开票根据商家支付方式的不同分为 支付后判断支付前判断 两种开票模式,两种模式不会影响开票入口展示透出。

1.4.1 支付后判断模式

支付后判断 模式下,开票传参数由支付宝的默认参数进行判定,适合大部分对开票信息没有特殊要求的商家。此模式开发票的入口判断放在支付完成后,由支付宝根据不同的维度进行判断:

  • 对于直连支付的交易支持 PID、收款账号 (sellerid)、门店 id(shopid) 三种维度的判断。
  • 对于间连支付的交易支持 SMID 维度的判断。

1.4.2 支付前判断模式

支付前判断 模式适用于商家门店较多,且对开票的后台信息有详细的要求。

  • 此模式开发票的入口判断需放在调用支付宝支付接口前,由商家委托开发支付功能的支付服务商进行接口入参改造。支付宝提供的所有支付接口均支持该模式,支付接口参见 支付 API
  • 在此模式下,调用支付宝支付接口时将开发票标志、品牌简称、商户门店简称同时传递给支付宝。

具体支付接口中传参字段如下:

{
    "invoice_info":{
        "key_info":{
            "is_support_invoice":"true",//是否支持开票  
            "invoice_merchant_name":"BEST_WONDER|BEST_WONDER",//品牌简称(m_short_name)|商户门店简称(sub_m_short_name)  不能带空格,必须要英文竖线中文无法校验,需与支付宝商户入驻接口入参一致
            "tax_num":"44010068329136"//税号  
        }
    }
}

说明:

  1. invoice_merchant_name 参数中品牌简称和商户门店简称为调用 alipay.ebpp.invoice.merchantlist.enter.apply(商户批量入驻申请接口)填写的品牌简称(m_short_name)和商户门店简称(sub_m_short_name);
  2. 优先推荐 支付后判断 模式,如果商家业务上类似有不同开票项目、不同税率的、部分商品才能开票的诉求请选择 支付前判断 模式;
  3. 支付前判断 模式,请提前协调好支付服务商进行改造处理。

2 前置条件

2.1 接入支付产品

如需使用支付开票功能,商家需接入支付宝收单支付功能(目前支付开票支持的支付功能包括:当面付手机网站支付生活缴费小程序支付 等)。

2.2 申请开通功能

正式接入支付开票功能前,商家/服务商需按照下方邮件模板要求发送邮件至 antinvoice@service.alipay.com 申请功能。

邮件格式如下:

  • 接入模式:【支付开票】
  • 主体:【公司名称】
  • 申请人:
  • 申请人手机号:
  • 公司支付宝账号:
  • 创建的APPID:
  • 公司简介:

说明:

  • 申请成功后支付宝将向您提供《配置申请表》,您需要如实填写以下信息:
    • 品牌及报销平台等简称只支持数字、英文大小写、下划线且不能存在敏感词汇。
    • logo 大小为 120 * 120 px。
  • 支付宝将在 2 个工作日内进行审核并通知您审核结果,审批通过后,您向支付宝提供的服务商简称 (s_short_name)、品牌简称(m_short_name)及品牌门店简称(sub_m_short_name)即可正式使用。
  • 后续接口的调用会需要使用到《配置申请表》中填写的信息,请妥善保管。

2.png

提交信息展示

说明:logo 及显示名称支持配置,由商家决定具体的 logo 和名称(通过【配置申请表】提交)。

image

3 功能接入

系统流程图

image

3.1 第一步:支付宝推送开票所需信息

  • 服务商需按照以下规范提供申请开票接口。当消费者在支付宝标准开票页提交开票申请之后,支付宝服务端会推送开票所需信息给服务商服务器的接口,由服务商检验开票参数,处理用户的开票申请。
  • 服务商需要自行验证参数的有效性,并且做幂等控制,接收到支付宝推送的开票信息之后,服务商需要实时返回受理申请的结果。
  • 支付宝端会对调用失败的申请进行重试,如有不可重试的错误需与支付宝进行约定。
  • 服务商接口仅受理开票申请,需马上返回结果是否申请成功,注意这里仅为提交开票申请的操作,而不是执行开票的操作,服务商需保证开票操作异步化,不要在该接口上执行同步开票操作。

说明:支付宝只调用开票申请接口,不调用开票接口,开票信息组装和接口调用由服务商自行设计。

3.1.1 服务商接口入参设计说明

参数名

参数类型

描述

applyIdString支付宝开票申请 id,该 id 为支付宝内的开票申请唯一标识,与订单号绑定。
invoiceAmountString开票金额
orderNoString订单号
mShortNameString商家的品牌名称简称,与发票回传接口的商户名称保持一致。
subShortNameString支付宝为商家分配的商户门店简称,与发票回传接口的商户名称保持一致。
payerNameString抬头名称
payerRegisterNoString纳税人识别号
payerAddressString地址
payerTelPhoneString电话
payerBankNameString开户行
payerBankAccountString账号
userEmailString用户邮箱
userMobileNoString用户手机号
signString签名,防篡改

注意:

支付开票场景中服务商需要生成 RSA2 方式及 RSA 各一套加签密钥,作用如下:

  • RSA2 方式加签密钥用于配置在开放平台控制台,在调用支付宝开票平台的接口使用,如:发票回传接口。
  • RSA 方式验签公钥需服务商在申请接入支付开票功能时提供给支付宝,服务商接收到开票申请推送后可用发票管家的公钥(由发票管家技术人员提供)进行验签操作,同步返回的申请结果使用服务商自己的私钥进行加签,支付宝端将使用服务商提供的公钥对申请结果进行验签,验签通过后会展示 商家开票中 状态。如下图所示:

3.1.2 服务商接口响应参数设计说明

参数名

参数类型

描述

resultMsgString业务结果说明
resultCodeString业务结果码
resultUrlString

用户提交申请之后,支付宝侧会提供入口让用户访问服务商提供的结果页面(可选,链接需进行 urlencode)。

signString签名

3.1.3 服务商接口结果码设计说明

如果支付宝推送的开票信息已经申请开票成功,服务商需明确返回申请开票成功,如果申请开票异常也需要返回明确的异常结果。

下面是结果码示例,服务商可以定义自己的错误码,但需要自定义的错误码需要跟支付宝端确认映射关系,错误码至少需要包含以下几种业务结果含义:

resultCoderesultMsg说明
APPLY_SUCCESS申请成功开票申请的信息校验无误,已提交开票。
INVOICE_PARAM_ILLEGAL开票参数非法加签验证不通过,金额不准确等开票参数异常。

3.2 第二步:服务商开票结果响应

3.2.1 开票成功

由于调用服务商开票申请接口是异步执行开票操作,服务商在开票成功时需调用 alipay.ebpp.invoice.info.send(发票信息回传接口(新版))并将支付宝开票申请 id(applyId)一并带回,用于将发票与用户的开票申请关联起来。

之后消费者便可以在支付宝端查看该发票信息或者下载发票 pdf 或 ofd 文件,接口的系统交互流程如下图所示:

1.png

调用代码
package com.java.sdk.demo;

import com.alipay.api.AlipayApiException;
import com.alipay.api.AlipayClient;
import com.alipay.api.DefaultAlipayClient;
import com.alipay.api.AlipayConfig;
import com.alipay.api.domain.InvoiceItemOpenModel;
import com.alipay.api.request.AlipayEbppInvoiceInfoSendRequest;
import com.alipay.api.domain.InvoiceSendOpenModel;
import com.alipay.api.response.AlipayEbppInvoiceInfoSendResponse;
import com.alipay.api.domain.AlipayEbppInvoiceInfoSendModel;
import com.alipay.api.domain.EinvTrade;
import com.alipay.api.domain.InvoiceTitleOpenModel;

import com.alipay.api.FileItem;
import java.util.Base64;
import java.util.ArrayList;
import java.util.List;
import java.util.TimeZone;
import java.text.ParseException;
import java.text.SimpleDateFormat;

public class AlipayEbppInvoiceInfoSend {

    public static void main(String[] args) throws AlipayApiException, ParseException {
        // 初始化SDK
        AlipayClient alipayClient = new DefaultAlipayClient(getAlipayConfig());
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        sdf.setTimeZone(TimeZone.getTimeZone("GMT+8"));

        // 构造请求参数以调用接口
        AlipayEbppInvoiceInfoSendRequest request = new AlipayEbppInvoiceInfoSendRequest();
        AlipayEbppInvoiceInfoSendModel model = new AlipayEbppInvoiceInfoSendModel();
        
        // 设置开票商户品牌简称
        model.setMShortName("XSD");
        
        // 设置开票商户门店简称
        model.setSubMShortName("XSD_HL");
        
        // 设置发票信息列表
        List<InvoiceSendOpenModel> invoiceInfoList = new ArrayList<InvoiceSendOpenModel>();
        InvoiceSendOpenModel invoiceInfoList0 = new InvoiceSendOpenModel();
        invoiceInfoList0.setTaxAmount("1.00");
        invoiceInfoList0.setSumAmount("101.00");
        invoiceInfoList0.setOpenId("074a1CcTG1LelxKe4xQC0zgNdId0nxi95b5lsNpazWYoCo5");
        InvoiceTitleOpenModel invoiceTitle = new InvoiceTitleOpenModel();
        invoiceTitle.setPayerBankNameAccount("中国建设银行11111111");
        invoiceTitle.setPayerAddressTel("杭州市西湖区天目山路黄龙时代广场0571-11111111");
        invoiceTitle.setPayerRegisterNo("9133010060913454XP");
        invoiceTitle.setTitleName("支付宝(中国)网络技术有限公司");
        invoiceInfoList0.setInvoiceTitle(invoiceTitle);
        invoiceInfoList0.setFileDownloadUrl("http://img.hadalo.com/aa/kq/ddhrtdefgxKVXXXXa6apXXXXXXXXXX.pdf");
        invoiceInfoList0.setChecker("李四");
        invoiceInfoList0.setPayeeAddressTel("杭州市西湖区某某办公楼 0571-237405862");
        invoiceInfoList0.setPayeeBankNameAccount("西湖区建行11111111111");
        invoiceInfoList0.setInvoiceDate("2017-10-10");
        invoiceInfoList0.setPayee("张三");
        List<EinvTrade> tradeList = new ArrayList<EinvTrade>();
        EinvTrade tradeList0 = new EinvTrade();
        tradeList0.setOutJson("透传字段");
        tradeList0.setPaymentTime(sdf.parse("2019-12-24 00:11:12"));
        tradeList0.setPaymentAmount("20.22");
        tradeList0.setMerchantName("肯德基");
        tradeList0.setBillNo("20200615102930XXXXX00");
        tradeList0.setBillTime(sdf.parse("2020-06-15 22:00:13"));
        tradeList0.setCityName("北京市");
        tradeList0.setSouce("itinerary");
        tradeList0.setDownloadUrl("http://img.hadalo.com/aa/kq/ddhrtdefgxKVXXXXa6apXXXXXXXXXX.pdf");
        tradeList0.setPayeeName("蚂蚁金服(杭州)网络技术有限公司");
        tradeList0.setTradeType("hotel");
        tradeList0.setExtendMap("k=v\nk2=v2");
        tradeList0.setDetailJson("账单明细信息,酒店水单信息,行程单信息,餐饮小票信息");
        tradeList.add(tradeList0);
        invoiceInfoList0.setTradeList(tradeList);
        invoiceInfoList0.setOriBlueInvCode("4112740002");
        invoiceInfoList0.setFileDownloadType("PDF");
        List<InvoiceItemOpenModel> invoiceContent = new ArrayList<InvoiceItemOpenModel>();
        InvoiceItemOpenModel invoiceContent0 = new InvoiceItemOpenModel();
        invoiceContent0.setItemUnit("台");
        invoiceContent0.setRowType("0");
        invoiceContent0.setItemTaxAmount("1.00");
        invoiceContent0.setItemSpec("G39");
        invoiceContent0.setItemSumAmount("101.00");
        invoiceContent0.setItemUnitPrice("100.00");
        invoiceContent0.setItemName("餐饮费");
        invoiceContent0.setItemExTaxAmount("100.00");
        invoiceContent0.setItemTaxRate("0.01");
        invoiceContent0.setItemQuantity(1L);
        invoiceContent0.setItemNo("1010101990000000000");
        invoiceContent.add(invoiceContent0);
        invoiceInfoList0.setInvoiceContent(invoiceContent);
        invoiceInfoList0.setCheckCode("15170246985745164986");
        invoiceInfoList0.setApplyId("2016112800152005000000000239");
        invoiceInfoList0.setInvoiceType("BLUE");
        invoiceInfoList0.setExtendFields("m_invoice_detail_url=http://196.021.871.011:8080/invoice/detail.action?fpdm= 4112740003&fphm=41791003");
        invoiceInfoList0.setPayeeRegisterName("支付宝(杭州)信息技术有限公司");
        invoiceInfoList0.setFinancialElectronicType("01");
        invoiceInfoList0.setInvoiceMemo("订单号:2017120800001");
        invoiceInfoList0.setOutInvoiceId("201710283459661232435535");
        invoiceInfoList0.setOriBlueInvNo("41791002");
        invoiceInfoList0.setPayeeRegisterNo("310101000000090");
        invoiceInfoList0.setInvoiceNo("41791003");
        invoiceInfoList0.setInvoiceCode("4112740003");
        invoiceInfoList0.setClerk("赵吴");
        invoiceInfoList0.setOutTradeNo("20171023293456785924325");
        invoiceInfoList0.setInvoiceKind("PLAIN");
        // uid参数未来计划废弃,存量商户可继续使用,新商户请使用openid。请根据应用-开发配置-openid配置选择支持的字段。
        // invoiceInfoList0.setUserId("2088399922382233");
        invoiceInfoList0.setExTaxAmount("100.00");
        invoiceInfoList.add(invoiceInfoList0);
        model.setInvoiceInfoList(invoiceInfoList);
        
        request.setBizModel(model);
        AlipayEbppInvoiceInfoSendResponse response = alipayClient.execute(request);
        System.out.println(response.getBody());

        if (response.isSuccess()) {
            System.out.println("调用成功");
        } else {
            System.out.println("调用失败");
            // sdk版本是"4.38.0.ALL"及以上,可以参考下面的示例获取诊断链接
            // String diagnosisUrl = DiagnosisUtils.getDiagnosisUrl(response);
            // System.out.println(diagnosisUrl);
        }
    }

    private static AlipayConfig getAlipayConfig() {
        String privateKey  = "<-- 请填写您的应用私钥,例如:MIIEvQIBADANB ... ... -->";
        String alipayPublicKey = "<-- 请填写您的支付宝公钥,例如:MIIBIjANBg... -->";
        AlipayConfig alipayConfig = new AlipayConfig();
        alipayConfig.setServerUrl("https://openapi.alipay.com/gateway.do");
        alipayConfig.setAppId("<-- 请填写您的AppId,例如:2019091767145019 -->");
        alipayConfig.setPrivateKey(privateKey);
        alipayConfig.setFormat("json");
        alipayConfig.setAlipayPublicKey(alipayPublicKey);
        alipayConfig.setCharset("UTF-8");
        alipayConfig.setSignType("RSA2");
        return alipayConfig;
    }
}

注意:

  • 有了 applyId 就无需填写 userId,因为开票申请 id 可以关联到用户。
  • out_trade_no 必须与支付宝推送的 orderNo 一致。

更多参数及响应示例详情可查看 alipay.ebpp.invoice.info.send(发票信息回传接口(新版))文档。

3.2.2 开票失败

如果开票失败服务商需调用 alipay.ebpp.invoice.apply.result.sync(ISV向支付宝同步发票申请结果接口),返回开票结果。

示例代码
package com.java.sdk.demo;

import com.alipay.api.AlipayApiException;
import com.alipay.api.AlipayClient;
import com.alipay.api.DefaultAlipayClient;
import com.alipay.api.AlipayConfig;
import com.alipay.api.request.AlipayEbppInvoiceApplyResultSyncRequest;
import com.alipay.api.domain.AlipayEbppInvoiceApplyResultSyncModel;
import com.alipay.api.response.AlipayEbppInvoiceApplyResultSyncResponse;

import com.alipay.api.FileItem;
import java.util.Base64;
import java.util.ArrayList;
import java.util.List;

public class AlipayEbppInvoiceApplyResultSync {

    public static void main(String[] args) throws AlipayApiException {
        // 初始化SDK
        AlipayClient alipayClient = new DefaultAlipayClient(getAlipayConfig());

        // 构造请求参数以调用接口
        AlipayEbppInvoiceApplyResultSyncRequest request = new AlipayEbppInvoiceApplyResultSyncRequest();
        AlipayEbppInvoiceApplyResultSyncModel model = new AlipayEbppInvoiceApplyResultSyncModel();
        
        // 设置支付宝向税控商或ISV发起发票申请后
        model.setResult("SUCCESS");
        
        // 设置结果描述
        model.setResultMsg("成功");
        
        // 设置支付宝发起开票申请的id
        model.setApplyId("2023000000000000000000000001");
        
        // 设置结果码
        model.setResultCode("SUCCESS");
        
        // 设置该字段是税控商或ISV收到支付宝开票请求后生成的申请id
        model.setTaxApplyId("20160707399929991001");
        
        request.setBizModel(model);
        AlipayEbppInvoiceApplyResultSyncResponse response = alipayClient.execute(request);
        System.out.println(response.getBody());

        if (response.isSuccess()) {
            System.out.println("调用成功");
        } else {
            System.out.println("调用失败");
            // sdk版本是"4.38.0.ALL"及以上,可以参考下面的示例获取诊断链接
            // String diagnosisUrl = DiagnosisUtils.getDiagnosisUrl(response);
            // System.out.println(diagnosisUrl);
        }
    }

    private static AlipayConfig getAlipayConfig() {
        String privateKey  = "<-- 请填写您的应用私钥,例如:MIIEvQIBADANB ... ... -->";
        String alipayPublicKey = "<-- 请填写您的支付宝公钥,例如:MIIBIjANBg... -->";
        AlipayConfig alipayConfig = new AlipayConfig();
        alipayConfig.setServerUrl("https://openapi.alipay.com/gateway.do");
        alipayConfig.setAppId("<-- 请填写您的AppId,例如:2019091767145019 -->");
        alipayConfig.setPrivateKey(privateKey);
        alipayConfig.setFormat("json");
        alipayConfig.setAlipayPublicKey(alipayPublicKey);
        alipayConfig.setCharset("UTF-8");
        alipayConfig.setSignType("RSA2");
        return alipayConfig;
    }
}

重要入参说明:

  • apply_id:必填,支付宝发起开票申请的 id,该id具有唯一性。具体值由支付宝向税控发起开票申请时传递,作为支付宝向税控开票申请的唯一标志。详情可查看 第一步:支付宝推送开票所需信息
  • tax_apply_id:税控商或服务商收到支付宝开票请求后自定义生成的申请 id,需保证该 id 唯一。 若服务商接入时是按照 tax_apply_id 来查询发票信息,则该字段必填。
  • result:必填,支付宝向税控商或服务商发起发票申请后,对应这笔申请的发票开具结果。取值如下:
    • SUCCESS:成功。
    • FAIL:失败。
  • result_code:结果码,支付宝向税控商或服务商发起发票申请后,对应这笔申请的发票开具结果进行详细说明的结果码。取值如下:
    • SUCCESS:成功。
    • PARAMETER_ILLEGAL:参数不合法。
    • MERCHANT_TAX_DEVICE_ERROR:商户税控设备异常。

3.3 加签验签说明

3.3.1 加签

  1. 排序:将筛选的参数按照第一个字符的键值 ASCII 码递增排序(字母升序排序),如果遇到相同字符则按照第二个字符的键值ASCII码递增排序,以此类推;
  2. 拼接:将排序后的参数与其对应值,组合成“参数=参数值”的格式,并且把这些参数用&字符连接起来,此时生成的字符串为待签名字符串;
  3. 生成 sign:用 SHA1 算法对待签名字符串计算摘要,然后使用 RSA 私钥对摘要进行加密,得到 sign;
  4. 生成最终参数:sign 的值放到参数 map 中一起生成 queryString 格式的字符串。
待加签字符串
customParam2=自定义参数2&invoiceAmount=10.75&mShortName=xiaokai&orderNo=123456&productCode=PAYMENT_OPEN&resultUrl=http%3A%2F%2Fdummy.com&subShortName=xiaokai
加签后最终参数
customParam2=自定义参数2&invoiceAmount=10.75&mShortName=xiaokai&orderNo=123456&productCode=PAYMENT_OPEN&resultUrl=http%3A%2F%2Fdummy.com&sign=NMd%2Fm%2B2TlaQoMPhj%2BmYHOrgWgOM02PNKqeX0xxKj3DmvrXbCdvgfXmyzzyOOAAxyftYTcFzLfV0CNhwsuZZcoJHAzSLAZAi%2BoyonRhKGL2pGpYJMsJy8qXdkkzj9tIXlBa2n6jJg%2BdWLdZjUGRjPCrYGbGzvwcHd51ikMn0wB10%3D&subShortName=xiaokai

3.3.2 验签

  1. 获取参数中的 sign:从参数的最后面截取 sign;
  2. 解密sign:用公钥解密 sign 得到摘要1;
  3. 对比摘要:用 SHA1 算法对参数(排除sign)计算摘要2,如果摘要1与摘要2相等,说明参数未被篡改,否则说明参数被篡改。

3.3.3 示例代码

/**
 * Alipay.com Inc.
 * Copyright (c) 2004-2017 All Rights Reserved.
 */
package com.alipay.antinvoice.common.util;
import org.apache.commons.codec.binary.Base64;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
import java.security.spec.EncodedKeySpec;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
/**
 * RSA加密算法工具类
 * @author xingqian.xq
 * @version $Id: RSAUtil.java, v 0.1 2017-01-13 下午3:16 xingqian.xq Exp $$
 */
public class RSAUtil {
    /**
     * 加签
     * @param data  待加签数据
     * @param privateKey  私钥
     * @return  签名
     * @throws Exception  异常
     */
    public static String sign(String data, PrivateKey privateKey) throws Exception {
        byte[] keyBytes = privateKey.getEncoded();
        PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(keyBytes);
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        PrivateKey privateK = keyFactory.generatePrivate(pkcs8KeySpec);
        Signature signature = Signature.getInstance("SHA1withRSA");
        signature.initSign(privateK);
        signature.update(data.getBytes("GBK"));
        return new String(Base64.encodeBase64(signature.sign()));
    }
    /**
     * 验签
     * @param publicKey  公钥
     * @param srcData  原始字符串
     * @param sign  签名
     * @return  是否验签通过
     * @throws Exception  异常
     */
    public static boolean verify(String srcData, PublicKey publicKey, String sign) throws Exception {
        byte[] keyBytes = publicKey.getEncoded();
        X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        PublicKey publicK = keyFactory.generatePublic(keySpec);
        Signature signature = Signature.getInstance("SHA1withRSA");
        signature.initVerify(publicK);
        signature.update(srcData.getBytes("GBK"));
        return signature.verify(Base64.decodeBase64(sign.getBytes()));
    }
    /**
     * 获取密钥对
     * @return  密钥对
     * @throws Exception  异常
     */
    public static KeyPair getKeyPair() throws Exception {
        KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
        generator.initialize(1024);
        KeyPair pair = generator.generateKeyPair();
        return pair;
    }
    /**
     * 获取私钥
     *
     * @param privateKey  私钥字符串
     * @return 私钥对象
     */
    public static PrivateKey getPrivateKey(String privateKey) throws Exception {
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        byte[] encodedKey = privateKey.getBytes();
        encodedKey = Base64.decodeBase64(encodedKey);
        EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(encodedKey);
        return keyFactory.generatePrivate(keySpec);
    }
    /**
     * 获取公钥
     * @param publicKey   公钥字符串
     * @return 公钥对象
     * @throws Exception 异常
     */
    public static PublicKey getPublickey(String publicKey) throws Exception {
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        X509EncodedKeySpec bobPubKeySpec = new X509EncodedKeySpec(Base64.decodeBase64(publicKey
            .getBytes()));
        return keyFactory.generatePublic(bobPubKeySpec);
    }
}
/**
 * Alipay.com Inc.
 * Copyright (c) 2004-2018 All Rights Reserved.
 */
package com.alipay.antinvoice.core.service.stripper;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.alipay.antinvoice.common.util.RSAUtil;
/**
 * @author hansong.xhs
 * @version $Id: SignTest.java, v 0.1 2018年05月03日 下午8:36 hansong.xhs Exp $
 */
public class SignTest {
    public static String privateKey = "MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBAIgp2pq98axr1IcLrsW5MPUqeggdeGPAyh8+2aHObn4ER6KmpTgo+pi2SScFoNgesWx72kY2PEEGsUwcTXKkSYaeZVs7z7Z/ujQvP0kbPGouG7Q6MN5+jud0sM2Gkmu60fGJXQ8IfeFi6oQCKZpMviXg00KV7ct6JmHlgUpP0jR5AgMBAAECgYBlxAo6/t1iBVFZATVFV4ysn2uHJyd0PoGR6rJTSWqxSleTy8LN/2qTuiFgRceZ3w6xyrsvIJfV7b+S59BGb1z3ZdO09gOeviP5W3pC23ClItBlsyNf4njXylQus9Nl4ZnKcV/UEDvjChGIma8ZZChkxHNpLls9WGWFkQkFk88TkQJBALuOBCgCVyI1Lm2r0mPlC7RbuPiKzmT5kOs0+1696WulbRMODPGXsz8ivMJSBQy+ehU6xV//nd5GOAfjt5PqRw0CQQC52rJZdqPGxHhESw8eI5ZBe9Mz7b6T2UKuPJch6xwpiMPlQB7p0hk5Nhnyic01fppG3gpP6MEtHwiTOTMwXsgdAkAqzpkoQJB+oEC+i07zud1YBu9K2vOMnGF1LZyJ3TKffRxOExDlO0iQCm+msm2woPDgU4+k/4SarNAxDMpjmj8pAkA6/1WGWMb8nfmflEQkSR+1gd01qs7ImDs2nD1Noxi5hpTI/WXSy8L+ClKKT3w48wt+W5Xib/yCmktakNnTDQNxAkBbXimHdGVY3GrZszuAN1n3cyafBSTqpFUdqhfRZ/QLj7wJcmJ+PrLpyB6KMZVsnjzOCS9/tWtIZi14ynhOvM2l";
    public static String publicKey  = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCIKdqavfGsa9SHC67FuTD1KnoIHXhjwMofPtmhzm5+BEeipqU4KPqYtkknBaDYHrFse9pGNjxBBrFMHE1ypEmGnmVbO8+2f7o0Lz9JGzxqLhu0OjDefo7ndLDNhpJrutHxiV0PCH3hYuqEAimaTL4l4NNCle3LeiZh5YFKT9I0eQIDAQAB";
    public static void main(String[] args) throws Exception {
        String params = sign(RSAUtil.getPrivateKey(privateKey));
        System.out.println(params);
        verify(params, RSAUtil.getPublickey(publicKey));
    }
    public static void verify(String params, PublicKey publicKey) throws Exception {
        //解析参数
        Map<String, String> map = restoreMap(params, "&");
        //urldecode 签名
        String sign = URLDecoder.decode(map.get("sign"), "utf-8");
        //去除签名参数
        map.remove("sign");
        //生成待验签字符串
        String signatureContent = getSignatureContent(map);
        System.out.println(signatureContent);
        System.out.println(RSAUtil.verify(signatureContent, publicKey, sign));
    }
    /**
     * 加签demo方法
     */
    public static String sign(PrivateKey privateKey) {
        try {
            //获取待验签原始数据
            Map<String, String> params = new HashMap<String, String>();
            params.put("invoiceAmount", "开票金额");
            params.put("orderNo", "订单号");
            params.put("mShortName", "商户的品牌名称简称");
            params.put("subShortName", "商户门店简称");
            params.put("customParam1", "自定义参数1");
            params.put("customParam2", "自定义参数2");
            //获取原始加签内容
            String signatureContent = getSignatureContent(params);
            //获取签名,得到签名之后进行urlencode
            String sign = URLEncoder.encode(RSAUtil.sign(signatureContent, privateKey), "utf-8");
             params.put("sign", sign);
            return getSignatureContent(params);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return "";
    }
    /**
     * 获得需要签名的数据,按照参数名字母升序的顺序将所有参数用&连接起来
     * @param params 待签名参数集
     * @return 排好序的待签名字符串
     */
    public static String getSignatureContent(Map<String, String> params) {
        if (params == null) {
            return null;
        }
        StringBuffer content = new StringBuffer();
        List<String> keys = new ArrayList<String>(params.keySet());
        Collections.sort(keys);
        for (int i = 0; i < keys.size(); i++) {
            String key = keys.get(i);
            String value = params.get(key);
            content.append((i == 0 ? "" : "&") + key + "=" + value);
        }
        return content.toString();
    }
    /**
     * 把数据库里边String型的扩展字段转换成map型
     * @param extension 需要转换的String
     * @param spiltorKey 指定的字符串
     * @return      转换后的扩展MAP
     */
    public static Map<String, String> restoreMap(String extension, String spiltorKey) {
        Map<String, String> extensionMap = new HashMap<String, String>();
        String[] fieldList = extension.split(spiltorKey);
        for (String field : fieldList) {
            String[] entry = field.split("=");
            if (entry.length >= 2) {
                if (entry.length > 2) {
                    //如果包含多个“=”取第一个后边的为value,如URL后边的
                    for (int i = 2; i < entry.length; i++) {
                        entry[1] += ("=" + entry[i]);
                    }
                }
                extensionMap.put(entry[0], entry[1]);
            }
        }
        return extensionMap;
    }
}

参考文档:

小程序文档 - 支付宝文档中心

  • 7
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值