☰
特约商户进件API开发实战:签名加密与异步查询链路解析
2026/10/2 18:21:51 网站建设 项目流程

简介:这是一份面向微信支付服务商与特约商户进件场景的 PHP 版简易 Demo,基于 API v3 实现特约商户进件、查询等接口调用,适合需要快速接入微信支付进件流程或理解服务商接口调用逻辑的开发者参考。压缩包共 3 个文件,以 2 个 PHP 源码文件为主,分别承担接口封装与示例调用逻辑,另附 1 个 TXT 文档说明 API 证书存放方式,整体仅 8KB,结构精简。代码全部使用中文注释,原作者对关键步骤和参数说明较为详细,能帮助初学者理清特约商户/小微进件的请求参数、证书配置与查询流程;示例本身不复杂,但足以作为联调起点。目前已有 1458 人学习,适合具备一定 PHP 基础、想对照微信支付官方文档完成进件接口对接或二次开发的开发者。

1. 特约商户进件API demo:进件、查询链路为什么最难一次跑通

特约商户进件API听起来就是“把商户资料传上去、再查一下结果”,但真正动手接的时候,大多数后端开发第一次都会卡在验签失败或者回调超时上。进件接口和普通的查询接口不一样,它一次提交的动作会引出一条完整的异步审核链路:平台先受理,再做资质审核、结算账户校验、终端配置,最后才把开通结果告诉你。这中间任何一环出问题,都不会在第一次请求的响应里体现,只能靠查询接口和回调去追。这篇笔记围绕进件、查询这两个接口的demo展开,把报文组装、签名、加密、状态机、轮询和排错讲完整,适合正在对接收单机构进件接口的后端开发,也给做商户管理后台的团队一个可复现的最小实现。你看完能照着搭出一个自己的demo,并且知道每个参数为什么这么填。

2. 进件接口协议拆解:签名机制、敏感字段加密与状态流转

2.1 报文结构:公共参数、业务data与msg_no的约束

特约商户进件接口的请求报文一般分两层:最外层是公共参数,里面是一个业务数据块。公共参数通常包含app_id、msg_no、timestamp、sign、sign_type,业务数据块在JSON里叫data。data内部放的才是进件真正的业务字段,比如商户名称、经营品类、法人信息、结算账户。这种结构把“每次请求都要带的认证信息”和“业务上真正变化的字段”拆开,接口平台侧校验公共参数时可以用同一套逻辑处理所有接口,业务参数的变化不会影响签名框架。

msg_no在这套结构里是一个容易被低估的字段。它要求每次请求唯一,作用是幂等和排重。进件接口尤其依赖它,因为网络重试时如果msg_no不换,平台可以判定这是同一次请求,避免同一笔进件被重复处理。实际对接中很多新手图省事,把msg_no用随机数拼一下就提交,结果日志排查时根本分不清哪条请求对应哪次业务操作。我一般建议msg_no的规则做成“业务方标识+日期+自增序号”,比如ONB20250617001,这样翻开日志就能一眼定位到是哪个渠道在几点几分提交的第几次进件。

再看data内部的业务参数,不同收单机构的字段名会有差异,但核心字段基本一致:merchant_name(商户名称)、merchant_short_name(商户简称)、business_code(经营品类编码)、legal_person_name(法人姓名)、legal_person_cert_no(法人证件号)、settle_bank_account(结算账户)、settle_rate(结算费率)。这里有一个共同约束:所有证件号、银行卡号、手机号都属于敏感字段,不能明文直接放在data里,见2.3。把进件、查询、回调三个接口放在一起看,它其实是典型的RESTful API接口规范:提交类用POST、查询类用GET或POST、异步结果通过回调推送,理解了这一点,接口的联调思路就顺了。

2.2 签名与密钥:先用一张表说清四个文件分别干嘛

接入特约商户进件接口前,建议先把密钥体系理清楚。进件接口常用SHA256withRSA做请求签名,有些机构已经升级到国密SM2,但整体思路没有变化:请求方用私钥签名,平台用请求方的公钥验签;平台响应用平台的私钥签名,请求方用平台公钥验签。一个完整的接入配置里会出现四个和密钥相关的对象,拿demo之前先对照下表确认自己手里有哪些:

密钥/证书由谁生成用途存放位置
商户私钥接入方生成对请求报文签名接入方服务器,不上传
商户公钥接入方生成平台侧验签用上传给收单机构
平台公钥收单机构提供验签平台响应、加密敏感字段配置在demo里
平台私钥收单机构持有对响应和回调签名机构侧保存,不对外

这个表里最容易弄反的是“商户私钥”和“平台公钥”的分工。私钥只属于自己,公钥才需要交给对方;验签永远用对方公钥。demo工程里一般有一个config.properties或application.yml存放app_id、商户私钥、平台公钥,这三个值的来源不同,建议一拿到就分开标注,不要混在一起。我在对接时习惯把商户公钥上传这件事列为前置条件,因为有些新手拿着demo直接跑,忽略了公钥未上传导致平台验签失败,平台返回的报错却是“请求来源不合法”,很难排查。

2.3 敏感字段加密:身份证与结算卡号的上送方式

进件接口对敏感字段的上送方式,常见做法是用平台公钥做非对称加密。具体流程是把证件号等明文字符串用平台公钥RSA加密,对加密后的字节做Base64编码,再放进data字段。这意味着data里同时存在两种类型的字段:普通明文业务字段和加密后的密文。密文的长度会比明文长很多,JSON序列化后整个data体积变大,调试日志里会看到一长串Base64字符,这属于正常现象。

// 用平台公钥加密敏感字段,RSA/ECB/PKCS1Padding 是支付行业最常见的组合 public static String encryptSensitive(String plainText, PublicKey platformPublicKey) throws Exception { Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding"); cipher.init(Cipher.ENCRYPT_MODE, platformPublicKey); byte[] encrypted = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8)); return Base64.getEncoder().encodeToString(encrypted); }

逻辑说明:加密前先把明文字符串用UTF-8转成字节,避免身份证号里的数字和字母在默认编码环境下出现不一致;加密结果是二进制字节流,直接用Base64编码才能放进JSON字符串。

参数说明:RSA加密有长度上限,PKCS1Padding模式单次最多加密117字节,身份证号18位、银行卡号19位都不会超,但要注意进件里如果有对公账户开户许可证号码这类长编号,可能超过阈值,需要按机构文档换用分段加密或者改用SM2方案。这里我建议加密时对明文做一次trim和大写处理,见第5章。

2.4 进件状态机:从受理到开通要经过几个节点

进件接口返回的受理结果和最终开通结果不是一回事。第一次提交成功后,响应里通常只有受理状态,例如status=1表示“进件受理中”。之后平台开始审核,状态会依次经过审核中、审核通过(或拒绝)、开通成功(或失败)。这个链路要提前在demo里建模,否则后续查询接口写出来也只能照着文档抄,没法判断什么时候算结束。

常见的进件状态值定义如下:0表示已受理,1表示审核中,2表示审核通过,3表示审核拒绝,4表示开通成功,5表示开通失败。有些机构会额外细分出“待补充材料”和“已签约”两个中间态,字段含义要以你们对接的那份接口文档为准。这里有一个关键点:状态是单向流转的,一般不会从“审核拒绝”跳回“审核中”,因为被拒后需要重新发起进件,拿到的是新的进件单号。所以demo里对状态的判断只需要处理两个分支——终态和中间态。

进件过程中还有一类状态容易被误判:审核通过(2)和开通成功(4)之间的时间差。审核通过只代表资质审核过了,商户级权限开通、终端参数下发、结算账户打款验证这些环节还没有完成。我在服务端写查询逻辑时,会把2和4都当成“还没结束”,只有4和5才视为终态。这样设计轮询逻辑才不会提前退出。

3. 进件提交接口demo:用最小Java工程跑通第一次返回

3.1 demo的工程结构与三个类

做进件接口demo程序不需要引入复杂的微服务框架,一个普通的Java工程配好依赖就够了。我习惯只保留三个类:一个用于加载配置和组装请求,一个封装签名和加密工具,一个封装HTTP发送。这样看代码的人不会被Spring容器里的注入关系干扰,能直接看到进件接口调用的完整链路。

merchant-onboard-demo/ ├── pom.xml ├── src/main/java/com/demo/onboard/ │ ├── MerchantOnboardClient.java # 进件+查询主流程 │ ├── SignUtil.java # 签名、验签、加密 │ └── HttpUtil.java # POST JSON 请求 └── src/main/resources/ └── config.properties # app_id、私钥、平台公钥

pom.xml里只需要两个依赖,一个发HTTP请求用OkHttp,一个做JSON序列化用Jackson。不要去引一堆Spring全家桶,进件demo的价值在于逻辑清晰,而不是架构完整。config.properties里的key命名固定住:app_id、merchant_private_key、platform_public_key,私钥和公钥统一去掉PKCS8头尾换行再存,避免加载时出现格式问题。

3.2 组装进件参数并生成签名

进件请求的组装顺序直接影响签名结果。下面这段代码先构造业务data,再和公共参数拼成待签名串,最后用商户私钥签名。注意data是作为一个整体JSON字符串参与签名的,这是很多demo的默认约定,但不同机构的处理方式不一样,你要以文档为准。

// 组装进件参数并生成签名 public String buildSubmitRequest(MerchantInfo merchant, Config config) throws Exception { // 1. 先构造业务参数data Map<String, Object> data = new TreeMap<>(); data.put("out_merchant_no", merchant.getOutMerchantNo()); // 接入方自己的商户编号 data.put("merchant_name", merchant.getName()); data.put("merchant_short_name", merchant.getShortName()); data.put("business_code", merchant.getBusinessCode()); data.put("legal_person_name", merchant.getLegalPersonName()); // 敏感字段先加密 data.put("legal_person_cert_no", SignUtil.encryptSensitive( merchant.getLegalPersonCertNo(), config.getPlatformPublicKey())); // 2. 公共参数使用TreeMap保证key有序 Map<String, String> common = new TreeMap<>(); common.put("app_id", config.getAppId()); common.put("msg_no", buildMsgNo(merchant.getOutMerchantNo())); common.put("timestamp", String.valueOf(System.currentTimeMillis())); // 3. 把data的JSON字符串作为公共参数里的一项参与签名 ObjectMapper mapper = new ObjectMapper(); String dataJson = mapper.writeValueAsString(data); common.put("data", dataJson); // 4. 拼接待签名串:key1=value1&key2=value2 StringBuilder sb = new StringBuilder(); for (Map.Entry<String, String> e : common.entrySet()) { if (sb.length() > 0) sb.append("&"); sb.append(e.getKey()).append("=").append(e.getValue()); } String signString = sb.toString(); String sign = SignUtil.rsaSign(signString, config.getMerchantPrivateKey()); // 5. 最终请求体里带上sign common.put("sign", sign); return mapper.writeValueAsString(common); }

逻辑说明:第一步把业务参数放进TreeMap,目的是让data内部的key有序,序列化出的JSON字符串是固定的,不会因字段顺序变化导致签名不稳定。第三步把整个dataJson字符串当作公共参数里的一个普通value拼入待签名串,这样签名范围覆盖全部业务字段,平台验签时能发现任何一个字段被篡改。

参数说明:out_merchant_no是接入方自己的商户内部编号,进件时带上它,后续查询接口可以直接用它反查进件结果,不必每次都记住平台生成的apply_no。msg_no每次请求必须不同,这里用out_merchant_no加时间戳拼接,保证同一商户不同时间提交的请求也能区分。签名算法默认SHA256withRSA,商户私钥从配置里读取时要去掉PKCS8头尾。

3.3 发送请求并解析返回:拿到apply_no只是开始

请求体组装完后,发送这一步用OkHttp封装非常简洁。下面是发送和解析响应的代码,关键在两点:响应体也要验签,以及响应里的status只是受理状态。

// 发送进件请求并验签 public SubmitResult submit(MerchantInfo merchant, Config config) throws Exception { String requestBody = buildSubmitRequest(merchant, config); String responseBody = HttpUtil.postJson( "https://open.example.com/merchant/submit", requestBody, 5000); // 1. 解析响应公共参数 ObjectMapper mapper = new ObjectMapper(); JsonNode node = mapper.readTree(responseBody); String respSign = node.get("sign").asText(); String respData = node.get("data").toString(); // 2. 用平台公钥验签,验签串只包含平台返回的公共参数和data boolean valid = SignUtil.rsaVerify(respData, respSign, config.getPlatformPublicKey()); if (!valid) { throw new RuntimeException("平台响应验签失败"); } // 3. 解析data里的进件结果 JsonNode data = node.get("data"); SubmitResult result = new SubmitResult(); result.setApplyNo(data.get("apply_no").asText()); result.setStatus(data.get("status").asInt()); return result; }

逻辑说明:响应验签用data的JSON字符串做待验签内容,和平台文档约定的验签范围必须一致。有些平台会把验签串定义为“去掉sign字段后的所有key=value拼接”,和请求侧的签名方式不完全一样,写这段逻辑前先看响应说明。

参数说明:超时时间我设置的是5秒,进件接口走的是人工审核流程,平台受理响应一般都在这个时间范围内;如果你对接的平台进件接口内部会同步做工商校验,建议放宽到10秒,但不要太长,慢请求应该交给查询接口去兜底,而不是死等一次响应。

这一段还有一个容易被忽略的点:apply_no拿到之后要立即落库。它是后面所有查询、回调、工单处理的主键。如果程序在响应验签完成后、落库前崩溃了,可以靠msg_no幂等重新提交一次,平台会返回同一笔apply_no,不会生成重复进件单。

4. 查询接口的轮询与状态比对:进件单号拿到后别干等

4.1 查询入参选哪个:按进件单号还是按业务方商户号

进件提交后,查询接口是排错的必经之路。查询接口的入参一般是两个方向:按apply_no查询,或者按out_merchant_no查询。apply_no是平台生成的进件单号,一笔进件唯一;out_merchant_no是接入方自己的商户编号,在进件请求里主动带上,同一家商户重复进件时会对应多条记录。

我建议demo里两个入参都支持,但主查询用apply_no。原因很直接:apply_no能唯一定位到一次进件申请,返回的状态和这份进件单是严格绑定的;out_merchant_no虽然可读性强,但同一商户可能发起过多次进件,机构侧的返回可能是最新一条,也可能是多条列表,语义不够精确。如果你在写商户管理后台,展示给运营看的状态应该以apply_no维度的查询结果为准。

// 按进件单号查询进件结果 public QueryResult queryByApplyNo(String applyNo, Config config) throws Exception { Map<String, String> params = new TreeMap<>(); params.put("app_id", config.getAppId()); params.put("msg_no", UUID.randomUUID().toString().replace("-", "")); params.put("timestamp", String.valueOf(System.currentTimeMillis())); params.put("apply_no", applyNo); String signString = buildQuerySignString(params); params.put("sign", SignUtil.rsaSign(signString, config.getMerchantPrivateKey())); String responseBody = HttpUtil.postJson( "https://open.example.com/merchant/query", new ObjectMapper().writeValueAsString(params), 5000); // 验签后解析出 status、reason、merchant_no QueryResult result = parseQueryResponse(responseBody, config); return result; }

逻辑说明:查询接口的请求签名方式和进件接口保持一致,只是业务参数从data换成了平铺的apply_no。这里msg_no不需要幂等,但依然要保持唯一,因为平台日志排查时会把msg_no当作关联请求的唯一线索。

参数说明:apply_no是字符串类型,传入前建议做trim,避免从数据库读出来带着空格导致平台找不到进件单。响应解析后重点关注两个字段:status是进件状态,reason是审核拒绝或开通失败时的原因文本,后者要直接展示给运营人员看,所以解析时不要丢弃。

4.2 轮询参数设计:间隔、超时与最终态判断

进件状态不会主动告诉你,网络上能用的就是查询接口。常见的做法是发起进件成功后,起一个定时任务去轮询查询接口,直到状态到达终态。轮询参数有三个需要定死:间隔、总超时时间、退出条件。

间隔我一般设5秒,太密会给平台造成压力,而且进件审核通常以分钟计,1秒轮询毫无意义;有些机构的查询接口对api调用量有限制,轮询频率过密会被限流。总超时时间设15分钟,因为人工审核场景下大多数进件都能在这个时间内出结果,如果超过15分钟还在审核中,继续轮询的边际收益很低,应该转人工去查。退出条件就是状态进入4(开通成功)或5(开通失败)。

// 循环轮询直到终态或超时 public QueryResult pollUntilFinal(String applyNo, Config config) throws Exception { long start = System.currentTimeMillis(); long timeout = 15 * 60 * 1000L; // 15分钟超时 int interval = 5000; // 5秒一次 QueryResult result = null; while (System.currentTimeMillis() - start < timeout) { result = queryByApplyNo(applyNo, config); int status = result.getStatus(); // 1. 只有开通成功和开通失败是终态 if (status == 4 || status == 5) { break; } // 2. 审核拒绝也是终态,不需要等开通结果 if (status == 3) { break; } Thread.sleep(interval); } return result; }

逻辑说明:这个循环把审核拒绝(3)也视为终态,因为被拒绝的进件不会再进入后续开通流程。超时退出后,返回的result里status还在中间态,调用方要根据返回值判断是“真的出结果了”还是“超时了”,不能把超时当成开通失败。

参数说明:Thread.sleep的间隔时间可以用从配置读取,测试环境联调时为了快速看到效果可以改成2秒,生产环境建议保持5秒以上。如果你对接的平台有多个环境,建议测试环境轮询间隔短一些、生产环境长一些。

4.3 状态别名陷阱:审核通过和开通成功是两件事

写轮询逻辑时要刻意区分“审核通过”和“开通成功”。审核通过只代表商户的资质和资料没有硬伤,后续还有结算账户验证、终端绑定等步骤,任何一个环节出问题都会把状态推向开通失败。如果轮询代码把2当作终态,业务系统就会提前把商户标记成可用,这时如果商户真实状态是“结算账户打款验证中”,后续交易会全部失败,而且很难排查。

我见过一个真实翻车场景:联调环境里进件提交后几秒就返回开通成功,开发就把2当成终态处理了。到了生产环境,进件审核通过后卡了十分钟都没有开通成功,状态停在了2和4之间的“待签约”。原因就是生产环境的进件流程多了线下签约确认环节,联调环境没有这个步骤。所以状态判断必须以你生产环境的实际流程为准,文档上标注的状态流转顺序可能是理想情况。

状态别名里还要注意另一个细节:不同机构对状态值的定义可能相同,但“终态集合”可能不同。有的机构把“审核拒绝”定义成3,有的定义成5,开通失败和审核拒绝混在一起。demo里我会把所有状态值定义成一个枚举或常量类,集中管理,避免在轮询逻辑里散落魔法数字。这样平台升级或换机构时,改动只集中在一个文件里。

查询接口的轮询结束不代表整个进件流程结束。开通成功后,商户号merchant_no才真正下发,这个字段是后续交易接口、结算接口的必传参数。轮询结果里如果返回了merchant_no,要把它和apply_no一起保存到商户表里,作为进件链路和交易链路的桥接字段。

5. 进件API联调避坑清单:5个反复翻车的问题与解决

5.1 反复报验签失败:密钥、拼接顺序与换行符

现象:进件接口的返回一直提示“验签失败”,你确认参数没填错,重新生成了密钥还是不行。

原因:最常见的是三个。第一个是商户私钥和平台公钥放反了,请求签名用了平台私钥,验签自然对不上。第二个是拼接待签名串时key没有按字典序排序,用了HashMap或JSON原有顺序,导致两端拼接结果不一致。第三个是证书文件从配置里读出来后带了换行符,Base64解码时报错,但日志里看不太出来。

解决:第一,写一段单测,把Demo里示例报文重新跑一遍签名和验签,自测通过再放到接口联调,不要用线上报文来调试签名。第二,拼串前强制用TreeMap排序,并且把data的JSON序列化后的字符串打印出来,肉眼确认两端的data串完全一致。第三,加载私钥和公钥时对字符串做统一清洗,去掉\r、\n和空格,再用Base64解码。

5.2 进件单号已返回但回调永远不来

现象:进件接口提交成功,apply_no正常返回,查询接口也能查到审核通过,但平台配置的进件结果回调地址一直没有收到通知。

原因:回调地址在进件请求里没有提交,或者提交了但平台服务器访问不到。部分机构的回调地址需要在“进件请求里显式传callback_url”,部分机构则要求提前在商户控制台配置,漏了任何一边都不会触发回调。另一方面,回调地址如果返回了非200状态码,平台会判定回调失败并进入退避重试,次数耗尽后不再发起。

解决:第一,自查进件请求体里有没有传callback_url,以及该字段名是否和文档一致。第二,在演示环境用内网机器起一个临时的HTTP服务,把回调接收逻辑预先写好,收到报文先记录完整原文再返回“SUCCESS”,不要返回业务报错。第三,确认你的回调地址能被平台的服务器访问,检查防火墙和网络白名单后再提工单让平台重新触发一次。

5.3 身份证号带X:加密结果对不上的玄学

现象:法人身份证号末位是X,进件提交后平台侧解析出来的号码和提交的不一致,反复核对明文也没发现差异。

原因:身份证号里的X有大写和小写之分。业务系统里录入的可能是小写x,但平台侧校验时按大写X比对,加密前没有做归一化。另外,如果把身份证号从数据库读出来带着前导空格,加密后的密文也不同,而且这个差异在解密前完全看不出来。

解决:所有敏感字段在加密前统一做一次trim和toUpperCase处理。注意不要对整个字段统一toUpperCase,只有证件类型是身份证时才需要,银行卡号的大写没有意义,反而会破坏数字。建议在进件参数组装方法里对legal_person_cert_no单独处理,不要放在通用加密方法里一刀切。

5.4 测试环境通过、生产环境被拒

现象:进件demo在测试环境跑通了,切到生产环境同样的参数发起进件,返回“经营品类编码不存在”或“费率档位不合法”。

原因:经营品类编码和结算费率档位是机构侧的业务配置,不是通用标准。测试环境通常开放全量编码,但生产环境会根据你的签约范围只开通一部分品类,费率档位也可能因商户区域不同受限。这类问题接口文档不会标注,只能在联调时曝出来。

解决:进生产前先向机构客户经理要一份“生产环境可用品类编码表”,和测试环境用的编码表做一次diff。demo里不要把business_code写死,做成从配置或数据库读取。另外注意结算费率传的是小数还是万分比,有的机构用0.006表示万六,有的用6,这个差异在测试环境可能被平台的容错逻辑掩盖,生产环境校验严格就暴露了。

5.5 同一家商户重复进件产生多个商户号

现象:同一家商户在系统里被提交了两次进件请求,平台生成了两个apply_no,最后开通出了两个不同的merchant_no,交易时不知道该用哪个。

原因:进件前没有做幂等控制。进件接口只认msg_no,你换了msg_no再提交,平台会当作一次新进件处理。如果业务侧“新增商户”按钮被双击,或者管理后台重试机制把同一家商户重放了一次,就会生成多笔进件单。

解决:第一,本地维护一张进件申请记录表,以out_merchant_no为唯一键,进件前先查这张表,存在且未到终态就直接返回已有apply_no,不要重复提交。第二,确认平台侧有没有“同营业执照号重复进件”的服务端校验,没有的话风险更高。第三,在demo里加一个标记:只有查询接口返回终态且处理完业务回写后,才允许同一out_merchant_no发起新的进件。

6. 进阶调试:把进件demo的日志与验签改造成排错工具

进件接口联调到后期,真正耗时间的往往不是业务逻辑,而是定位“平台说收到了但我这边没看到数据”这类互扯的问题。我习惯在demo里加两段代码,把它从“调用示例”改造成“排错工具”。

第一段是所有请求和响应的原文日志。日志里同时打出拼接前的待签名串、签名后的sign值、加密后的密文、最终发送的完整请求体。这样平台侧说报文解析失败时,可以把日志原文贴给机构的接口支持,他们能直接看到是哪一段数据异常。

public static void logRequest(String action, String signString, String sign, String requestBody) { System.out.println("=== " + action + " ==="); System.out.println("signString: " + signString); System.out.println("sign: " + sign); System.out.println("requestBody: " + requestBody); }

参数说明:这个日志方法在任何环境都要能开关,生产环境可以关闭signString输出,因为日志里出现完整的敏感字段密文有泄露风险。我建议把密文打出来但明文字段不打,排错时密文长度和是否变化已经能说明大部分问题。

第二段是响应验签工具。把平台返回的响应原文和sign字段单独保存成样例文件,每次修改代码后重新跑一遍验签,确保解析逻辑没有回归。平台接口升级或者换新环境时,也先用这个工具验一次,确认平台公钥没有换。

public static boolean verifyResponse(String responseData, String sign, String platformPublicKeyStr) throws Exception { byte[] keyBytes = Base64.getDecoder().decode(platformPublicKeyStr); X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes); KeyFactory kf = KeyFactory.getInstance("RSA"); PublicKey publicKey = kf.generatePublic(spec); Signature sig = Signature.getInstance("SHA256withRSA"); sig.initVerify(publicKey); sig.update(responseData.getBytes(StandardCharsets.UTF_8)); return sig.verify(Base64.getDecoder().decode(sign)); }

我踩过一次很深的坑,把生产私钥当成测试私钥配进了demo,当天所有进件请求全部验签失败,排查了两个小时才发现是key配置错了。从那以后我在demo里加了一个校验:加载私钥时打印公钥指纹,每次切换环境都先对比指纹,确认没有放错密钥。这套习惯帮我后来对接其他接口省了很多时间。进件、查询这条链路做顺了,后面接交易、结算都不慌,希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询