- 小程序备案时,显示【主体证件住所】“工商数据对比不通过” 如何处理?
在平台备案时,填写【主办单位信息-证件住所】提示错误,显示为:【主体证件住所】工商数据对比不通过,如带有标点符号或字母,请检查中、英文标点符号和字母大小写。 [图片] 显示【工商数据对比不通过】一般是什么原因? 平台会将所填写的数据与企业工商数据进行对比,若不一致则会提示该错误,开发者可按以下进行排查: 首先确认企业是否为新建企业或近期有做信息变更,如有,可能会存在企业工商数据更新延迟的情况,建议过段时间(5~15个工作日)再进行重试,否则无法正常发起验证流程。若企业最近无更新或更新已超过1个月,建议重新核查信息,确保所填写的信息和营业执照上的信息一致。[图片]若核实所填写信息均无误,可登录国家企业信用信息公示系统(请勿登录非官方系统查询,如企查查等),输入对应企业名称、统一社会信用代码或注册号,可查询到目前该企业的工商数据。[图片] 请自行核实营业执照上的【住所】信息,是否与国家企业信用信息公示系统的【经营场所】信息相一致,若不一致(包括地址不一致或空白情况),请联系营业执照发证机关更新公示系统信息。 注:若营业执照与国家企业信用信息公示系统不一致,请务必进行更新,以免后续审核被驳回。 [图片] 若营业执照上的【住所】信息,与国家企业信用信息公示系统的【经营场所】信息一致,且按照以上方式排查仍然无法解决问题,请通过微信开放社区发帖咨询,并说明已通过以上方式排查,仍无法提交备案,会有相应的社区运营专员为您进行后续核实。
04-22 - 手把手教你备案微信小程序(非个人主体备案)
备案材料准备 在提交备案前,请务必提前准备好备案所需材料,以免由于材料更新问题,导致备案需延期提交。下面将会带大家详细了解备案材料的要求,这样后续在提交时就能避免因为材料问题而导致失败。 材料示例及注意事项 [图片] 注:所有上传材料大小应不超过2M,分辨率不低于720* 1280 ,仅支持JPG、JPEG、PNG 格式 若想查看更多小程序备案材料示例,详情可查看文档 备案信息填写 备案材料准备好后,就可以前往【小程序管理后台-设置-小程序备案】提交备案申请了。下面将会详细教大家如何进行备案信息的填写,一共分为五个部分:主办单位信息填写、主体负责人信息填写、小程序信息填写、小程序管理员信息填写和上传其他信息材料。 1.主办单位信息填写 [图片] [图片] 填写说明 常见报错/问题 解决方案 ①选择地区:选择与证件地址相一致的省市区信息 该主体已在XX完成备案,请修改备案省份或注销备案主体重新备案 请核实该主体是否有在其他省份备案过,由于同主体在所有平台的备案省份必须保持一致,需修改备案省份或注销备案主体重新备案 ②主办者性质:默认与小程序主体认证信息相一致 / / ③证件类型:默认与小程序主体认证信息相一致 / / ④上传证件:按要求提供最新版证件 营业执照有效期不足 请联系工商部门更新证件有效期 ⑤企业名称:填写证件相对应名称信息 主办者与小程序主体不一致 请核实填写企业名称是否与小程序主体名称、上传营业执照名称相一致 ⑤企业名称:填写证件相对应名称信息 营业执照名称为空或者* 号 请联系工商部门更新企业名称信息 ⑥证件住所:填写证件相对应经营场所信息 【主体证件住所】工商数据对比不通过 请参考文档进行排查 ⑦证件号码:填写证件相对应统一社会信用代码信息 未查询到企业信息,请检查主体证件号是否有误 请核实填写的是否为统一社会信用代码,若无,请联系工商部门更新证件信息,不能填写其他如工商注册号等 ⑧通讯地址:填写当前主体所在的实际通讯地址(无需填写省、市、区) 通讯地址未能精确到门牌号 若无具体门牌号,需要在备注中说明情况 ⑨备注(选填):针对主体信息进行补充说明,如有可填写 / / 注:若为新建企业或近期有做信息变更,可能会存在企业工商数据更新延迟的情况,建议过段时间(5~15个工作日)再进行重试,否则无法正常发起验证流程。 2.主体负责人信息填写 [图片] 填写说明 常见报错/问题 解决方案 ①证件类型:选择主体负责人证件类型信息 / / ②上传证件:按要求提供最新版证件 / / ③负责人名称:通过上传证件自动识别,有误可自行修改 主体负责人与法定代表人不一致,且备案所在地不支持法定代表人授权 请核实填写的主体负责人是否为法人,需与营业执照信息一致,由于所属地区不支持授权,只能填写法人信息 ④负责人证件号:通过上传证件自动识别,有误可自行修改 【主体负责人证件号码】企业工商四要素核验失败 请核实填写的主体负责人名称、证件号信息是否正确 ⑤证件有效期:通过上传证件自动识别,有误可自行修改 / / ⑥手机号:主体负责人手机号码 【主体负责人手机号码】不允许被多人使用 请核实填写的手机号是否为其他人的信息,仅在同一主体下,同一个人允许为多个小程序备案,可提交一致的手机号、应急手机号及邮箱信息,否则不能出现个人信息混用的情况 ⑦验证码:主体负责人手机号码收到的对应验证码 验证码不正确 请核实验证码是否已失效,验证码有效期为10分钟 ⑧应急手机号:主体负责人的应急电话 【主体负责人应急联系方式】不允许被多人使用 请核实填写的应急手机号是否为其他人的信息,仅在同一主体下,同一个人允许为多个小程序备案,可提交一致的手机号、应急手机号及邮箱信息,否则不能出现个人信息混用的情况 ⑨邮箱地址:主体负责人的电子邮箱 / / 3.小程序信息填写 [图片] 填写说明 常见报错/问题 解决方案 ①服务内容标识:根据小程序实际运营内容选择合适的即可 小程序服务内容类型数目不能超过5个 服务内容标识是通信管局对各个行业的分类,平台部分行业类目与管局行业类目名称不完全不一致,建议根据备案小程序实际运营内容尽可能选择对应的服务内容标识,最多选择5个。 ②互联网信息服务前置审批项:根据小程序实际运营内容判断是否需要进行前置审批 如从事XXX业务,请上传前置审批文件 小程序实际运营内容涉及前置审批项,需上传对应的审批文件 ②互联网信息服务前置审批项:根据小程序实际运营内容判断是否需要进行前置审批 前置审批项必须选择“以上都不涉及” 小程序实际运营内容不涉及前置审批项,需要选择"以上都不涉及” ③备注(必填):具体描述小程序实际经营内容,主要服务内容 请在小程序备注按格式填写 请核实是否有根据备注格式进行填写,仅自行补充带星号内容即可。 4.小程序管理员信息填写 [图片] 填写说明 常见报错/问题 解决方案 ①证件类型:选择小程序负责人证件类型信息(目前仅支持身份证) / / ②上传证件:按要求提供最新版证件(目前仅支持身份证) / / ③负责人名称:通过上传证件自动识别,有误可自行修改 【小程序负责人姓名】负责人与小程序管理员不一致 请核实小程序是否未完善管理员实名信息,需参考指引文档进行补充 ④负责人证件号:通过上传证件自动识别,有误可自行修改 【小程序负责人证件号码】负责人与小程序管理员不一致 请核实小程序是否未完善管理员实名信息,需参考指引文档进行补充 ⑤证件有效期:通过上传证件自动识别,有误可自行修改 / / ⑥手机号:小程序负责人手机号码 【小程序负责人手机号码】不允许被多人使用 请核实填写的手机号是否为其他人的信息,仅在同一主体下,同一个人允许为多个小程序备案,可提交一致的手机号、应急手机号及邮箱信息,否则不能出现个人信息混用的情况 ⑦验证码:小程序负责人手机号码收到的对应验证码 验证码不正确 请核实验证码是否已失效,验证码有效期为10分钟 ⑧应急手机号:小程序负责人的应急电话 【小程序负责人应急联系方式】不允许被多人使用 请核实填写的应急手机号是否为其他人的信息,仅在同一主体下,同一个人允许为多个小程序备案,可提交一致的手机号、应急手机号及邮箱信息,否则不能出现个人信息混用的情况 ⑨邮箱地址:小程序负责人的电子邮箱 / / ⑩负责人人脸核身:小程序管理员(小程序负责人)需使用微信APP扫码,完成人脸核身 当前场景仅支持居民身份证 核实小程序管理员证件是否为大陆居民身份证,目前港澳台管理员无法进行人脸核身,建议先更换小程序管理员为中国大陆地区的人员,作为备案小程序负责人。 5.上传其他信息材料 [图片] 填写说明 常见报错/问题 解决方案 互联网信息服务承诺书:1. 广东地区:下载页面提供的模版文件,填写完整后上传提交;2. 非广东地区:点击阅读确认后提交 承诺书需加盖公章,但个体户没有公章 若个体工商户无公章,需要主体负责人手写日期+签名+盖手印+身份证号码,同时请在主体备注处备注“个体工商户无公章”。注:江苏、宁夏、福建地区,须刻章后提交备案,不接受负责人手印。 以上信息都填写完毕,就可点击提交,后续的备案审核流程可参考: [图片] 如有其他相关疑问,欢迎随时参与社区讨论。
05-28 - 暂未对存量小程序开放,还需要备案吗
https://developers.weixin.qq.com/miniprogram/product/record_guidelines.html
2023-10-13 - 微信小程序一定需要做备案吗?
微信小程序一定要做备案吗?如不做的话有什么影响呢?
2023-12-05 - 微信会把事件消息推送给所有有相关授权的第三方平台吗?
服务号将模板消息的权限授权给多个第三方平台,其中一个第三方平台发送模板消息之后,微信会不会将模板消息发送结果推送给所有有授权的第三方平台? 目前测试结果是会,有没有官方的人来解答一下。
2021-09-26 - Template Send Job Finish,开发者5秒内没有返回?
时间: 2021-09-02 11:05:48 内容: 微信服务器向公众号推送消息或事件后,开发者5秒内没有返回 次数: 5分钟 49次 Event=Template Send Job Finish 当我收到模板消息发送完成的事件推送时,由于我服务器的问题,偶尔不能及时对此推送做出正确返回, 请问会有什么不好的影响吗? 比如会不会影响我后续发送模板消息?
2021-09-03 - 公众号模板消息有没有办法跳转到体验版或开发版小程序?
如题
2020-03-22 - 公众平台模版消息没有全部显示
[图片]这里的告警内容只显示了部分,备注没有了
2023-05-05 - 公众号开启服务器配置,后台的自定义菜单不能同步开启吗?
没办法服务器配置和自定义菜单同时开启吗 [图片][图片]
2023-11-29 - 微信公众号、微信小程序、微信开放平台,三者之间的认证资质复用关系到底是怎么样的?
之前是微信公众号资质可以复用给小程序。但现在好像又不行了,有没有官方相关的说明呀。
2023-12-21 - 申请微信开放平台费用?
申请微信开放平台时验证方式有两种:支付验证和微信认证,公司公户打款验证要走流程时间比较长,如果使用微信认证支付300元,那么再开发者资质认证时是否还需要300块钱认证费用?
2023-09-08 - 小程序备案常见问题
1、小程序备案收费吗? 小程序备案是免费的,不收取任何费用。PS:自主备案免费,如果你找第三方,可能会收取一定费用,都是辛苦钱,要太多的就不要理会了。这里有备案流程:https://developers.weixin.qq.com/community/develop/article/doc/0008403911c1107ec14043aef66813 官方给出的备案指引:https://developers.weixin.qq.com/miniprogram/product/record_guidelines.html 2、什么样的小程序需要备案? 平台中所有小程序都需要备案。PS:非个人、个人都需要备案,且同一个主体的小程序,备案需要一个个排队备案,前一个备案好,才能备案下一个,备案负责人,同一个主体可以是一个人;不同主体的不能是同一个人。 3、主体负责人身份有什么要求? 个人类型主体:注册人本人; 非个人类型主体:通常由单位法定代表人担任,如有特殊情况(如法代身份涉密、长期不在国内等)可授权单位高管担任。PS:官方给出的有授权书,按照要求填写,加盖公章。 4、小程序负责人身份有什么要求? 个人类型主体:注册人本人; 非个人类型主体:小程序负责人应为本单位/公司具体负责小程序管理、小程序维护的相关人员。PS:可以不用是法人,同一个主体下的小程序备案可以是同一个人,不同主体的不能是同一个负责人。 5、上传的图片资料有什么要求? 上传证件彩色原件即可,大小不超过200K,分辨率不低于1500*1100,必须为彩色。若图片添加水印,请确保水印内容与实际用途一致。PS:证件上注明:小程序备案所用,他用无效。 6、同一个主体域名已经备案,小程序是否还需要备案?同一主体名下多个小程序是不是可以统一备案? 每个小程序都需要单独申请备案。PS:所有小程序都要进行单独备案。 7、作为服务商,帮客户制作小程序的,有没有快速备案的接口? 服务商可以采用接口调用方式帮助商户进行备案资料收集及上传,相关接口文档请查看:https://developers.weixin.qq.com/doc/oplatform/openApi/OpenApiDoc/miniprogram-management/record/queryIcpVerifyTask.html PS:服务商这个有开发能力的就上吧。赚点辛苦钱,也是不容易。 8、小程序备案后每次更新迭代都需要备案吗? 小程序完成备案之后可以正常迭代线上版本,无需每次都进行备案。PS:备案一次就行了,除非主体发生变更。 9、已经上架的小程序在未备案前是否可正常版本迭代? 2024年3月31日前可以正常发版。若微信小程序已上架,请于2024年3月31日前完成备案,逾期未完成备案,平台将按照备案相关规定于2024年4月1日起进行清退。PS:听话,咱要好好的备案。 更详细问答,参考官方文档:https://developers.weixin.qq.com/miniprogram/product/record/record_faq.html
2023-11-08 - 我们的小程序和公众号互相绑定了,但是为啥获取不到unionid?
小程序绑定了公众号,调用「[代码]wx.login[代码]」和「[代码]code2Session[代码]」接口只能获取到session_key和openId 这是为什么
2023-10-30 - 小程序与公众号下发统一消息接口作废后,那我只有小程序openid如何推送公众号?
[图片] 你好 我之前都是用小程序的openid来调用小程序和公众号统一发送消息接口,现在接口作废了;我如何通过小程序的openid 来让公众号发送消息 ;我如何获得公众号openid?
2023-09-19 - 微信退款no required SSL certificate was sent
支付是成功的,退款报这个错误,路径是觉得路径没有问题,证书是新下载的,也没有问题,请官方给个解释?感谢[图片]
2020-09-10 - 微信支付退款接口提示异常
[Fatal Error] :7:3: 元素类型 "hr" 必须由匹配的结束[图片]标记 "</hr>" 终止。
2020-11-16 - 微信支付V3版java整合
1.引入依赖 [代码]<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.5.RELEASE</version> <relativePath/> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>com.squareup.okhttp3</groupId> <artifactId>okhttp</artifactId> <version>4.6.0</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <scope>provided</scope> </dependency> <dependency> <groupId>net.sf.json-lib</groupId> <artifactId>json-lib</artifactId> <version>2.4</version> <classifier>jdk15</classifier> </dependency> <!-- huTool 工具包 --> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-crypto</artifactId> <version>5.5.0</version> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-http</artifactId> <version>5.5.0</version> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-json</artifactId> <version>5.5.0</version> </dependency> <dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> <version>4.5.12</version> <scope>compile</scope> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>servlet-api</artifactId> <version>2.5</version> <scope>provided</scope> </dependency> <dependency> <groupId>com.github.xkzhangsan</groupId> <artifactId>xk-time</artifactId> <version>2.1.2</version> </dependency> </dependencies> [代码] 2.创建时间工具类 DateTimeZoneUtil.class [代码]import cn.hutool.core.util.StrUtil; import com.xkzhangsan.time.converter.DateTimeConverterUtil; import com.xkzhangsan.time.formatter.DateTimeFormatterUtil; import java.io.Serializable; import java.time.ZonedDateTime; import java.util.Date; /** * 〈一句话功能简述〉<br> * 〈时间转换工具类〉 * * @author Gym * @create 2020/12/18 * @since 1.0.0 */ public class DateTimeZoneUtil implements Serializable { /** * 时间转 TimeZone * * @param time * @return * @throws Exception */ public static String dateToTimeZone(long time) throws Exception { return dateToTimeZone(new Date(time)); } /** * 时间转 TimeZone * * @param date * @return * @throws Exception */ public static String dateToTimeZone(Date date) throws Exception { String time; if (date == null) { throw new Exception("date is not null"); } ZonedDateTime zonedDateTime = DateTimeConverterUtil.toZonedDateTime(date); time = DateTimeFormatterUtil.format(zonedDateTime, DateTimeFormatterUtil.YYYY_MM_DD_T_HH_MM_SS_XXX_FMT); return time; } /** * TimeZone 时间转标准时间 * * @param str * @return * @throws Exception */ public static String timeZoneDateToStr(String str) throws Exception { String time; if (StrUtil.isBlank(str)) { throw new Exception("str is not null"); } ZonedDateTime zonedDateTime = DateTimeFormatterUtil.parseToZonedDateTime(str, DateTimeFormatterUtil.YYYY_MM_DD_T_HH_MM_SS_XXX_FMT); if (zonedDateTime == null) { throw new Exception("str to zonedDateTime fail"); } time = zonedDateTime.format(DateTimeFormatterUtil.YYYY_MM_DD_HH_MM_SS_FMT); return time; } } [代码] 3.解密工具类 AesUtil.class [代码] import cn.hutool.core.codec.Base64; import javax.crypto.Cipher; import javax.crypto.NoSuchPaddingException; import javax.crypto.spec.GCMParameterSpec; import javax.crypto.spec.SecretKeySpec; import java.nio.charset.StandardCharsets; import java.security.GeneralSecurityException; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; /** * 〈一句话功能简述〉<br> * 〈〉 * * @author Gym * @create 2020/12/18 * @since 1.0.0 */ public class AesUtil { static final int KEY_LENGTH_BYTE = 32; static final int TAG_LENGTH_BIT = 128; private final byte[] aesKey; /** * @param key APIv3 密钥 */ public AesUtil(byte[] key) { if (key.length != KEY_LENGTH_BYTE) { throw new IllegalArgumentException("无效的ApiV3Key,长度必须为32个字节"); } this.aesKey = key; } /** * 证书和回调报文解密 * * @param associatedData associated_data * @param nonce nonce * @param cipherText ciphertext * @return {String} 平台证书明文 * @throws GeneralSecurityException 异常 */ public String decryptToString(byte[] associatedData, byte[] nonce, String cipherText) throws GeneralSecurityException { try { Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); SecretKeySpec key = new SecretKeySpec(aesKey, "AES"); GCMParameterSpec spec = new GCMParameterSpec(TAG_LENGTH_BIT, nonce); cipher.init(Cipher.DECRYPT_MODE, key, spec); cipher.updateAAD(associatedData); return new String(cipher.doFinal(Base64.decode(cipherText)), StandardCharsets.UTF_8); } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { throw new IllegalStateException(e); } catch (InvalidKeyException | InvalidAlgorithmParameterException e) { throw new IllegalArgumentException(e); } } } [代码] 4.对外暴露方法 公共参数 [代码] //请求网关 private static final String url_prex = "https://api.mch.weixin.qq.com/"; //编码 private static final String charset = "UTF-8"; [代码] 4-1.支付下单 V3PayGet() [代码] /** * 微信支付下单 * * @param url 请求地址(只需传入域名之后的路由地址) * @param jsonStr 请求体 json字符串 此参数与微信官方文档一致 * @param mercId 商户ID * @param serial_no 证书序列号 * @param privateKeyFilePath 私钥的路径 * @return 订单支付的参数 * @throws Exception */ public static String V3PayGet(String url, String jsonStr, String mercId, String serial_no, String privateKeyFilePath) throws Exception { String body = ""; //创建httpclient对象 CloseableHttpClient client = HttpClients.createDefault(); //创建post方式请求对象 HttpPost httpPost = new HttpPost(url_prex + url); //装填参数 StringEntity s = new StringEntity(jsonStr, charset); s.setContentEncoding(new BasicHeader(HTTP.CONTENT_TYPE, "application/json")); //设置参数到请求对象中 httpPost.setEntity(s); String post = getToken("POST", HttpUrl.parse(url_prex + url), mercId, serial_no, privateKeyFilePath, jsonStr); //设置header信息 //指定报文头【Content-type】、【User-Agent】 httpPost.setHeader("Content-type", "application/json"); httpPost.setHeader("User-Agent", "Mozilla/4.0 (compatible; MSIE 5.0; Windows NT; DigExt)"); httpPost.setHeader("Accept", "application/json"); httpPost.setHeader("Authorization", "WECHATPAY2-SHA256-RSA2048 " + post); //执行请求操作,并拿到结果(同步阻塞) CloseableHttpResponse response = client.execute(httpPost); //获取结果实体 HttpEntity entity = response.getEntity(); if (entity != null) { //按指定编码转换结果实体为String类型 body = EntityUtils.toString(entity, charset); } EntityUtils.consume(entity); //释放链接 response.close(); switch (url) { case "v3/pay/transactions/app"://返回APP支付所需的参数 return JSONObject.fromObject(body).getString("prepay_id"); case "v3/pay/transactions/jsapi"://返回JSAPI支付所需的参数 return JSONObject.fromObject(body).getString("prepay_id"); case "v3/pay/transactions/native"://返回native的请求地址 return JSONObject.fromObject(body).getString("code_url"); case "v3/pay/transactions/h5"://返回h5支付的链接 return JSONObject.fromObject(body).getString("h5_url"); } return null; } [代码] 4-2.微信调起支付参数 WxTuneUp() [代码] /** * 微信调起支付参数 * 返回参数如有不理解 请访问微信官方文档 * https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter4_1_4.shtml * * @param prepayId 微信下单返回的prepay_id * @param appId 应用ID(appid) * @param privateKeyFilePath 私钥的地址 * @return 当前调起支付所需的参数 * @throws Exception */ public static JSONObject WxTuneUp(String prepayId, String appId, String privateKeyFilePath) throws Exception { String time = System.currentTimeMillis() / 1000 + ""; String nonceStr = UUID.randomUUID().toString().replace("-", ""); String packageStr = "prepay_id=" + prepayId; ArrayList<String> list = new ArrayList<>(); list.add(appId); list.add(time); list.add(nonceStr); list.add(packageStr); //加载签名 String packageSign = sign(buildSignMessage(list).getBytes(), privateKeyFilePath); JSONObject jsonObject = new JSONObject(); jsonObject.put("appid", appId); jsonObject.put("timeStamp", time); jsonObject.put("nonceStr", nonceStr); jsonObject.put("packages", packageStr); jsonObject.put("signType", "RSA"); jsonObject.put("paySign", packageSign); return jsonObject; } [代码] 4-3.处理微信异步回调 notify() [代码] /** * 处理微信异步回调 * * @param request * @param response * @param privateKey 32的秘钥 */ public static String notify(HttpServletRequest request, HttpServletResponse response, String privateKey) throws Exception { Map<String, String> map = new HashMap<>(12); String result = readData(request); // 需要通过证书序列号查找对应的证书,verifyNotify 中有验证证书的序列号 String plainText = verifyNotify(result, privateKey); if (StrUtil.isNotEmpty(plainText)) { response.setStatus(200); map.put("code", "SUCCESS"); map.put("message", "SUCCESS"); } else { response.setStatus(500); map.put("code", "ERROR"); map.put("message", "签名错误"); } response.setHeader("Content-type", ContentType.JSON.toString()); response.getOutputStream().write(JSONUtil.toJsonStr(map).getBytes(StandardCharsets.UTF_8)); response.flushBuffer(); String out_trade_no = JSONObject.fromObject(plainText).getString("out_trade_no"); return out_trade_no; } [代码] 5. 同类类方法 5-1.生成组装请求头 getToken() [代码]/** * 生成组装请求头 * * @param method 请求方式 * @param url 请求地址 * @param mercId 商户ID * @param serial_no 证书序列号 * @param privateKeyFilePath 私钥路径 * @param body 请求体 * @return 组装请求的数据 * @throws Exception */ static String getToken(String method, HttpUrl url, String mercId, String serial_no, String privateKeyFilePath, String body) throws Exception { String nonceStr = UUID.randomUUID().toString().replace("-", ""); long timestamp = System.currentTimeMillis() / 1000; String message = buildMessage(method, url, timestamp, nonceStr, body); String signature = sign(message.getBytes("UTF-8"), privateKeyFilePath); return "mchid=\"" + mercId + "\"," + "nonce_str=\"" + nonceStr + "\"," + "timestamp=\"" + timestamp + "\"," + "serial_no=\"" + serial_no + "\"," + "signature=\"" + signature + "\""; } [代码] 5-2.生成签名 sign() [代码] /** * 生成签名 * * @param message 请求体 * @param privateKeyFilePath 私钥的路径 * @return 生成base64位签名信息 * @throws Exception */ static String sign(byte[] message, String privateKeyFilePath) throws Exception { Signature sign = Signature.getInstance("SHA256withRSA"); sign.initSign(getPrivateKey(privateKeyFilePath)); sign.update(message); return Base64.getEncoder().encodeToString(sign.sign()); } [代码] 5-3.组装签名加载 buildMessage() [代码] /** * 组装签名加载 * * @param method 请求方式 * @param url 请求地址 * @param timestamp 请求时间 * @param nonceStr 请求随机字符串 * @param body 请求体 * @return 组装的字符串 */ static String buildMessage(String method, HttpUrl url, long timestamp, String nonceStr, String body) { String canonicalUrl = url.encodedPath(); if (url.encodedQuery() != null) { canonicalUrl += "?" + url.encodedQuery(); } return method + "\n" + canonicalUrl + "\n" + timestamp + "\n" + nonceStr + "\n" + body + "\n"; } [代码] 5-4.获取私钥 getPrivateKey() [代码] /** * 获取私钥。 * * @param filename 私钥文件路径 (required) * @return 私钥对象 */ static PrivateKey getPrivateKey(String filename) throws IOException { String content = new String(Files.readAllBytes(Paths.get(filename)), "UTF-8"); try { String privateKey = content.replace("-----BEGIN PRIVATE KEY-----", "") .replace("-----END PRIVATE KEY-----", "") .replaceAll("\\s+", ""); KeyFactory kf = KeyFactory.getInstance("RSA"); return kf.generatePrivate( new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKey))); } catch (NoSuchAlgorithmException e) { throw new RuntimeException("当前Java环境不支持RSA", e); } catch (InvalidKeySpecException e) { throw new RuntimeException("无效的密钥格式"); } } [代码] 5-5.构造签名串 buildSignMessage() [代码] /** * 构造签名串 * * @param signMessage 待签名的参数 * @return 构造后带待签名串 */ static String buildSignMessage(ArrayList<String> signMessage) { if (signMessage == null || signMessage.size() <= 0) { return null; } StringBuilder sbf = new StringBuilder(); for (String str : signMessage) { sbf.append(str).append("\n"); } return sbf.toString(); } [代码] 5-6.v3支付异步通知验证签名 verifyNotify() [代码] /** * v3 支付异步通知验证签名 * * @param body 异步通知密文 * @param key api 密钥 * @return 异步通知明文 * @throws Exception 异常信息 */ static String verifyNotify(String body, String key) throws Exception { // 获取平台证书序列号 cn.hutool.json.JSONObject resultObject = JSONUtil.parseObj(body); cn.hutool.json.JSONObject resource = resultObject.getJSONObject("resource"); String cipherText = resource.getStr("ciphertext"); String nonceStr = resource.getStr("nonce"); String associatedData = resource.getStr("associated_data"); AesUtil aesUtil = new AesUtil(key.getBytes(StandardCharsets.UTF_8)); // 密文解密 return aesUtil.decryptToString( associatedData.getBytes(StandardCharsets.UTF_8), nonceStr.getBytes(StandardCharsets.UTF_8), cipherText ); } [代码] 5-7.处理返回对象 readData() [代码] /** * 处理返回对象 * * @param request * @return */ static String readData(HttpServletRequest request) { BufferedReader br = null; try { StringBuilder result = new StringBuilder(); br = request.getReader(); for (String line; (line = br.readLine()) != null; ) { if (result.length() > 0) { result.append("\n"); } result.append(line); } return result.toString(); } catch (IOException e) { throw new RuntimeException(e); } finally { if (br != null) { try { br.close(); } catch (IOException e) { e.printStackTrace(); } } } } [代码] 6.接口调用实例 6-1支付请求 [代码]@GetMapping("/wxPay") public Object wxPay() throws Exception { //支付的请求参数信息(此参数与微信支付文档一致,文档地址:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_1.shtml) WxPayRequestBo wxPayRequestBo = new WxPayRequestBo() .setAppid(appId) .setMchid(mercId) .setDescription("商品描述") .setOut_trade_no(out_trade_no) .setTime_expire(time_expire) .setNotify_url(notify_url) .setAmount(new Amount().setTotal(1)) .setPayer(new Payer().setOpenid(openId)) .setScene_info(new SceneInfo().setPayer_client_ip(client_ip)); String wxPayRequestJsonStr = JSONUtil.toJsonStr(wxPayRequestBo); //第一步获取prepay_id String prepayId = WxPayV3Util.V3PayGet(url, wxPayRequestJsonStr, mercId, serial_no, privateKeyFilePath); //第二步获取调起支付的参数 JSONObject object = JSONObject.fromObject(WxPayV3Util.WxTuneUp(prepayId, appId, privateKeyFilePath)); return object; } [代码] 6-2.异步回调 [代码] @RequestMapping(value = "/wxnoty", method = {org.springframework.web.bind.annotation.RequestMethod.POST, org.springframework.web.bind.annotation.RequestMethod.GET}) public void wxnoty(HttpServletRequest request, HttpServletResponse response) throws Exception { System.err.println("小程序支付异步消息"); String out_trade_no = WxPayV3Util.notify(request, response, privateKey); System.out.println(out_trade_no); } [代码] 文件秘钥截图: [图片]
2021-01-12 - 搞懂微信支付 v3 接口规则-【附Java源码】
简介 为了在保证支付安全的前提下,带给商户简单、一致且易用的开发体验,我们推出了全新的微信支付API v3。 其实还要一个主要因素是「为了符合监管的要求」。 主要是为了符合监管的要求,保证更高的安全级别。《中华人民共和国电子签名法》、《金融电子认证规范》及《非银行支付机构网络支付业务管理办法》中规定 “电子签名需要第三方认证的,由依法设立的电子认证服务提供者提供认证服务。”,所以需使用第三方 CA 来确保数字证书的唯一性、完整性及交易的不可抵赖性。 支付宝支付也是如此,从之前的「普通公钥方式」新增了 「公钥证书方式」。今天的主角是微信支付 Api v3 这里就不展开讲支付宝支付了。 微信支付 Api v3 接口规则 官方文档 v2 与 v3 的区别 V3 规则差异 V2 JSON 参数格式 XML POST、GET 或 DELETE 提交方式 POST AES-256-GCM加密 回调加密 无需加密 RSA 加密 敏感加密 无需加密 UTF-8 编码方式 UTF-8 非对称密钥SHA256-RSA 签名方式 MD5 或 HMAC-SHA256 微信支付 Api-v2 版本详细介绍请参数之前博客 微信支付,你想知道的一切都在这里 干货多,屁话少,下面直接进入主题,读完全文你将 Get 到以下知识点 如何获取证书序列号 非对称密钥 SHA256-RSA 加密与验证签名 AES-256-GCM 如何解密 API 密钥设置 请登录商户平台进入【账户中心】->【账户设置】->【API安全】->【APIv3密钥】中设置 API 密钥。 具体操作步骤请参见:什么是APIv3密钥?如何设置? 获取 API 证书 请登录商户平台进入【账户中心】->【账户设置】->【API安全】根据提示指引下载证书。 具体操作步骤请参见:什么是API证书?如何获取API证书? 按照以上步骤操作后你将获取如下内容: apiKey API 密钥 apiKey3 APIv3 密钥 mchId 商户号 apiclient_key.pem X.509 标准证书的密钥 apiclient_cert.p12 X.509 标准的证书+密钥 apiclient_cert.pem X.509 标准的证书 请求签名 如何生成签名参数?官方文档 描述得非常清楚这里就不啰嗦了。 示例代码 构造签名串 [代码] /** * 构造签名串 * * @param method {@link RequestMethod} GET,POST,PUT等 * @param url 请求接口 /v3/certificates * @param timestamp 获取发起请求时的系统当前时间戳 * @param nonceStr 随机字符串 * @param body 请求报文主体 * @return 待签名字符串 */ public static String buildSignMessage(RequestMethod method, String url, long timestamp, String nonceStr, String body) { return new StringBuilder() .append(method.toString()) .append("\n") .append(url) .append("\n") .append(timestamp) .append("\n") .append(nonceStr) .append("\n") .append(body) .append("\n") .toString(); } [代码] 构造 HTTP 头中的 Authorization [代码]/** * 构建 v3 接口所需的 Authorization * * @param method {@link RequestMethod} 请求方法 * @param urlSuffix 可通过 WxApiType 来获取,URL挂载参数需要自行拼接 * @param mchId 商户Id * @param serialNo 商户 API 证书序列号 * @param keyPath key.pem 证书路径 * @param body 接口请求参数 * @param nonceStr 随机字符库 * @param timestamp 时间戳 * @param authType 认证类型 * @return {@link String} 返回 v3 所需的 Authorization * @throws Exception 异常信息 */ public static String buildAuthorization(RequestMethod method, String urlSuffix, String mchId, String serialNo, String keyPath, String body, String nonceStr, long timestamp, String authType) throws Exception { // 构建签名参数 String buildSignMessage = PayKit.buildSignMessage(method, urlSuffix, timestamp, nonceStr, body); // 获取商户私钥 String key = PayKit.getPrivateKey(keyPath); // 生成签名 String signature = RsaKit.encryptByPrivateKey(buildSignMessage, key); // 根据平台规则生成请求头 authorization return PayKit.getAuthorization(mchId, serialNo, nonceStr, String.valueOf(timestamp), signature, authType); } /** * 获取授权认证信息 * * @param mchId 商户号 * @param serialNo 商户API证书序列号 * @param nonceStr 请求随机串 * @param timestamp 时间戳 * @param signature 签名值 * @param authType 认证类型,目前为WECHATPAY2-SHA256-RSA2048 * @return 请求头 Authorization */ public static String getAuthorization(String mchId, String serialNo, String nonceStr, String timestamp, String signature, String authType) { Map<String, String> params = new HashMap<>(5); params.put("mchid", mchId); params.put("serial_no", serialNo); params.put("nonce_str", nonceStr); params.put("timestamp", timestamp); params.put("signature", signature); return authType.concat(" ").concat(createLinkString(params, ",", false, true)); } [代码] 拼接参数 [代码] public static String createLinkString(Map<String, String> params, String connStr, boolean encode, boolean quotes) { List<String> keys = new ArrayList<String>(params.keySet()); Collections.sort(keys); StringBuilder content = new StringBuilder(); for (int i = 0; i < keys.size(); i++) { String key = keys.get(i); String value = params.get(key); // 拼接时,不包括最后一个&字符 if (i == keys.size() - 1) { if (quotes) { content.append(key).append("=").append('"').append(encode ? urlEncode(value) : value).append('"'); } else { content.append(key).append("=").append(encode ? urlEncode(value) : value); } } else { if (quotes) { content.append(key).append("=").append('"').append(encode ? urlEncode(value) : value).append('"').append(connStr); } else { content.append(key).append("=").append(encode ? urlEncode(value) : value).append(connStr); } } } return content.toString(); } [代码] 从上面示例来看我们还差两个参数 serial_no 证书序列号 signature 使用商户私钥对待签名串进行 SHA256 with RSA 签名 如何获取呢?不要着急,容我喝杯 「89年的咖啡」提提神。 获取证书序列号 通过工具获取 openssl x509 -in apiclient_cert.pem -noout -serial 使用证书解析工具 https://myssl.com/cert_decode.html 通过代码获取 [代码]// 获取证书序列号 X509Certificate certificate = PayKit.getCertificate(FileUtil.getInputStream("apiclient_cert.pem 证书路径")); System.out.println("输出证书信息:\n" + certificate.toString()); System.out.println("证书序列号:" + certificate.getSerialNumber().toString(16)); System.out.println("版本号:" + certificate.getVersion()); System.out.println("签发者:" + certificate.getIssuerDN()); System.out.println("有效起始日期:" + certificate.getNotBefore()); System.out.println("有效终止日期:" + certificate.getNotAfter()); System.out.println("主体名:" + certificate.getSubjectDN()); System.out.println("签名算法:" + certificate.getSigAlgName()); System.out.println("签名:" + certificate.getSignature().toString()); /** * 获取证书 * * @param inputStream 证书文件 * @return {@link X509Certificate} 获取证书 */ public static X509Certificate getCertificate(InputStream inputStream) { try { CertificateFactory cf = CertificateFactory.getInstance("X509"); X509Certificate cert = (X509Certificate) cf.generateCertificate(inputStream); cert.checkValidity(); return cert; } catch (CertificateExpiredException e) { throw new RuntimeException("证书已过期", e); } catch (CertificateNotYetValidException e) { throw new RuntimeException("证书尚未生效", e); } catch (CertificateException e) { throw new RuntimeException("无效的证书", e); } } [代码] SHA256 with RSA 签名 获取商户私钥 [代码] /** * 获取商户私钥 * * @param keyPath 商户私钥证书路径 * @return 商户私钥 * @throws Exception 解析 key 异常 */ public static String getPrivateKey(String keyPath) throws Exception { String originalKey = FileUtil.readUtf8String(keyPath); String privateKey = originalKey .replace("-----BEGIN PRIVATE KEY-----", "") .replace("-----END PRIVATE KEY-----", "") .replaceAll("\\s+", ""); return RsaKit.getPrivateKeyStr(RsaKit.loadPrivateKey(privateKey)); } public static String getPrivateKeyStr(PrivateKey privateKey) { return Base64.encode(privateKey.getEncoded()); } /** * 从字符串中加载私钥 * * @param privateKeyStr 私钥 * @return {@link PrivateKey} * @throws Exception 异常信息 */ public static PrivateKey loadPrivateKey(String privateKeyStr) throws Exception { try { byte[] buffer = Base64.decode(privateKeyStr); PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(buffer); KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM); return keyFactory.generatePrivate(keySpec); } catch (NoSuchAlgorithmException e) { throw new Exception("无此算法"); } catch (InvalidKeySpecException e) { throw new Exception("私钥非法"); } catch (NullPointerException e) { throw new Exception("私钥数据为空"); } } [代码] 私钥签名 [代码]/** * 私钥签名 * * @param data 需要加密的数据 * @param privateKey 私钥 * @return 加密后的数据 * @throws Exception 异常信息 */ public static String encryptByPrivateKey(String data, String privateKey) throws Exception { PKCS8EncodedKeySpec priPkcs8 = new PKCS8EncodedKeySpec(Base64.decode(privateKey)); KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM); PrivateKey priKey = keyFactory.generatePrivate(priPkcs8); java.security.Signature signature = java.security.Signature.getInstance("SHA256WithRSA"); signature.initSign(priKey); signature.update(data.getBytes(StandardCharsets.UTF_8)); byte[] signed = signature.sign(); return StrUtil.str(Base64.encode(signed)); } [代码] 至此微信支付 Api-v3 接口请求参数已封装完成。 执行请求 [代码]/** * V3 接口统一执行入口 * * @param method {@link RequestMethod} 请求方法 * @param urlPrefix 可通过 {@link WxDomain}来获取 * @param urlSuffix 可通过 {@link WxApiType} 来获取,URL挂载参数需要自行拼接 * @param mchId 商户Id * @param serialNo 商户 API 证书序列号 * @param keyPath apiclient_key.pem 证书路径 * @param body 接口请求参数 * @param nonceStr 随机字符库 * @param timestamp 时间戳 * @param authType 认证类型 * @param file 文件 * @return {@link String} 请求返回的结果 * @throws Exception 接口执行异常 */ public static Map<String, Object> v3Execution(RequestMethod method, String urlPrefix, String urlSuffix, String mchId, String serialNo, String keyPath, String body, String nonceStr, long timestamp, String authType, File file) throws Exception { // 构建 Authorization String authorization = WxPayKit.buildAuthorization(method, urlSuffix, mchId, serialNo, keyPath, body, nonceStr, timestamp, authType); if (method == RequestMethod.GET) { return doGet(urlPrefix.concat(urlSuffix), authorization, serialNo, null); } else if (method == RequestMethod.POST) { return doPost(urlPrefix.concat(urlSuffix), authorization, serialNo, body); } else if (method == RequestMethod.DELETE) { return doDelete(urlPrefix.concat(urlSuffix), authorization, serialNo, body); } else if (method == RequestMethod.UPLOAD) { return doUpload(urlPrefix.concat(urlSuffix), authorization, serialNo, body, file); } return null; } [代码] 网络请求库默认是使用的 Hutool 封装的一套 Java 工具集合来实现 GET 请求 [代码]/** * @param url 请求url * @param authorization 授权信息 * @param serialNumber 公钥证书序列号 * @param jsonData 请求参数 * @return {@link HttpResponse} 请求返回的结果 */ private HttpResponse doGet(String url, String authorization, String serialNumber, String jsonData) { return HttpRequest.post(url) .addHeaders(getHeaders(authorization, serialNumber)) .body(jsonData) .execute(); } [代码] POST 请求 [代码] /** * @param url 请求url * @param authorization 授权信息 * @param serialNumber 公钥证书序列号 * @param jsonData 请求参数 * @return {@link HttpResponse} 请求返回的结果 */ private HttpResponse doPost(String url, String authorization, String serialNumber, String jsonData) { return HttpRequest.post(url) .addHeaders(getHeaders(authorization, serialNumber)) .body(jsonData) .execute(); } [代码] DELETE 请求 [代码]/** * delete 请求 * * @param url 请求url * @param authorization 授权信息 * @param serialNumber 公钥证书序列号 * @param jsonData 请求参数 * @return {@link HttpResponse} 请求返回的结果 */ private HttpResponse doDelete(String url, String authorization, String serialNumber, String jsonData) { return HttpRequest.delete(url) .addHeaders(getHeaders(authorization, serialNumber)) .body(jsonData) .execute(); } [代码] 上传文件 [代码] /** * @param url 请求url * @param authorization 授权信息 * @param serialNumber 公钥证书序列号 * @param jsonData 请求参数 * @param file 上传的文件 * @return {@link HttpResponse} 请求返回的结果 */ private HttpResponse doUpload(String url, String authorization, String serialNumber, String jsonData, File file) { return HttpRequest.post(url) .addHeaders(getUploadHeaders(authorization, serialNumber)) .form("file", file) .form("meta", jsonData) .execute(); } [代码] 构建 Http 请求头 [代码]private Map<String, String> getBaseHeaders(String authorization) { String userAgent = String.format( "WeChatPay-IJPay-HttpClient/%s (%s) Java/%s", getClass().getPackage().getImplementationVersion(), OS, VERSION == null ? "Unknown" : VERSION); Map<String, String> headers = new HashMap<>(3); headers.put("Accept", ContentType.JSON.toString()); headers.put("Authorization", authorization); headers.put("User-Agent", userAgent); return headers; } private Map<String, String> getHeaders(String authorization, String serialNumber) { Map<String, String> headers = getBaseHeaders(authorization); headers.put("Content-Type", ContentType.JSON.toString()); if (StrUtil.isNotEmpty(serialNumber)) { headers.put("Wechatpay-Serial", serialNumber); } return headers; } private Map<String, String> getUploadHeaders(String authorization, String serialNumber) { Map<String, String> headers = getBaseHeaders(authorization); headers.put("Content-Type", "multipart/form-data;boundary=\"boundary\""); if (StrUtil.isNotEmpty(serialNumber)) { headers.put("Wechatpay-Serial", serialNumber); } return headers; } [代码] 构建 Http 请求返回值 从响应的 HttpResponse 中获取微信响应头信息、状态码以及 body [代码]/** * 构建返回参数 * * @param httpResponse {@link HttpResponse} * @return {@link Map} */ private Map<String, Object> buildResMap(HttpResponse httpResponse) { Map<String, Object> map = new HashMap<>(); String timestamp = httpResponse.header("Wechatpay-Timestamp"); String nonceStr = httpResponse.header("Wechatpay-Nonce"); String serialNo = httpResponse.header("Wechatpay-Serial"); String signature = httpResponse.header("Wechatpay-Signature"); String body = httpResponse.body(); int status = httpResponse.getStatus(); map.put("timestamp", timestamp); map.put("nonceStr", nonceStr); map.put("serialNumber", serialNo); map.put("signature", signature); map.put("body", body); map.put("status", status); return map; } [代码] 至此已完成构建请求参数,执行请求。接下来我们就要实现响应数据的解密以及响应结果的验证签名 对应的官方文档 证书和回调报文解密 签名验证 验证签名 构建签名参数 [代码]/** * 构造签名串 * * @param timestamp 应答时间戳 * @param nonceStr 应答随机串 * @param body 应答报文主体 * @return 应答待签名字符串 */ public static String buildSignMessage(String timestamp, String nonceStr, String body) { return new StringBuilder() .append(timestamp) .append("\n") .append(nonceStr) .append("\n") .append(body) .append("\n") .toString(); } [代码] 证书和回调报文解密 官方文档文末有完整的源码这里就不贴了。贴一个示例大家参数一下 [代码]try { String associatedData = "certificate"; String nonce = "80d28946a64a"; String cipherText = "DwAqW4+4TeUaOEylfKEXhw+XqGh/YTRhUmLw/tBfQ5nM9DZ9d+9aGEghycwV1jwo52vXb/t6ueBvBRHRIW5JgDRcXmTHw9IMTrIK6HxTt2qiaGTWJU9whsF+GGeQdA7gBCHZm3AJUwrzerAGW1mclXBTvXqaCl6haE7AOHJ2g4RtQThi3nxOI63/yc3WaiAlSR22GuCpy6wJBfljBq5Bx2xXDZXlF2TNbDIeodiEnJEG2m9eBWKuvKPyUPyClRXG1fdOkKnCZZ6u+ipb4IJx28n3MmhEtuc2heqqlFUbeONaRpXv6KOZmH/IdEL6nqNDP2D7cXutNVCi0TtSfC7ojnO/+PKRu3MGO2Z9q3zyZXmkWHCSms/C3ACatPUKHIK+92MxjSQDc1E/8faghTc9bDgn8cqWpVKcL3GHK+RfuYKiMcdSkUDJyMJOwEXMYNUdseQMJ3gL4pfxuQu6QrVvJ17q3ZjzkexkPNU4PNSlIBJg+KX61cyBTBumaHy/EbHiP9V2GeM729a0h5UYYJVedSo1guIGjMZ4tA3WgwQrlpp3VAMKEBLRJMcnHd4pH5YQ/4hiUlHGEHttWtnxKFwnJ6jHr3OmFLV1FiUUOZEDAqR0U1KhtGjOffnmB9tymWF8FwRNiH2Tee/cCDBaHhNtfPI5129SrlSR7bZc+h7uzz9z+1OOkNrWHzAoWEe3XVGKAywpn5HGbcL+9nsEVZRJLvV7aOxAZBkxhg8H5Fjt1ioTJL+qXgRzse1BX1iiwfCR0fzEWT9ldDTDW0Y1b3tb419MhdmTQB5FsMXYOzqp5h+Tz1FwEGsa6TJsmdjJQSNz+7qPSg5D6C2gc9/6PkysSu/6XfsWXD7cQkuZ+TJ/Xb6Q1Uu7ZB90SauA8uPQUIchW5zQ6UfK5dwMkOuEcE/141/Aw2rlDqjtsE17u1dQ6TCax/ZQTDQ2MDUaBPEaDIMPcgL7fCeijoRgovkBY92m86leZvQ+HVbxlFx5CoPhz4a81kt9XJuEYOztSIKlm7QNfW0BvSUhLmxDNCjcxqwyydtKbLzA+EBb2gG4ORiH8IOTbV0+G4S6BqetU7RrO+/nKt21nXVqXUmdkhkBakLN8FUcHygyWnVxbA7OI2RGnJJUnxqHd3kTbzD5Wxco4JIQsTOV6KtO5c960oVYUARZIP1SdQhqwELm27AktEN7kzg/ew/blnTys/eauGyw78XCROb9F1wbZBToUZ7L+8/m/2tyyyqNid+sC9fYqJoIOGfFOe6COWzTI/XPytCHwgHeUxmgk7NYfU0ukR223RPUOym6kLzSMMBKCivnNg68tbLRJHEOpQTXFBaFFHt2qpceJpJgw5sKFqx3eQnIFuyvA1i8s2zKLhULZio9hpsDJQREOcNeHVjEZazdCGnbe3Vjg7uqOoVHdE/YbNzJNQEsB3/erYJB+eGzyFwFmdAHenG5RE6FhCutjszwRiSvW9F7wvRK36gm7NnVJZkvlbGwh0UHr0pbcrOmxT81xtNSvMzT0VZNLTUX2ur3AGLwi2ej8BIC0H41nw4ToxTnwtFR1Xy55+pUiwpB7JzraA08dCXdFdtZ72Tw/dNBy5h1P7EtQYiKzXp6rndfOEWgNOsan7e1XRpCnX7xoAkdPvy40OuQ5gNbDKry5gVDEZhmEk/WRuGGaX06CG9m7NfErUsnQYrDJVjXWKYuARd9R7W0aa5nUXqz/Pjul/LAatJgWhZgFBGXhNr9iAoade/0FPpBj0QWa8SWqKYKiOqXqhfhppUq35FIa0a1Vvxcn3E38XYpVZVTDEXcEcD0RLCu/ezdOa6vRcB7hjgXFIRZQAka0aXnQxwOZwE2Rt3yWXqc+Q1ah2oOrg8Lg3ETc644X9QP4FxOtDwz/A=="; AesUtil aesUtil = new AesUtil(wxPayV3Bean.getApiKey3().getBytes(StandardCharsets.UTF_8)); // 平台证书密文解密 // encrypt_certificate 中的 associated_data nonce ciphertext String publicKey = aesUtil.decryptToString( associatedData.getBytes(StandardCharsets.UTF_8), nonce.getBytes(StandardCharsets.UTF_8), cipherText ); // 保存证书 FileWriter writer = new FileWriter(wxPayV3Bean.getPlatformCertPath()); writer.write(publicKey); // 获取平台证书序列号 X509Certificate certificate = PayKit.getCertificate(new ByteArrayInputStream(publicKey.getBytes())); return certificate.getSerialNumber().toString(16).toUpperCase(); } catch (Exception e) { e.printStackTrace(); } [代码] 验证签名 [代码]/** * 验证签名 * * @param signature 待验证的签名 * @param body 应答主体 * @param nonce 随机串 * @param timestamp 时间戳 * @param certInputStream 微信支付平台证书输入流 * @return 签名结果 * @throws Exception 异常信息 */ public static boolean verifySignature(String signature, String body, String nonce, String timestamp, InputStream certInputStream) throws Exception { String buildSignMessage = PayKit.buildSignMessage(timestamp, nonce, body); // 获取证书 X509Certificate certificate = PayKit.getCertificate(certInputStream); PublicKey publicKey = certificate.getPublicKey(); return RsaKit.checkByPublicKey(buildSignMessage, signature, publicKey); } /** * 公钥验证签名 * * @param data 需要加密的数据 * @param sign 签名 * @param publicKey 公钥 * @return 验证结果 * @throws Exception 异常信息 */ public static boolean checkByPublicKey(String data, String sign, PublicKey publicKey) throws Exception { java.security.Signature signature = java.security.Signature.getInstance("SHA256WithRSA"); signature.initVerify(publicKey); signature.update(data.getBytes(StandardCharsets.UTF_8)); return signature.verify(Base64.decode(sign.getBytes(StandardCharsets.UTF_8))); } [代码] 至此微信支付 Api-v3 接口已介绍完,如有疑问欢迎留言一起探讨。 完整示例 SpringBoot 参考资料 你真的了解 HTTPS 吗? WechatPay-API-v3
2021-03-02