├── .editorconfig ├── .github └── workflows │ ├── build.yml │ └── deploy.yml ├── .gitignore ├── LICENSE ├── README.md ├── UPGRADING.md ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src ├── main └── java │ └── com │ └── wechat │ └── pay │ └── contrib │ └── apache │ └── httpclient │ ├── Credentials.java │ ├── SignatureExec.java │ ├── Validator.java │ ├── WechatPayHttpClientBuilder.java │ ├── WechatPayUploadHttpPost.java │ ├── auth │ ├── AutoUpdateCertificatesVerifier.java │ ├── CertificatesVerifier.java │ ├── MixVerifier.java │ ├── PrivateKeySigner.java │ ├── PublicKeyVerifier.java │ ├── Signer.java │ ├── Verifier.java │ ├── WechatPay2Credentials.java │ └── WechatPay2Validator.java │ ├── cert │ ├── CertificatesManager.java │ └── SafeSingleScheduleExecutor.java │ ├── constant │ └── WechatPayHttpHeaders.java │ ├── exception │ ├── HttpCodeException.java │ ├── NotFoundException.java │ ├── ParseException.java │ ├── ValidationException.java │ └── WechatPayException.java │ ├── notification │ ├── Notification.java │ ├── NotificationHandler.java │ ├── NotificationRequest.java │ └── Request.java │ ├── proxy │ └── HttpProxyFactory.java │ └── util │ ├── AesUtil.java │ ├── CertSerializeUtil.java │ ├── PemUtil.java │ └── RsaCryptoUtil.java └── test └── java └── com └── wechat └── pay └── contrib └── apache └── httpclient ├── AutoUpdateVerifierTest.java ├── CertificatesManagerTest.java ├── HttpClientBuilderTest.java ├── NotificationHandlerTest.java └── RsaCryptoTest.java /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps maintain consistent coding styles for multiple developers working on the same project across various editors and IDEs 2 | # https://editorconfig.org 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | indent_size = 2 9 | indent_style = space 10 | insert_final_newline = true 11 | max_line_length = 100 12 | tab_width = 2 13 | trim_trailing_whitespace = true 14 | continuation_indent_size = 4 15 | 16 | [*.java] 17 | indent_size = 4 18 | max_line_length = 120 19 | tab_width = 4 20 | continuation_indent_size = 8 21 | 22 | [{*.gant,*.gradle,*.groovy,*.gson,*.gy}] 23 | indent_size = 4 24 | tab_width = 4 25 | continuation_indent_size = 8 26 | 27 | [{*.markdown,*.md}] 28 | indent_size = 4 29 | tab_width = 4 30 | continuation_indent_size = 8 31 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Gradle Build 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | 8 | # pr时校验gradle build是否通过 9 | build: 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | java-version: [ 8, 11 ] 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Set up JDK 17 | uses: actions/setup-java@v2 18 | with: 19 | java-version: ${{ matrix.java-version }} 20 | distribution: 'adopt' 21 | - name: Validate Gradle wrapper 22 | uses: gradle/wrapper-validation-action@e6e38bacfdf1a337459f332974bb2327a31aaf4b 23 | - name: Build with Gradle 24 | run: ./gradlew build 25 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Gradle Publish 2 | 3 | on: 4 | release: 5 | types: published 6 | jobs: 7 | 8 | # 校验tag是否满足语义化版本格式 9 | check-tag: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: Regex Match 14 | id: regex-match 15 | run: | 16 | result=$(printf ${{github.ref_name}} | perl -ne 'printf if /^v(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)(?:-(?P(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?P[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/') 17 | echo "::set-output name=result::$result" 18 | - name: Check Tag 19 | if: ${{ steps.regex-match.outputs.result != github.ref_name }} 20 | uses: actions/github-script@v4 21 | with: 22 | script: core.setFailed('Invalid Tag:${{github.ref_name}}') 23 | 24 | # Push Tag时自动发布新版本到Maven中央仓库 25 | publish: 26 | needs: [check-tag] 27 | runs-on: ubuntu-latest 28 | steps: 29 | - uses: actions/checkout@v2 30 | - name: Set up Java 31 | uses: actions/setup-java@v2 32 | with: 33 | java-version: '8' 34 | distribution: 'adopt' 35 | - name: Validate Gradle wrapper 36 | uses: gradle/wrapper-validation-action@e6e38bacfdf1a337459f332974bb2327a31aaf4b 37 | # 发布项目 38 | - name: Publish 39 | run: ./gradlew publish 40 | env: 41 | SONATYPE_NEXUS_USERNAME: ${{secrets.SONATYPE_NEXUS_USERNAME}} 42 | SONATYPE_NEXUS_PASSWORD: ${{secrets.SONATYPE_NEXUS_PASSWORD}} 43 | ORG_GRADLE_PROJECT_signingKey: ${{secrets.SIGNING_KEY}} 44 | ORG_GRADLE_PROJECT_signingPassword: ${{secrets.SIGNING_PASSWORD}} 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *.swp 3 | .idea 4 | .vscode 5 | .DS_Store 6 | *.iml 7 | 8 | # exclude jar for gradle wrapper 9 | !gradle/wrapper/*.jar 10 | 11 | 12 | # Gradle files 13 | .gradle/* 14 | build/* 15 | out/* 16 | */build/* 17 | */out/* 18 | 19 | # compiler output 20 | bin/ 21 | */bin/ 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wechatpay-apache-httpclient 2 | 3 | ## 概览 4 | 5 | [微信支付API v3](https://wechatpay-api.gitbook.io/wechatpay-api-v3/)的[Apache HttpClient](https://hc.apache.org/httpcomponents-client-ga/index.html)扩展,实现了请求签名的生成和应答签名的验证。 6 | 7 | > [!IMPORTANT] 8 | > 我们强烈建议你改为使用 [WechatPay-Java](https://github.com/wechatpay-apiv3/wechatpay-java),该SDK同样支持 Apache HttpClient 且提供了更完善的功能,本库未来只会进行必要的修复更新。 9 | 10 | ## 项目状态 11 | 12 | 当前版本`0.6.0`为测试版本。请商户的专业技术人员在使用时注意系统和软件的正确性和兼容性,以及带来的风险。 13 | 14 | ## 升级指引 15 | 16 | 若你使用的版本为`<=0.5.0`,升级前请参考[升级指南](UPGRADING.md)。 17 | 18 | ## 环境要求 19 | 20 | + Java 1.8+ 21 | 22 | ## 安装 23 | 24 | 最新版本已经在 [Maven Central](https://search.maven.org/artifact/com.github.wechatpay-apiv3/wechatpay-apache-httpclient) 发布。 25 | 26 | ### Gradle 27 | 28 | 在你的`build.gradle`文件中加入如下的依赖 29 | 30 | ```groovy 31 | implementation 'com.github.wechatpay-apiv3:wechatpay-apache-httpclient:0.6.0' 32 | ``` 33 | 34 | ### Maven 35 | 加入以下依赖 36 | 37 | ```xml 38 | 39 | com.github.wechatpay-apiv3 40 | wechatpay-apache-httpclient 41 | 0.6.0 42 | 43 | ``` 44 | 45 | ## 名词解释 46 | 47 | + **商户API证书**,是用来证实商户身份的。证书中包含商户号、证书序列号、证书有效期等信息,由证书授权机构(Certificate Authority ,简称CA)签发,以防证书被伪造或篡改。如何获取请见[商户API证书](https://wechatpay-api.gitbook.io/wechatpay-api-v3/ren-zheng/zheng-shu#shang-hu-api-zheng-shu)。 48 | + **商户API私钥**。商户申请商户API证书时,会生成商户私钥,并保存在本地证书文件夹的文件apiclient_key.pem中。注:不要把私钥文件暴露在公共场合,如上传到Github,写在客户端代码等。 49 | + **微信支付平台证书**。平台证书是指由微信支付负责申请的,包含微信支付平台标识、公钥信息的证书。商户可以使用平台证书中的公钥进行应答签名的验证。获取平台证书需通过[获取平台证书列表](https://wechatpay-api.gitbook.io/wechatpay-api-v3/ren-zheng/zheng-shu#ping-tai-zheng-shu)接口下载。 50 | + **微信支付公钥**。由微信支付生成,商户可以使用该公钥进行应答签名、回调签名的验证,详见:[微信支付公钥](https://pay.weixin.qq.com/doc/v3/merchant/4012153196)。 51 | + **证书序列号**。每个证书都有一个由CA颁发的唯一编号,即证书序列号。如何查看证书序列号请看[这里](https://wechatpay-api.gitbook.io/wechatpay-api-v3/chang-jian-wen-ti/zheng-shu-xiang-guan#ru-he-cha-kan-zheng-shu-xu-lie-hao)。 52 | + **API v3密钥**。为了保证安全性,微信支付在回调通知和平台证书下载接口中,对关键信息进行了AES-256-GCM加密。API v3密钥是加密时使用的对称密钥。商户可以在【商户平台】->【API安全】的页面设置该密钥。 53 | 54 | ## 开始 55 | 56 | 如果你使用的是`HttpClientBuilder`或者`HttpClients#custom()`来构造`HttpClient`,你可以直接替换为`WechatPayHttpClientBuilder`。 57 | ```java 58 | import com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder; 59 | 60 | //... 61 | WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create() 62 | .withMerchant(merchantId, merchantSerialNumber, merchantPrivateKey) 63 | .withWechatPay(wechatpayPublicKeyId, wechatPayPublicKey); 64 | // ... 接下来,你仍然可以通过builder设置各种参数,来配置你的HttpClient 65 | 66 | // 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签 67 | CloseableHttpClient httpClient = builder.build(); 68 | 69 | // 后面跟使用Apache HttpClient一样 70 | CloseableHttpResponse response = httpClient.execute(...); 71 | ``` 72 | 73 | 参数说明: 74 | 75 | + `merchantId`商户号。 76 | + `merchantSerialNumber`商户API证书的证书序列号。 77 | + `merchantPrivateKey`商户API私钥,如何加载商户API私钥请看[常见问题](#如何加载商户私钥)。 78 | + `wechatpayPublicKeyId`微信支付公钥ID,登录商户平台可获取,详见:[获取微信支付公钥](https://pay.weixin.qq.com/doc/v3/merchant/4013053249#1.-%E8%8E%B7%E5%8F%96%E5%BE%AE%E4%BF%A1%E6%94%AF%E4%BB%98%E5%85%AC%E9%92%A5) 79 | + `wechatPayPublicKey`微信支付公钥,登录商户平台可获取,详见:[获取微信支付公钥](https://pay.weixin.qq.com/doc/v3/merchant/4013053249#1.-%E8%8E%B7%E5%8F%96%E5%BE%AE%E4%BF%A1%E6%94%AF%E4%BB%98%E5%85%AC%E9%92%A5) 80 | 81 | ### 示例:获取平台证书 82 | 83 | 你可以使用`WechatPayHttpClientBuilder`构造的`HttpClient`发送请求和应答了。 84 | 85 | ```java 86 | URIBuilder uriBuilder = new URIBuilder("https://api.mch.weixin.qq.com/v3/certificates"); 87 | HttpGet httpGet = new HttpGet(uriBuilder.build()); 88 | httpGet.addHeader("Accept", "application/json"); 89 | 90 | CloseableHttpResponse response = httpClient.execute(httpGet); 91 | 92 | String bodyAsString = EntityUtils.toString(response.getEntity()); 93 | System.out.println(bodyAsString); 94 | ``` 95 | 96 | ### 示例:JSAPI下单 97 | 98 | 注: 99 | 100 | + 我们使用了 jackson-databind 演示拼装 Json,你也可以使用自己熟悉的 Json 库 101 | + 请使用你自己的测试商户号、appid 以及对应的 openid 102 | 103 | ```java 104 | HttpPost httpPost = new HttpPost("https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi"); 105 | httpPost.addHeader("Accept", "application/json"); 106 | httpPost.addHeader("Content-type","application/json; charset=utf-8"); 107 | 108 | ByteArrayOutputStream bos = new ByteArrayOutputStream(); 109 | ObjectMapper objectMapper = new ObjectMapper(); 110 | 111 | ObjectNode rootNode = objectMapper.createObjectNode(); 112 | rootNode.put("mchid","1900009191") 113 | .put("appid", "wxd678efh567hg6787") 114 | .put("description", "Image形象店-深圳腾大-QQ公仔") 115 | .put("notify_url", "https://www.weixin.qq.com/wxpay/pay.php") 116 | .put("out_trade_no", "1217752501201407033233368018"); 117 | rootNode.putObject("amount") 118 | .put("total", 1); 119 | rootNode.putObject("payer") 120 | .put("openid", "oUpF8uMuAJO_M2pxb1Q9zNjWeS6o"); 121 | 122 | objectMapper.writeValue(bos, rootNode); 123 | 124 | httpPost.setEntity(new StringEntity(bos.toString("UTF-8"), "UTF-8")); 125 | CloseableHttpResponse response = httpClient.execute(httpPost); 126 | 127 | String bodyAsString = EntityUtils.toString(response.getEntity()); 128 | System.out.println(bodyAsString); 129 | ``` 130 | 131 | ### 示例:查单 132 | 133 | ```java 134 | URIBuilder uriBuilder = new URIBuilder("https://api.mch.weixin.qq.com/v3/pay/transactions/id/4200000889202103303311396384?mchid=1230000109"); 135 | HttpGet httpGet = new HttpGet(uriBuilder.build()); 136 | httpGet.addHeader("Accept", "application/json"); 137 | 138 | CloseableHttpResponse response = httpClient.execute(httpGet); 139 | 140 | String bodyAsString = EntityUtils.toString(response.getEntity()); 141 | System.out.println(bodyAsString); 142 | ``` 143 | 144 | ### 示例:关单 145 | 146 | ```java 147 | HttpPost httpPost = new HttpPost("https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/1217752501201407033233368018/close"); 148 | httpPost.addHeader("Accept", "application/json"); 149 | httpPost.addHeader("Content-type","application/json; charset=utf-8"); 150 | 151 | ByteArrayOutputStream bos = new ByteArrayOutputStream(); 152 | ObjectMapper objectMapper = new ObjectMapper(); 153 | 154 | ObjectNode rootNode = objectMapper.createObjectNode(); 155 | rootNode.put("mchid","1900009191"); 156 | 157 | objectMapper.writeValue(bos, rootNode); 158 | 159 | httpPost.setEntity(new StringEntity(bos.toString("UTF-8"), "UTF-8")); 160 | CloseableHttpResponse response = httpClient.execute(httpPost); 161 | 162 | String bodyAsString = EntityUtils.toString(response.getEntity()); 163 | System.out.println(bodyAsString); 164 | ``` 165 | 166 | ## 定制 167 | 168 | 当默认的本地签名和验签方式不适合你的系统时,你可以通过实现`Signer`或者`Verifier`来定制签名和验签。比如,你的系统把商户私钥集中存储,业务系统需通过远程调用进行签名,你可以这样做。 169 | 170 | ```java 171 | import com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder; 172 | import com.wechat.pay.contrib.apache.httpclient.Credentials; 173 | 174 | // ... 175 | Credentials credentials = new WechatPay2Credentials(merchantId, new Signer() { 176 | @Override 177 | public Signer.SignatureResult sign(byte[] message) { 178 | // ... call your sign-RPC, then return sign & serial number 179 | } 180 | }); 181 | WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create() 182 | .withCredentials(credentials) 183 | .withWechatPay(wechatpayPublicKeyId, wechatPayPublicKey); 184 | ``` 185 | 186 | ## 定时更新平台证书功能 187 | 188 | > [!IMPORTANT] 189 | > 新注册的商户使用「微信支付公钥」验签,不需要下载和更新平台证书。仅尚未完全迁移至「微信支付公钥」验签的旧商户才需要此能力。 190 | 191 | 版本>=`0.4.0`可使用 CertificatesManager.getVerifier(merchantId) 得到的验签器替代默认的验签器。它会定时下载和更新商户对应的[微信支付平台证书](https://wechatpay-api.gitbook.io/wechatpay-api-v3/ren-zheng/zheng-shu#ping-tai-zheng-shu) (默认下载间隔为UPDATE_INTERVAL_MINUTE)。 192 | 193 | 示例代码: 194 | ```java 195 | // 获取证书管理器实例 196 | certificatesManager = CertificatesManager.getInstance(); 197 | // 向证书管理器增加需要自动更新平台证书的商户信息 198 | certificatesManager.putMerchant(merchantId, new WechatPay2Credentials(merchantId, 199 | new PrivateKeySigner(merchantSerialNumber, merchantPrivateKey)), apiV3Key.getBytes(StandardCharsets.UTF_8)); 200 | // ... 若有多个商户号,可继续调用putMerchant添加商户信息 201 | 202 | // 从证书管理器中获取verifier 203 | verifier = certificatesManager.getVerifier(merchantId); 204 | WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create() 205 | .withMerchant(merchantId, merchantSerialNumber, merchantPrivateKey) 206 | .withValidator(new WechatPay2Validator(verifier)); 207 | // ... 接下来,你仍然可以通过builder设置各种参数,来配置你的HttpClient 208 | 209 | // 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新 210 | CloseableHttpClient httpClient = builder.build(); 211 | 212 | // 后面跟使用Apache HttpClient一样 213 | CloseableHttpResponse response = httpClient.execute(...); 214 | ``` 215 | 216 | ### 风险 217 | 218 | 因为不需要传入微信支付平台证书,CertificatesManager 在首次更新证书时**不会验签**,也就无法确认应答身份,可能导致下载错误的证书。 219 | 220 | 但下载时会通过 **HTTPS**、**AES 对称加密**来保证证书安全,所以可以认为,在使用官方 JDK、且 APIv3 密钥不泄露的情况下,CertificatesManager 是**安全**的。 221 | 222 | ## 敏感信息加解密 223 | 224 | ### 加密 225 | 226 | 使用` RsaCryptoUtil.encryptOAEP(String, PublicKey publicKey)`进行公钥加密。示例代码如下。 227 | 228 | ```java 229 | // 建议从Verifier中获得微信支付平台证书,或使用预先下载到本地的平台证书文件中 230 | PublicKey publicKey = verifier.getValidPublicKey(); 231 | try { 232 | String ciphertext = RsaCryptoUtil.encryptOAEP(text, publicKey); 233 | } catch (IllegalBlockSizeException e) { 234 | e.printStackTrace(); 235 | } 236 | ``` 237 | 238 | ### 解密 239 | 240 | 使用`RsaCryptoUtil.decryptOAEP(String ciphertext, PrivateKey privateKey)`进行私钥解密。示例代码如下。 241 | 242 | ```java 243 | // 使用商户私钥解密 244 | try { 245 | String ciphertext = RsaCryptoUtil.decryptOAEP(text, merchantPrivateKey); 246 | } catch (BadPaddingException e) { 247 | e.printStackTrace(); 248 | } 249 | ``` 250 | 251 | ## 图片/视频上传 252 | 253 | 我们对上传的参数组装和签名逻辑进行了一定的封装,只需要以下几步: 254 | 255 | 1. 使用`WechatPayUploadHttpPost`构造一个上传的`HttpPost`,需设置待上传文件的文件名,SHA256摘要,文件的输入流。在`0.4.1`及以上版本,支持设置媒体文件元信息。 256 | 2. 通过`WechatPayHttpClientBuilder`得到的`HttpClient`发送请求。 257 | 258 | 示例请参考下列代码。 259 | 260 | ```java 261 | String filePath = "/your/home/hellokitty.png"; 262 | URI uri = new URI("https://api.mch.weixin.qq.com/v3/merchant/media/upload"); 263 | File file = new File(filePath); 264 | 265 | try (FileInputStream ins1 = new FileInputStream(file)) { 266 | String sha256 = DigestUtils.sha256Hex(ins1); 267 | try (InputStream ins2 = new FileInputStream(file)) { 268 | HttpPost request = new WechatPayUploadHttpPost.Builder(uri) 269 | // 如需直接设置媒体文件元信息,可使用withFile代替withImage 270 | .withImage(file.getName(), sha256, ins2) 271 | .build(); 272 | CloseableHttpResponse response1 = httpClient.execute(request); 273 | } 274 | } 275 | ``` 276 | 277 | [AutoUpdateVerifierTest.uploadImageTest](/src/test/java/com/wechat/pay/contrib/apache/httpclient/AutoUpdateVerifierTest.java#90)是一个更完整的示例。 278 | 279 | ## 回调通知的验签与解密 280 | 版本>=`0.4.2`可使用 `NotificationHandler.parse(request)` 对回调通知验签和解密: 281 | 282 | 1. 使用`NotificationRequest`构造一个回调通知请求体,需设置应答平台证书序列号、应答随机串、应答时间戳、应答签名串、应答主体。 283 | 2. 使用`NotificationHandler`构造一个回调通知处理器,需设置验证器、apiV3密钥。调用`parse(request)`得到回调通知`notification`。 284 | 285 | 示例请参考下列代码。 286 | 287 | ```java 288 | // 构建request,传入必要参数 289 | NotificationRequest request = new NotificationRequest.Builder().withSerialNumber(wechatPaySerial) 290 | .withNonce(nonce) 291 | .withTimestamp(timestamp) 292 | .withSignature(signature) 293 | .withBody(body) 294 | .build(); 295 | 296 | // 如果已经初始化了 NotificationHandler 则直接使用,否则根据具体情况创建一个 297 | 298 | // 1. 如果你使用的是微信支付公私钥,则使用公钥初始化 Verifier 以创建 NotificationHandler 299 | NotificationHandler handler = new NotificationHandler( 300 | new PublicKeyVerifier(wechatPayPublicKeyId, wechatPayPublicKey), 301 | apiV3Key.getBytes(StandardCharsets.UTF_8) 302 | ); 303 | 304 | // 2. 如果你使用的事微信支付平台证书,则从 CertificatesManager 获取 Verifier 以创建 NotificationHandler 305 | NotificationHandler handler = new NotificationHandler( 306 | certificatesManager.getVerifier(merchantId), 307 | apiV3Key.getBytes(StandardCharsets.UTF_8) 308 | ); 309 | 310 | // 3. 如果你正在进行微信支付平台证书到微信支付公私钥的灰度切换,希望保持切换兼容,则需要使用 MixVerifier 创建 NotificationHandler 311 | Verifier mixVerifier = new MixVerifier( 312 | new PublicKeyVerifier(wechatPayPublicKeyId, wechatPayPublicKey), 313 | certificatesManager.getVerifier(merchantId) 314 | ); 315 | NotificationHandler handler = new NotificationHandler( 316 | mixVerifier, 317 | apiV3Key.getBytes(StandardCharsets.UTF_8) 318 | ); 319 | 320 | // 验签和解析请求体 321 | Notification notification = handler.parse(request); 322 | // 从notification中获取解密报文 323 | System.out.println(notification.getDecryptData()); 324 | ``` 325 | 326 | [NotificationHandlerTest](src/test/java/com/wechat/pay/contrib/apache/httpclient/NotificationHandlerTest.java#105)是一个更完整的示例。 327 | 328 | ### 异常处理 329 | `parse(request)`可能返回以下异常,推荐对异常打日志或上报监控。 330 | - 抛出`ValidationException`时,请先检查传入参数是否与回调通知参数一致。若一致,说明参数可能被恶意篡改导致验签失败。 331 | - 抛出`ParseException`时,请先检查传入包体是否与回调通知包体一致。若一致,请检查AES密钥是否正确设置。若正确,说明包体可能被恶意篡改导致解析失败。 332 | 333 | ## 常见问题 334 | 335 | ### 如何加载商户私钥 336 | 337 | 商户申请商户API证书时,会生成商户私钥,并保存在本地证书文件夹的文件`apiclient_key.pem`中。商户开发者可以使用方法`PemUtil.loadPrivateKey()`加载证书。 338 | 339 | ```java 340 | // 示例:私钥存储在文件 341 | PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey( 342 | new FileInputStream("/path/to/apiclient_key.pem")); 343 | 344 | // 示例:私钥为String字符串 345 | PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey( 346 | new ByteArrayInputStream(privateKey.getBytes("utf-8"))); 347 | ``` 348 | 349 | ### 如何下载平台证书? 350 | 351 | 使用`WechatPayHttpClientBuilder`需要调用`withWechatPay`设置[微信支付平台证书](https://wechatpay-api.gitbook.io/wechatpay-api-v3/ren-zheng/zheng-shu#ping-tai-zheng-shu),而平台证书又只能通过调用[获取平台证书接口](https://wechatpay-api.gitbook.io/wechatpay-api-v3/jie-kou-wen-dang/ping-tai-zheng-shu#huo-qu-ping-tai-zheng-shu-lie-biao)下载。为了解开"死循环",你可以在第一次下载平台证书时,按照下述方法临时"跳过”应答签名的验证。 352 | 353 | ```java 354 | CloseableHttpClient httpClient = WechatPayHttpClientBuilder.create() 355 | .withMerchant(merchantId, merchantSerialNumber, merchantPrivateKey) 356 | .withValidator(response -> true) // NOTE: 设置一个空的应答签名验证器,**不要**用在业务请求 357 | .build(); 358 | ``` 359 | 360 | **注意**:业务请求请使用标准的初始化流程,务必验证应答签名。 361 | 362 | ### 如何下载账单 363 | 364 | 因为下载的账单文件可能会很大,为了平衡系统性能和签名验签的实现成本,[账单下载API](https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/pay/bill/chapter3_3.shtml)被分成了两个步骤: 365 | 366 | 1. `/v3/bill/tradebill` 获取账单下载链接和账单摘要 367 | 2. `/v3/billdownload/file` 账单文件下载,请求需签名但应答不签名 368 | 369 | 因为第二步不包含应答签名,我们可以参考上一个问题下载平台证书的方法,使用`withValidator(response -> true)`“跳过”应答的签名校验。 370 | 371 | **注意**:开发者在下载文件之后,应使用第一步获取的账单摘要校验文件的完整性。 372 | 373 | ### 证书和回调解密需要的AesGcm解密在哪里? 374 | 375 | 请参考[AesUtil.Java](https://github.com/wechatpay-apiv3/wechatpay-apache-httpclient/blob/master/src/main/java/com/wechat/pay/contrib/apache/httpclient/util/AesUtil.java)。 376 | 377 | ### 我想使用以前的版本,要怎么办 378 | 379 | 之前的版本可以从 [jitpack](https://jitpack.io/#wechatpay-apiv3/wechatpay-apache-httpclient) 获取。例如希望使用0.1.6版本,gradle中可以使用以下的方式。 380 | 381 | ```groovy 382 | repositories { 383 | ... 384 | maven { url 'https://jitpack.io' } 385 | } 386 | ... 387 | dependencies { 388 | implementation 'com.github.wechatpay-apiv3:wechatpay-apache-httpclient:0.1.6' 389 | ... 390 | } 391 | ``` 392 | 393 | ### 如何解决Jackson NoSuchMethodError报错 394 | 395 | 在之前的版本中,我们出于安全考虑升级 Jackson 到`2.12`,并使用了`2.11`版本中新增的方法`readValue(String src, Class valueType)`。如果你的项目所依赖的其他组件又依赖了低于`2.11`版本的 Jackson ,可能会出现依赖冲突。 396 | 397 | 我们建议有能力的开发者,升级冲突组件至较新的兼容版本。例如,issue [#125](https://github.com/wechatpay-apiv3/wechatpay-apache-httpclient/issues/125) 版本 <`2.3.x` 的 SpringBoot 官方已不再维护,继续使用可能会有安全隐患。 398 | 399 | 如果难以升级,你可以用下面的方式引入 [jackson-bom](https://github.com/FasterXML/jackson-bom) 来升级 Jackson 版本。根据[通用漏洞披露信息](https://cve.mitre.org/),我们推荐升级到`2.13.2.20220328`版本。 400 | 401 | #### Gradle 402 | ```groovy 403 | implementation(platform("com.fasterxml.jackson:jackson-bom:2.13.2.20220328")) 404 | ``` 405 | #### Maven 406 | ```xml 407 | 408 | com.fasterxml.jackson 409 | jackson-bom 410 | 2.13.2.20220328 411 | 412 | ``` 413 | 414 | 如果出现其他组件的 `NoSuchMethodError` 报错,一般是依赖冲突导致。我们可以参考下面的解决思路: 415 | 1. 从报错信息中找到出现问题的组件(如上面的 Jackson )。根据你的项目的构建方式,选择 [Gradle](https://docs.gradle.org/current/userguide/viewing_debugging_dependencies.html#sec:listing_dependencies) 或 [Maven](https://maven.apache.org/plugins/maven-dependency-plugin/tree-mojo.html) 工具列出项目的依赖关系树,找到问题组件的所有版本号。 416 | 2. 从报错信息中找到正确的组件版本号。一般来说,导致报错的原因是使用的组件版本太低,所以我们可以找组件在依赖关系树中最新的版本号。 417 | 3. 指定组件版本。如果组件提供了 bom 依赖,可以使用上述方式引入 bom 依赖来指定版本。否则,根据你的项目的构建方式,选择 [Gradle](https://docs.gradle.org/current/userguide/dependency_constraints.html#sec:adding-constraints-transitive-deps) 或 [Maven](https://maven.apache.org/guides/introduction/introduction-to-dependency-mechanism.html) 的方式来指定版本。 418 | 419 | ### 更多常见问题 420 | 421 | 请看商户平台的[常见问题](https://pay.weixin.qq.com/wiki/doc/apiv3_partner/wechatpay/wechatpay7_0.shtml),或者[这里](https://wechatpay-api.gitbook.io/wechatpay-api-v3/chang-jian-wen-ti)。 422 | 423 | ## 联系我们 424 | 425 | 如果你发现了**BUG**或者有任何疑问、建议,请通过issue进行反馈。 426 | 427 | 也欢迎访问我们的[开发者社区](https://developers.weixin.qq.com/community/pay)。 428 | -------------------------------------------------------------------------------- /UPGRADING.md: -------------------------------------------------------------------------------- 1 | # 升级指南 2 | 3 | ## 从 0.5.0 升级至 0.6.0 4 | `interface Verifier` 不再提供 `getValidCertificate` 接口,请换用 `getValidPublicKey` 接口。 5 | 请注意 `getValidCertificate` 与 `getValidPublicKey` 并不能等价替换,但其返回值都可以用于调用 `RsaCryptoUtil.encryptOAEP` 实现加密。 6 | 7 | ## 从 0.3.0 升级至 0.4.0 8 | 9 | 版本`0.4.0`提供了支持多商户号的[定时更新平台证书功能](README.md#定时更新平台证书功能),不兼容版本`0.3.0`。推荐升级方式如下: 10 | 11 | - 若你使用了`ScheduledUpdateCertificatesVerifier`,请使用`CertificatesManager`替换: 12 | 13 | ```diff 14 | -verifier = new ScheduledUpdateCertificatesVerifier( 15 | - new WechatPay2Credentials(merchantId, new PrivateKeySigner(merchantSerialNumber, merchantPrivateKey)), 16 | - apiV3Key.getBytes(StandardCharsets.UTF_8)); 17 | +// 获取证书管理器实例 18 | +certificatesManager = CertificatesManager.getInstance(); 19 | +// 向证书管理器增加需要自动更新平台证书的商户信息 20 | +certificatesManager.putMerchant(merchantId, new WechatPay2Credentials(merchantId, 21 | + new PrivateKeySigner(merchantSerialNumber, merchantPrivateKey)), apiV3Key.getBytes(StandardCharsets.UTF_8)); 22 | +// 从证书管理器中获取verifier 23 | +verifier = certificatesManager.getVerifier(merchantId); 24 | ``` 25 | 26 | - 若你使用了`getLatestCertificate`方法,请使用`getValidCertificate`方法替换。 27 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java-library' 3 | id 'maven-publish' 4 | id 'signing' 5 | } 6 | 7 | group 'com.github.wechatpay-apiv3' 8 | version '0.6.0' 9 | 10 | sourceCompatibility = 1.8 11 | targetCompatibility = 1.8 12 | 13 | repositories { 14 | mavenCentral() 15 | } 16 | 17 | ext { 18 | publishedArtifactId = project.name 19 | 20 | httpclient_version = "4.5.13" 21 | slf4j_version = "1.7.36" 22 | junit_version = "4.13.2" 23 | jackson_version = "2.13.4.2" 24 | } 25 | 26 | jar { 27 | manifest { 28 | attributes('Automatic-Module-Name': 'com.wechat.pay.contrib.apache.httpclient') 29 | attributes('Implementation-Version': project.version) 30 | } 31 | } 32 | 33 | dependencies { 34 | api "org.apache.httpcomponents:httpclient:$httpclient_version" 35 | implementation "org.apache.httpcomponents:httpmime:$httpclient_version" 36 | implementation "com.fasterxml.jackson.core:jackson-databind:$jackson_version" 37 | implementation "org.slf4j:slf4j-api:$slf4j_version" 38 | testImplementation "org.slf4j:slf4j-simple:$slf4j_version" 39 | testImplementation "junit:junit:$junit_version" 40 | } 41 | 42 | test { 43 | ignoreFailures = true 44 | } 45 | 46 | publishing { 47 | java { 48 | withJavadocJar() 49 | withSourcesJar() 50 | } 51 | 52 | publications { 53 | maven(MavenPublication) { 54 | artifactId = project.ext.publishedArtifactId 55 | from components.java 56 | 57 | pom { 58 | name = project.name 59 | description = 'An Apache HttpClient decorator for WeChat Pay\'s API' 60 | url = 'https://github.com/wechatpay-apiv3/wechatpay-apache-httpclient' 61 | licenses { 62 | license { 63 | name = 'The Apache Software License, Version 2.0' 64 | url = 'https://www.apache.org/licenses/LICENSE-2.0.txt' 65 | } 66 | } 67 | developers { 68 | developer { 69 | name = 'WeChat Pay APIv3 Team' 70 | } 71 | } 72 | scm { 73 | connection = 'scm:git:https://github.com/wechatpay-apiv3/wechatpay-apache-httpclient.git' 74 | developerConnection = 'scm:git:ssh://github.com/wechatpay-apiv3/wechatpay-apache-httpclient.git' 75 | url = 'https://github.com/wechatpay-apiv3/wechatpay-apache-httpclient' 76 | } 77 | } 78 | } 79 | } 80 | 81 | repositories { 82 | maven { 83 | name = "mavencentral" 84 | def snapshotsRepoUrl = "https://oss.sonatype.org/content/repositories/snapshots" 85 | def releasesRepoUrl = "https://oss.sonatype.org/service/local/staging/deploy/maven2/" 86 | url = version.endsWith('SNAPSHOT') ? snapshotsRepoUrl : releasesRepoUrl 87 | credentials { 88 | username System.getenv('SONATYPE_NEXUS_USERNAME') 89 | password System.getenv('SONATYPE_NEXUS_PASSWORD') 90 | } 91 | } 92 | } 93 | } 94 | 95 | signing { 96 | def signingKey = findProperty("signingKey") 97 | def signingPassword = findProperty("signingPassword") 98 | useInMemoryPgpKeys(signingKey, signingPassword) 99 | sign publishing.publications.maven 100 | } 101 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechatpay-apiv3/wechatpay-apache-httpclient/d60ea25b3c1d60797cf544dbbc3655c07b2739a5/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.4.1-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto init 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto init 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :init 68 | @rem Get command-line arguments, handling Windows variants 69 | 70 | if not "%OS%" == "Windows_NT" goto win9xME_args 71 | 72 | :win9xME_args 73 | @rem Slurp the command line arguments. 74 | set CMD_LINE_ARGS= 75 | set _SKIP=2 76 | 77 | :win9xME_args_slurp 78 | if "x%~1" == "x" goto execute 79 | 80 | set CMD_LINE_ARGS=%* 81 | 82 | :execute 83 | @rem Setup the command line 84 | 85 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 86 | 87 | 88 | @rem Execute Gradle 89 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 90 | 91 | :end 92 | @rem End local scope for the variables with windows NT shell 93 | if "%ERRORLEVEL%"=="0" goto mainEnd 94 | 95 | :fail 96 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 97 | rem the _cmd.exe /c_ return code! 98 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 99 | exit /b 1 100 | 101 | :mainEnd 102 | if "%OS%"=="Windows_NT" endlocal 103 | 104 | :omega 105 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'wechatpay-apache-httpclient' 2 | -------------------------------------------------------------------------------- /src/main/java/com/wechat/pay/contrib/apache/httpclient/Credentials.java: -------------------------------------------------------------------------------- 1 | package com.wechat.pay.contrib.apache.httpclient; 2 | 3 | import java.io.IOException; 4 | import org.apache.http.client.methods.HttpRequestWrapper; 5 | 6 | /** 7 | * @author xy-peng 8 | */ 9 | public interface Credentials { 10 | 11 | String getSchema(); 12 | 13 | String getToken(HttpRequestWrapper request) throws IOException; 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/wechat/pay/contrib/apache/httpclient/SignatureExec.java: -------------------------------------------------------------------------------- 1 | package com.wechat.pay.contrib.apache.httpclient; 2 | 3 | import static com.wechat.pay.contrib.apache.httpclient.constant.WechatPayHttpHeaders.WECHAT_PAY_SERIAL; 4 | import static org.apache.http.HttpHeaders.AUTHORIZATION; 5 | import static org.apache.http.HttpStatus.SC_MULTIPLE_CHOICES; 6 | import static org.apache.http.HttpStatus.SC_OK; 7 | 8 | import java.io.IOException; 9 | import java.util.Arrays; 10 | import org.apache.http.HttpEntity; 11 | import org.apache.http.HttpEntityEnclosingRequest; 12 | import org.apache.http.HttpException; 13 | import org.apache.http.StatusLine; 14 | import org.apache.http.client.methods.CloseableHttpResponse; 15 | import org.apache.http.client.methods.HttpExecutionAware; 16 | import org.apache.http.client.methods.HttpRequestWrapper; 17 | import org.apache.http.client.protocol.HttpClientContext; 18 | import org.apache.http.conn.routing.HttpRoute; 19 | import org.apache.http.entity.BufferedHttpEntity; 20 | import org.apache.http.impl.execchain.ClientExecChain; 21 | import org.apache.http.util.EntityUtils; 22 | import org.slf4j.Logger; 23 | import org.slf4j.LoggerFactory; 24 | 25 | /** 26 | * @author xy-peng 27 | */ 28 | public class SignatureExec implements ClientExecChain { 29 | 30 | private static final String WECHAT_PAY_HOST_NAME_SUFFIX = ".mch.weixin.qq.com"; 31 | private static final Logger log = LoggerFactory.getLogger(SignatureExec.class); 32 | private final ClientExecChain mainExec; 33 | private final Credentials credentials; 34 | private final Validator validator; 35 | 36 | protected SignatureExec(Credentials credentials, Validator validator, ClientExecChain mainExec) { 37 | this.credentials = credentials; 38 | this.validator = validator; 39 | this.mainExec = mainExec; 40 | } 41 | 42 | protected void convertToRepeatableResponseEntity(CloseableHttpResponse response) throws IOException { 43 | HttpEntity entity = response.getEntity(); 44 | if (entity != null) { 45 | response.setEntity(new BufferedHttpEntity(entity)); 46 | } 47 | } 48 | 49 | protected void convertToRepeatableRequestEntity(HttpRequestWrapper request) throws IOException { 50 | if (isEntityEnclosing(request)) { 51 | HttpEntity entity = ((HttpEntityEnclosingRequest) request).getEntity(); 52 | if (entity != null) { 53 | ((HttpEntityEnclosingRequest) request).setEntity(new BufferedHttpEntity(entity)); 54 | } 55 | } 56 | } 57 | 58 | @Override 59 | public CloseableHttpResponse execute(HttpRoute route, HttpRequestWrapper request, HttpClientContext context, 60 | HttpExecutionAware execAware) throws IOException, HttpException { 61 | if (request.getTarget().getHostName().endsWith(WECHAT_PAY_HOST_NAME_SUFFIX)) { 62 | return executeWithSignature(route, request, context, execAware); 63 | } else { 64 | return mainExec.execute(route, request, context, execAware); 65 | } 66 | } 67 | 68 | private boolean isEntityEnclosing(HttpRequestWrapper request) { 69 | return request instanceof HttpEntityEnclosingRequest; 70 | } 71 | 72 | private boolean isUploadHttpPost(HttpRequestWrapper request) { 73 | return request.getOriginal() instanceof WechatPayUploadHttpPost; 74 | } 75 | 76 | private CloseableHttpResponse executeWithSignature(HttpRoute route, HttpRequestWrapper request, 77 | HttpClientContext context, 78 | HttpExecutionAware execAware) throws IOException, HttpException { 79 | // 上传类不需要消耗两次故不做转换 80 | if (!isUploadHttpPost(request)) { 81 | convertToRepeatableRequestEntity(request); 82 | } 83 | // 添加认证信息 84 | request.addHeader(AUTHORIZATION, credentials.getSchema() + " " + credentials.getToken(request)); 85 | request.addHeader(WECHAT_PAY_SERIAL, validator.getSerialNumber()); 86 | // 执行 87 | CloseableHttpResponse response = mainExec.execute(route, request, context, execAware); 88 | // 对成功应答验签 89 | StatusLine statusLine = response.getStatusLine(); 90 | if (statusLine.getStatusCode() >= SC_OK && statusLine.getStatusCode() < SC_MULTIPLE_CHOICES) { 91 | convertToRepeatableResponseEntity(response); 92 | if (!validator.validate(response)) { 93 | throw new HttpException("应答的微信支付签名验证失败"); 94 | } 95 | } else { 96 | // 错误应答需要打日志 97 | log.error("应答的状态码不为200-299。status code[{}]\trequest headers[{}]", statusLine.getStatusCode(), 98 | Arrays.toString(request.getAllHeaders())); 99 | if (isEntityEnclosing(request) && !isUploadHttpPost(request)) { 100 | HttpEntity entity = ((HttpEntityEnclosingRequest) request).getEntity(); 101 | String body = EntityUtils.toString(entity); 102 | log.error("应答的状态码不为200-299。request body[{}]", body); 103 | } 104 | } 105 | return response; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/main/java/com/wechat/pay/contrib/apache/httpclient/Validator.java: -------------------------------------------------------------------------------- 1 | package com.wechat.pay.contrib.apache.httpclient; 2 | 3 | import java.io.IOException; 4 | import org.apache.http.client.methods.CloseableHttpResponse; 5 | 6 | /** 7 | * @author xy-peng 8 | */ 9 | public interface Validator { 10 | 11 | boolean validate(CloseableHttpResponse response) throws IOException; 12 | 13 | String getSerialNumber(); 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/wechat/pay/contrib/apache/httpclient/WechatPayHttpClientBuilder.java: -------------------------------------------------------------------------------- 1 | package com.wechat.pay.contrib.apache.httpclient; 2 | 3 | import com.wechat.pay.contrib.apache.httpclient.auth.CertificatesVerifier; 4 | import com.wechat.pay.contrib.apache.httpclient.auth.PrivateKeySigner; 5 | import com.wechat.pay.contrib.apache.httpclient.auth.PublicKeyVerifier; 6 | import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Credentials; 7 | import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Validator; 8 | import java.security.PrivateKey; 9 | import java.security.PublicKey; 10 | import java.security.cert.X509Certificate; 11 | import java.util.List; 12 | import org.apache.http.impl.client.CloseableHttpClient; 13 | import org.apache.http.impl.client.HttpClientBuilder; 14 | import org.apache.http.impl.execchain.ClientExecChain; 15 | import org.apache.http.HttpHost; 16 | 17 | /** 18 | * @author xy-peng 19 | */ 20 | public class WechatPayHttpClientBuilder extends HttpClientBuilder { 21 | 22 | private static final String OS = System.getProperty("os.name") + "/" + System.getProperty("os.version"); 23 | private static final String VERSION = System.getProperty("java.version"); 24 | private Credentials credentials; 25 | private Validator validator; 26 | 27 | 28 | private WechatPayHttpClientBuilder() { 29 | super(); 30 | 31 | String userAgent = String.format( 32 | "WechatPay-Apache-HttpClient/%s (%s) Java/%s", 33 | getClass().getPackage().getImplementationVersion(), 34 | OS, 35 | VERSION == null ? "Unknown" : VERSION); 36 | setUserAgent(userAgent); 37 | } 38 | 39 | public static WechatPayHttpClientBuilder create() { 40 | return new WechatPayHttpClientBuilder(); 41 | } 42 | 43 | public WechatPayHttpClientBuilder withMerchant(String merchantId, String serialNo, PrivateKey privateKey) { 44 | this.credentials = new WechatPay2Credentials(merchantId, new PrivateKeySigner(serialNo, privateKey)); 45 | return this; 46 | } 47 | 48 | public WechatPayHttpClientBuilder withCredentials(Credentials credentials) { 49 | this.credentials = credentials; 50 | return this; 51 | } 52 | 53 | public WechatPayHttpClientBuilder withWechatPay(List certificates) { 54 | this.validator = new WechatPay2Validator(new CertificatesVerifier(certificates)); 55 | return this; 56 | } 57 | 58 | public WechatPayHttpClientBuilder withWechatPay(String publicKeyId, PublicKey publicKey) { 59 | this.validator = new WechatPay2Validator(new PublicKeyVerifier(publicKeyId, publicKey)); 60 | return this; 61 | } 62 | 63 | public WechatPayHttpClientBuilder withProxy(HttpHost proxy) { 64 | if (proxy != null) { 65 | this.setProxy(proxy); 66 | } 67 | return this; 68 | } 69 | 70 | /** 71 | * Please use {@link #withWechatPay(List)} instead 72 | * 73 | * @param certificates 平台证书list 74 | * @return 具有验证器的builder 75 | */ 76 | @SuppressWarnings("SpellCheckingInspection") 77 | @Deprecated 78 | public WechatPayHttpClientBuilder withWechatpay(List certificates) { 79 | return withWechatPay(certificates); 80 | } 81 | 82 | public WechatPayHttpClientBuilder withValidator(Validator validator) { 83 | this.validator = validator; 84 | return this; 85 | } 86 | 87 | @Override 88 | public CloseableHttpClient build() { 89 | if (credentials == null) { 90 | throw new IllegalArgumentException("缺少身份认证信息"); 91 | } 92 | if (validator == null) { 93 | throw new IllegalArgumentException("缺少签名验证信息"); 94 | } 95 | return super.build(); 96 | } 97 | 98 | @Override 99 | protected ClientExecChain decorateProtocolExec(final ClientExecChain requestExecutor) { 100 | return new SignatureExec(this.credentials, this.validator, requestExecutor); 101 | } 102 | 103 | } 104 | -------------------------------------------------------------------------------- /src/main/java/com/wechat/pay/contrib/apache/httpclient/WechatPayUploadHttpPost.java: -------------------------------------------------------------------------------- 1 | package com.wechat.pay.contrib.apache.httpclient; 2 | 3 | import static org.apache.http.HttpHeaders.ACCEPT; 4 | import static org.apache.http.entity.ContentType.APPLICATION_JSON; 5 | 6 | import java.io.InputStream; 7 | import java.net.URI; 8 | import java.net.URLConnection; 9 | import org.apache.http.HttpEntity; 10 | import org.apache.http.client.methods.HttpPost; 11 | import org.apache.http.entity.ContentType; 12 | import org.apache.http.entity.mime.HttpMultipartMode; 13 | import org.apache.http.entity.mime.MultipartEntityBuilder; 14 | import org.slf4j.Logger; 15 | import org.slf4j.LoggerFactory; 16 | 17 | /** 18 | * @author xy-peng 19 | */ 20 | public class WechatPayUploadHttpPost extends HttpPost { 21 | 22 | private final String meta; 23 | private static final Logger log = LoggerFactory.getLogger(WechatPayUploadHttpPost.class); 24 | 25 | private WechatPayUploadHttpPost(URI uri, String meta) { 26 | super(uri); 27 | this.meta = meta; 28 | } 29 | 30 | public String getMeta() { 31 | return meta; 32 | } 33 | 34 | public static class Builder { 35 | 36 | private final URI uri; 37 | private String fileName; 38 | private InputStream fileInputStream; 39 | private ContentType fileContentType; 40 | private String meta; 41 | 42 | public Builder(URI uri) { 43 | if (uri == null) { 44 | throw new IllegalArgumentException("上传文件接口URL为空"); 45 | } 46 | this.uri = uri; 47 | } 48 | 49 | public Builder withImage(String fileName, String fileSha256, InputStream inputStream) { 50 | if (fileSha256 == null || fileSha256.isEmpty()) { 51 | throw new IllegalArgumentException("文件摘要为空"); 52 | } 53 | meta = String.format("{\"filename\":\"%s\",\"sha256\":\"%s\"}", fileName, fileSha256); 54 | return withFile(fileName, meta, inputStream); 55 | } 56 | 57 | public Builder withFile(String fileName, String meta, InputStream inputStream) { 58 | this.fileName = fileName; 59 | this.fileInputStream = inputStream; 60 | String mimeType = URLConnection.guessContentTypeFromName(fileName); 61 | if (mimeType == null) { 62 | // guess this is a video uploading 63 | this.fileContentType = ContentType.APPLICATION_OCTET_STREAM; 64 | } else { 65 | this.fileContentType = ContentType.create(mimeType); 66 | } 67 | this.meta = meta; 68 | return this; 69 | } 70 | 71 | public WechatPayUploadHttpPost build() { 72 | if (fileName == null || fileName.isEmpty()) { 73 | throw new IllegalArgumentException("文件名称为空"); 74 | } 75 | if (fileInputStream == null) { 76 | throw new IllegalArgumentException("文件为空"); 77 | } 78 | if (fileContentType == null) { 79 | throw new IllegalArgumentException("文件类型为空"); 80 | } 81 | if (meta == null || meta.isEmpty()) { 82 | throw new IllegalArgumentException("媒体文件元信息为空"); 83 | } 84 | WechatPayUploadHttpPost request = new WechatPayUploadHttpPost(uri, meta); 85 | MultipartEntityBuilder entityBuilder = MultipartEntityBuilder.create(); 86 | entityBuilder.setMode(HttpMultipartMode.RFC6532) 87 | .addTextBody("meta", meta, APPLICATION_JSON) 88 | .addBinaryBody("file", fileInputStream, fileContentType, fileName); 89 | HttpEntity entity = entityBuilder.build(); 90 | request.setEntity(entity); 91 | request.addHeader(ACCEPT, APPLICATION_JSON.toString()); 92 | log.debug("request content-type[{}]", entity.getContentType()); 93 | return request; 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/main/java/com/wechat/pay/contrib/apache/httpclient/auth/AutoUpdateCertificatesVerifier.java: -------------------------------------------------------------------------------- 1 | package com.wechat.pay.contrib.apache.httpclient.auth; 2 | 3 | import static org.apache.http.HttpHeaders.ACCEPT; 4 | import static org.apache.http.HttpStatus.SC_OK; 5 | import static org.apache.http.entity.ContentType.APPLICATION_JSON; 6 | 7 | import com.fasterxml.jackson.databind.JsonNode; 8 | import com.fasterxml.jackson.databind.ObjectMapper; 9 | import com.wechat.pay.contrib.apache.httpclient.Credentials; 10 | import com.wechat.pay.contrib.apache.httpclient.Validator; 11 | import com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder; 12 | import com.wechat.pay.contrib.apache.httpclient.util.AesUtil; 13 | import java.io.ByteArrayInputStream; 14 | import java.io.IOException; 15 | import java.nio.charset.StandardCharsets; 16 | import java.security.GeneralSecurityException; 17 | import java.security.PublicKey; 18 | import java.security.cert.CertificateExpiredException; 19 | import java.security.cert.CertificateFactory; 20 | import java.security.cert.CertificateNotYetValidException; 21 | import java.security.cert.X509Certificate; 22 | import java.time.Duration; 23 | import java.time.Instant; 24 | import java.util.ArrayList; 25 | import java.util.List; 26 | import java.util.concurrent.TimeUnit; 27 | import java.util.concurrent.locks.ReentrantLock; 28 | import org.apache.http.client.methods.CloseableHttpResponse; 29 | import org.apache.http.client.methods.HttpGet; 30 | import org.apache.http.impl.client.CloseableHttpClient; 31 | import org.apache.http.util.EntityUtils; 32 | import org.slf4j.Logger; 33 | import org.slf4j.LoggerFactory; 34 | 35 | /** 36 | * 在原有CertificatesVerifier基础上,增加自动更新证书功能 37 | * 该类已废弃,请使用CertificatesManager 38 | * 39 | * @author xy-peng 40 | */ 41 | @Deprecated 42 | public class AutoUpdateCertificatesVerifier implements Verifier { 43 | 44 | protected static final Logger log = LoggerFactory.getLogger(AutoUpdateCertificatesVerifier.class); 45 | /** 46 | * 证书下载地址 47 | */ 48 | private static final String CERT_DOWNLOAD_PATH = "https://api.mch.weixin.qq.com/v3/certificates"; 49 | /** 50 | * 证书更新间隔时间,单位为分钟 51 | */ 52 | protected final long minutesInterval; 53 | protected final Credentials credentials; 54 | protected final byte[] apiV3Key; 55 | protected final ReentrantLock lock = new ReentrantLock(); 56 | /** 57 | * 上次更新时间 58 | */ 59 | protected volatile Instant lastUpdateTime; 60 | protected CertificatesVerifier verifier; 61 | 62 | private static final Validator emptyValidator = 63 | new Validator() { 64 | @Override 65 | public boolean validate(CloseableHttpResponse response) throws IOException { 66 | return true; 67 | }; 68 | 69 | @Override 70 | public String getSerialNumber() { 71 | return ""; 72 | } 73 | }; 74 | 75 | public AutoUpdateCertificatesVerifier(Credentials credentials, byte[] apiV3Key) { 76 | this(credentials, apiV3Key, TimeUnit.HOURS.toMinutes(1)); 77 | } 78 | 79 | public AutoUpdateCertificatesVerifier(Credentials credentials, byte[] apiV3Key, long minutesInterval) { 80 | this.credentials = credentials; 81 | this.apiV3Key = apiV3Key; 82 | this.minutesInterval = minutesInterval; 83 | //构造时更新证书 84 | try { 85 | autoUpdateCert(); 86 | lastUpdateTime = Instant.now(); 87 | } catch (IOException | GeneralSecurityException e) { 88 | throw new RuntimeException(e); 89 | } 90 | } 91 | 92 | @Override 93 | public boolean verify(String serialNumber, byte[] message, String signature) { 94 | if (lastUpdateTime == null 95 | || Duration.between(lastUpdateTime, Instant.now()).toMinutes() >= minutesInterval) { 96 | if (lock.tryLock()) { 97 | try { 98 | autoUpdateCert(); 99 | //更新时间 100 | lastUpdateTime = Instant.now(); 101 | } catch (GeneralSecurityException | IOException e) { 102 | log.warn("Auto update cert failed: ", e); 103 | } finally { 104 | lock.unlock(); 105 | } 106 | } 107 | } 108 | return verifier.verify(serialNumber, message, signature); 109 | } 110 | 111 | @Override 112 | public PublicKey getValidPublicKey() { 113 | return verifier.getValidPublicKey(); 114 | } 115 | 116 | @Override 117 | public String getSerialNumber() { 118 | return verifier.getSerialNumber(); 119 | } 120 | 121 | protected void autoUpdateCert() throws IOException, GeneralSecurityException { 122 | try (CloseableHttpClient httpClient = WechatPayHttpClientBuilder.create() 123 | .withCredentials(credentials) 124 | .withValidator(verifier == null ? emptyValidator : new WechatPay2Validator(verifier)) 125 | .build()) { 126 | 127 | HttpGet httpGet = new HttpGet(CERT_DOWNLOAD_PATH); 128 | httpGet.addHeader(ACCEPT, APPLICATION_JSON.toString()); 129 | 130 | try (CloseableHttpResponse response = httpClient.execute(httpGet)) { 131 | int statusCode = response.getStatusLine().getStatusCode(); 132 | String body = EntityUtils.toString(response.getEntity()); 133 | if (statusCode == SC_OK) { 134 | List newCertList = deserializeToCerts(apiV3Key, body); 135 | if (newCertList.isEmpty()) { 136 | log.warn("Cert list is empty"); 137 | return; 138 | } 139 | this.verifier = new CertificatesVerifier(newCertList); 140 | } else { 141 | log.warn("Auto update cert failed, statusCode = {}, body = {}", statusCode, body); 142 | } 143 | } 144 | } 145 | } 146 | 147 | protected List deserializeToCerts(byte[] apiV3Key, String body) 148 | throws GeneralSecurityException, IOException { 149 | AesUtil aesUtil = new AesUtil(apiV3Key); 150 | ObjectMapper mapper = new ObjectMapper(); 151 | JsonNode dataNode = mapper.readTree(body).get("data"); 152 | List newCertList = new ArrayList<>(); 153 | if (dataNode != null) { 154 | for (int i = 0, count = dataNode.size(); i < count; i++) { 155 | JsonNode node = dataNode.get(i).get("encrypt_certificate"); 156 | //解密 157 | String cert = aesUtil.decryptToString( 158 | node.get("associated_data").toString().replace("\"", "") 159 | .getBytes(StandardCharsets.UTF_8), 160 | node.get("nonce").toString().replace("\"", "") 161 | .getBytes(StandardCharsets.UTF_8), 162 | node.get("ciphertext").toString().replace("\"", "")); 163 | 164 | CertificateFactory cf = CertificateFactory.getInstance("X509"); 165 | X509Certificate x509Cert = (X509Certificate) cf.generateCertificate( 166 | new ByteArrayInputStream(cert.getBytes(StandardCharsets.UTF_8)) 167 | ); 168 | try { 169 | x509Cert.checkValidity(); 170 | } catch (CertificateExpiredException | CertificateNotYetValidException e) { 171 | continue; 172 | } 173 | newCertList.add(x509Cert); 174 | } 175 | } 176 | return newCertList; 177 | } 178 | 179 | } 180 | -------------------------------------------------------------------------------- /src/main/java/com/wechat/pay/contrib/apache/httpclient/auth/CertificatesVerifier.java: -------------------------------------------------------------------------------- 1 | package com.wechat.pay.contrib.apache.httpclient.auth; 2 | 3 | import java.math.BigInteger; 4 | import java.security.InvalidKeyException; 5 | import java.security.NoSuchAlgorithmException; 6 | import java.security.PublicKey; 7 | import java.security.Signature; 8 | import java.security.SignatureException; 9 | import java.security.cert.CertificateExpiredException; 10 | import java.security.cert.CertificateNotYetValidException; 11 | import java.security.cert.X509Certificate; 12 | import java.util.Base64; 13 | import java.util.HashMap; 14 | import java.util.List; 15 | import java.util.Map; 16 | import java.util.NoSuchElementException; 17 | import org.slf4j.Logger; 18 | import org.slf4j.LoggerFactory; 19 | 20 | /** 21 | * @author xy-peng 22 | */ 23 | public class CertificatesVerifier implements Verifier { 24 | 25 | private static final Logger log = LoggerFactory.getLogger(CertificatesVerifier.class); 26 | protected final HashMap certificates = new HashMap<>(); 27 | 28 | public CertificatesVerifier(List list) { 29 | for (X509Certificate item : list) { 30 | certificates.put(item.getSerialNumber(), item); 31 | } 32 | } 33 | 34 | public CertificatesVerifier(Map certificates) { 35 | this.certificates.putAll(certificates); 36 | } 37 | 38 | 39 | public void updateCertificates(Map certificates) { 40 | this.certificates.clear(); 41 | this.certificates.putAll(certificates); 42 | } 43 | 44 | protected boolean verify(X509Certificate certificate, byte[] message, String signature) { 45 | try { 46 | Signature sign = Signature.getInstance("SHA256withRSA"); 47 | sign.initVerify(certificate); 48 | sign.update(message); 49 | return sign.verify(Base64.getDecoder().decode(signature)); 50 | 51 | } catch (NoSuchAlgorithmException e) { 52 | throw new RuntimeException("当前Java环境不支持SHA256withRSA", e); 53 | } catch (SignatureException e) { 54 | throw new RuntimeException("签名验证过程发生了错误", e); 55 | } catch (InvalidKeyException e) { 56 | throw new RuntimeException("无效的证书", e); 57 | } 58 | } 59 | 60 | @Override 61 | public boolean verify(String serialNumber, byte[] message, String signature) { 62 | BigInteger val = new BigInteger(serialNumber, 16); 63 | X509Certificate cert = certificates.get(val); 64 | if (cert == null) { 65 | log.error("找不到证书序列号对应的证书,序列号:{}", serialNumber); 66 | return false; 67 | } 68 | return verify(cert, message, signature); 69 | } 70 | 71 | public X509Certificate getValidCertificate() { 72 | X509Certificate latestCert = null; 73 | for (X509Certificate x509Cert : certificates.values()) { 74 | // 若latestCert为空或x509Cert的证书有效开始时间在latestCert之后,则更新latestCert 75 | if (latestCert == null || x509Cert.getNotBefore().after(latestCert.getNotBefore())) { 76 | latestCert = x509Cert; 77 | } 78 | } 79 | try { 80 | latestCert.checkValidity(); 81 | return latestCert; 82 | } catch (CertificateExpiredException | CertificateNotYetValidException e) { 83 | throw new NoSuchElementException("没有有效的微信支付平台证书"); 84 | } 85 | } 86 | 87 | 88 | @Override 89 | public PublicKey getValidPublicKey() { 90 | return getValidCertificate().getPublicKey(); 91 | } 92 | 93 | @Override 94 | public String getSerialNumber() { 95 | return getValidCertificate().getSerialNumber().toString(16).toUpperCase(); 96 | } 97 | } 98 | 99 | -------------------------------------------------------------------------------- /src/main/java/com/wechat/pay/contrib/apache/httpclient/auth/MixVerifier.java: -------------------------------------------------------------------------------- 1 | package com.wechat.pay.contrib.apache.httpclient.auth; 2 | 3 | 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | 7 | import java.security.PublicKey; 8 | import java.util.Objects; 9 | 10 | 11 | /** 12 | * MixVerifier 混合Verifier,仅用于切换平台证书与微信支付公钥时提供兼容 13 | * 14 | * 本实例需要使用一个 PublicKeyVerifier + 一个 Verifier 初始化,前者提供微信支付公钥验签,后者提供平台证书验签 15 | */ 16 | public class MixVerifier implements Verifier { 17 | private static final Logger log = LoggerFactory.getLogger(MixVerifier.class); 18 | 19 | final PublicKeyVerifier publicKeyVerifier; 20 | final Verifier certificateVerifier; 21 | 22 | public MixVerifier(PublicKeyVerifier publicKeyVerifier, Verifier certificateVerifier) { 23 | if (publicKeyVerifier == null) { 24 | throw new IllegalArgumentException("publicKeyVerifier cannot be null"); 25 | } 26 | 27 | this.publicKeyVerifier = publicKeyVerifier; 28 | this.certificateVerifier = certificateVerifier; 29 | } 30 | 31 | @Override 32 | public boolean verify(String serialNumber, byte[] message, String signature) { 33 | if (Objects.equals(publicKeyVerifier.getSerialNumber(), serialNumber)) { 34 | return publicKeyVerifier.verify(serialNumber, message, signature); 35 | } 36 | 37 | if (certificateVerifier != null) { 38 | return certificateVerifier.verify(serialNumber, message, signature); 39 | } 40 | 41 | log.error("找不到证书序列号对应的证书,序列号:{}", serialNumber); 42 | return false; 43 | } 44 | 45 | @Override 46 | public PublicKey getValidPublicKey() { 47 | return publicKeyVerifier.getValidPublicKey(); 48 | } 49 | 50 | @Override 51 | public String getSerialNumber() { 52 | return publicKeyVerifier.getSerialNumber(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/com/wechat/pay/contrib/apache/httpclient/auth/PrivateKeySigner.java: -------------------------------------------------------------------------------- 1 | package com.wechat.pay.contrib.apache.httpclient.auth; 2 | 3 | import java.security.InvalidKeyException; 4 | import java.security.NoSuchAlgorithmException; 5 | import java.security.PrivateKey; 6 | import java.security.Signature; 7 | import java.security.SignatureException; 8 | import java.util.Base64; 9 | 10 | /** 11 | * @author xy-peng 12 | */ 13 | public class PrivateKeySigner implements Signer { 14 | 15 | protected final String certificateSerialNumber; 16 | protected final PrivateKey privateKey; 17 | 18 | public PrivateKeySigner(String serialNumber, PrivateKey privateKey) { 19 | this.certificateSerialNumber = serialNumber; 20 | this.privateKey = privateKey; 21 | } 22 | 23 | @Override 24 | public SignatureResult sign(byte[] message) { 25 | try { 26 | Signature sign = Signature.getInstance("SHA256withRSA"); 27 | sign.initSign(privateKey); 28 | sign.update(message); 29 | return new SignatureResult(Base64.getEncoder().encodeToString(sign.sign()), certificateSerialNumber); 30 | 31 | } catch (NoSuchAlgorithmException e) { 32 | throw new RuntimeException("当前Java环境不支持SHA256withRSA", e); 33 | } catch (SignatureException e) { 34 | throw new RuntimeException("签名计算失败", e); 35 | } catch (InvalidKeyException e) { 36 | throw new RuntimeException("无效的私钥", e); 37 | } 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/wechat/pay/contrib/apache/httpclient/auth/PublicKeyVerifier.java: -------------------------------------------------------------------------------- 1 | package com.wechat.pay.contrib.apache.httpclient.auth; 2 | 3 | import java.security.InvalidKeyException; 4 | import java.security.NoSuchAlgorithmException; 5 | import java.security.PublicKey; 6 | import java.security.Signature; 7 | import java.security.SignatureException; 8 | import java.util.Base64; 9 | 10 | public class PublicKeyVerifier implements Verifier { 11 | protected final PublicKey publicKey; 12 | protected final String publicKeyId; 13 | 14 | public PublicKeyVerifier(String publicKeyId, PublicKey publicKey) { 15 | this.publicKey = publicKey; 16 | this.publicKeyId = publicKeyId; 17 | } 18 | 19 | @Override 20 | public boolean verify(String serialNumber, byte[] message, String signature) { 21 | try { 22 | Signature sign = Signature.getInstance("SHA256withRSA"); 23 | sign.initVerify(publicKey); 24 | sign.update(message); 25 | return sign.verify(Base64.getDecoder().decode(signature)); 26 | } catch (NoSuchAlgorithmException e) { 27 | throw new RuntimeException("当前Java环境不支持SHA256withRSA", e); 28 | } catch (SignatureException e) { 29 | throw new RuntimeException("签名验证过程发生了错误", e); 30 | } catch (InvalidKeyException e) { 31 | throw new RuntimeException("无效的证书", e); 32 | } 33 | } 34 | 35 | @Override 36 | public PublicKey getValidPublicKey() { 37 | return publicKey; 38 | } 39 | 40 | @Override 41 | public String getSerialNumber() { 42 | return publicKeyId; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/com/wechat/pay/contrib/apache/httpclient/auth/Signer.java: -------------------------------------------------------------------------------- 1 | package com.wechat.pay.contrib.apache.httpclient.auth; 2 | 3 | /** 4 | * @author xy-peng 5 | */ 6 | public interface Signer { 7 | 8 | SignatureResult sign(byte[] message); 9 | 10 | class SignatureResult { 11 | 12 | protected final String sign; 13 | protected final String certificateSerialNumber; 14 | 15 | public String getSign() { 16 | return sign; 17 | } 18 | 19 | public String getCertificateSerialNumber() { 20 | return certificateSerialNumber; 21 | } 22 | 23 | public SignatureResult(String sign, String serialNumber) { 24 | this.sign = sign; 25 | this.certificateSerialNumber = serialNumber; 26 | } 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/wechat/pay/contrib/apache/httpclient/auth/Verifier.java: -------------------------------------------------------------------------------- 1 | package com.wechat.pay.contrib.apache.httpclient.auth; 2 | 3 | import java.security.PublicKey; 4 | 5 | /** 6 | * @author xy-peng 7 | */ 8 | public interface Verifier { 9 | /** 10 | * @param serialNumber 微信支付序列号(微信支付公钥ID 或 平台证书序列号) 11 | * @param message 验签的原文 12 | * @param signature 验签的签名 13 | * @return 验证是否通过 14 | */ 15 | boolean verify(String serialNumber, byte[] message, String signature); 16 | 17 | /** 18 | * 获取合法的公钥,针对不同的验签模式有所区别 19 | *
    20 | *
  • 如果是微信支付公钥验签:则为微信支付公钥
  • 21 | *
  • 如果是平台证书验签:则为平台证书内部的公钥
  • 22 | *
23 | * @return 合法公钥 24 | */ 25 | PublicKey getValidPublicKey(); 26 | 27 | 28 | /** 29 | * 获取微信支付序列号,针对不同的验签模式有所区别: 30 | *
    31 | *
  • 如果是微信支付公钥验签:则为公钥ID,以 PUB_KEY_ID_ 开头
  • 32 | *
  • 如果是平台证书验签:则为平台证书序列号
  • 33 | *
34 | * @return 微信支付序列号 35 | */ 36 | String getSerialNumber(); 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/wechat/pay/contrib/apache/httpclient/auth/WechatPay2Credentials.java: -------------------------------------------------------------------------------- 1 | package com.wechat.pay.contrib.apache.httpclient.auth; 2 | 3 | import com.wechat.pay.contrib.apache.httpclient.Credentials; 4 | import com.wechat.pay.contrib.apache.httpclient.WechatPayUploadHttpPost; 5 | import java.io.IOException; 6 | import java.net.URI; 7 | import java.nio.charset.StandardCharsets; 8 | import java.security.SecureRandom; 9 | import org.apache.http.HttpEntityEnclosingRequest; 10 | import org.apache.http.client.methods.HttpRequestWrapper; 11 | import org.apache.http.util.EntityUtils; 12 | import org.slf4j.Logger; 13 | import org.slf4j.LoggerFactory; 14 | 15 | /** 16 | * @author xy-peng 17 | */ 18 | public class WechatPay2Credentials implements Credentials { 19 | 20 | protected static final Logger log = LoggerFactory.getLogger(WechatPay2Credentials.class); 21 | 22 | protected static final String SYMBOLS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; 23 | protected static final SecureRandom RANDOM = new SecureRandom(); 24 | protected final String merchantId; 25 | protected final Signer signer; 26 | 27 | public WechatPay2Credentials(String merchantId, Signer signer) { 28 | this.merchantId = merchantId; 29 | this.signer = signer; 30 | } 31 | 32 | public String getMerchantId() { 33 | return merchantId; 34 | } 35 | 36 | protected long generateTimestamp() { 37 | return System.currentTimeMillis() / 1000; 38 | } 39 | 40 | protected String generateNonceStr() { 41 | char[] nonceChars = new char[32]; 42 | for (int index = 0; index < nonceChars.length; ++index) { 43 | nonceChars[index] = SYMBOLS.charAt(RANDOM.nextInt(SYMBOLS.length())); 44 | } 45 | return new String(nonceChars); 46 | } 47 | 48 | @Override 49 | public final String getSchema() { 50 | return "WECHATPAY2-SHA256-RSA2048"; 51 | } 52 | 53 | @Override 54 | public final String getToken(HttpRequestWrapper request) throws IOException { 55 | String nonceStr = generateNonceStr(); 56 | long timestamp = generateTimestamp(); 57 | 58 | String message = buildMessage(nonceStr, timestamp, request); 59 | log.debug("authorization message=[{}]", message); 60 | 61 | Signer.SignatureResult signature = signer.sign(message.getBytes(StandardCharsets.UTF_8)); 62 | 63 | String token = "mchid=\"" + getMerchantId() + "\"," 64 | + "nonce_str=\"" + nonceStr + "\"," 65 | + "timestamp=\"" + timestamp + "\"," 66 | + "serial_no=\"" + signature.certificateSerialNumber + "\"," 67 | + "signature=\"" + signature.sign + "\""; 68 | log.debug("authorization token=[{}]", token); 69 | 70 | return token; 71 | } 72 | 73 | protected String buildMessage(String nonce, long timestamp, HttpRequestWrapper request) throws IOException { 74 | URI uri = request.getURI(); 75 | String canonicalUrl = uri.getRawPath(); 76 | if (uri.getQuery() != null) { 77 | canonicalUrl += "?" + uri.getRawQuery(); 78 | } 79 | 80 | String body = ""; 81 | // PATCH,POST,PUT 82 | if (request.getOriginal() instanceof WechatPayUploadHttpPost) { 83 | body = ((WechatPayUploadHttpPost) request.getOriginal()).getMeta(); 84 | } else if (request instanceof HttpEntityEnclosingRequest) { 85 | body = EntityUtils.toString(((HttpEntityEnclosingRequest) request).getEntity(), StandardCharsets.UTF_8); 86 | } 87 | 88 | return request.getRequestLine().getMethod() + "\n" 89 | + canonicalUrl + "\n" 90 | + timestamp + "\n" 91 | + nonce + "\n" 92 | + body + "\n"; 93 | } 94 | 95 | } 96 | -------------------------------------------------------------------------------- /src/main/java/com/wechat/pay/contrib/apache/httpclient/auth/WechatPay2Validator.java: -------------------------------------------------------------------------------- 1 | package com.wechat.pay.contrib.apache.httpclient.auth; 2 | 3 | 4 | import static com.wechat.pay.contrib.apache.httpclient.constant.WechatPayHttpHeaders.REQUEST_ID; 5 | import static com.wechat.pay.contrib.apache.httpclient.constant.WechatPayHttpHeaders.WECHAT_PAY_NONCE; 6 | import static com.wechat.pay.contrib.apache.httpclient.constant.WechatPayHttpHeaders.WECHAT_PAY_SERIAL; 7 | import static com.wechat.pay.contrib.apache.httpclient.constant.WechatPayHttpHeaders.WECHAT_PAY_SIGNATURE; 8 | import static com.wechat.pay.contrib.apache.httpclient.constant.WechatPayHttpHeaders.WECHAT_PAY_TIMESTAMP; 9 | 10 | import com.wechat.pay.contrib.apache.httpclient.Validator; 11 | import java.io.IOException; 12 | import java.nio.charset.StandardCharsets; 13 | import java.time.DateTimeException; 14 | import java.time.Duration; 15 | import java.time.Instant; 16 | import org.apache.http.Header; 17 | import org.apache.http.HttpEntity; 18 | import org.apache.http.client.methods.CloseableHttpResponse; 19 | import org.apache.http.util.EntityUtils; 20 | import org.slf4j.Logger; 21 | import org.slf4j.LoggerFactory; 22 | 23 | /** 24 | * @author xy-peng 25 | */ 26 | public class WechatPay2Validator implements Validator { 27 | 28 | protected static final Logger log = LoggerFactory.getLogger(WechatPay2Validator.class); 29 | /** 30 | * 应答超时时间,单位为分钟 31 | */ 32 | protected static final long RESPONSE_EXPIRED_MINUTES = 5; 33 | protected final Verifier verifier; 34 | 35 | public WechatPay2Validator(Verifier verifier) { 36 | this.verifier = verifier; 37 | } 38 | 39 | protected static IllegalArgumentException parameterError(String message, Object... args) { 40 | message = String.format(message, args); 41 | return new IllegalArgumentException("parameter error: " + message); 42 | } 43 | 44 | protected static IllegalArgumentException verifyFail(String message, Object... args) { 45 | message = String.format(message, args); 46 | return new IllegalArgumentException("signature verify fail: " + message); 47 | } 48 | 49 | @Override 50 | public final boolean validate(CloseableHttpResponse response) throws IOException { 51 | try { 52 | validateParameters(response); 53 | 54 | String message = buildMessage(response); 55 | String serial = response.getFirstHeader(WECHAT_PAY_SERIAL).getValue(); 56 | String signature = response.getFirstHeader(WECHAT_PAY_SIGNATURE).getValue(); 57 | 58 | if (!verifier.verify(serial, message.getBytes(StandardCharsets.UTF_8), signature)) { 59 | throw verifyFail("serial=[%s] message=[%s] sign=[%s], request-id=[%s]", 60 | serial, message, signature, response.getFirstHeader(REQUEST_ID).getValue()); 61 | } 62 | } catch (IllegalArgumentException e) { 63 | log.warn(e.getMessage()); 64 | return false; 65 | } 66 | 67 | return true; 68 | } 69 | 70 | @Override 71 | public final String getSerialNumber() { 72 | return verifier.getSerialNumber(); 73 | } 74 | 75 | protected final void validateParameters(CloseableHttpResponse response) { 76 | Header firstHeader = response.getFirstHeader(REQUEST_ID); 77 | if (firstHeader == null) { 78 | throw parameterError("empty " + REQUEST_ID); 79 | } 80 | String requestId = firstHeader.getValue(); 81 | 82 | // NOTE: ensure HEADER_WECHAT_PAY_TIMESTAMP at last 83 | String[] headers = {WECHAT_PAY_SERIAL, WECHAT_PAY_SIGNATURE, WECHAT_PAY_NONCE, WECHAT_PAY_TIMESTAMP}; 84 | 85 | Header header = null; 86 | for (String headerName : headers) { 87 | header = response.getFirstHeader(headerName); 88 | if (header == null) { 89 | throw parameterError("empty [%s], request-id=[%s]", headerName, requestId); 90 | } 91 | } 92 | 93 | String timestampStr = header.getValue(); 94 | try { 95 | Instant responseTime = Instant.ofEpochSecond(Long.parseLong(timestampStr)); 96 | // 拒绝过期应答 97 | if (Duration.between(responseTime, Instant.now()).abs().toMinutes() >= RESPONSE_EXPIRED_MINUTES) { 98 | throw parameterError("timestamp=[%s] expires, request-id=[%s]", timestampStr, requestId); 99 | } 100 | } catch (DateTimeException | NumberFormatException e) { 101 | throw parameterError("invalid timestamp=[%s], request-id=[%s]", timestampStr, requestId); 102 | } 103 | } 104 | 105 | protected final String buildMessage(CloseableHttpResponse response) throws IOException { 106 | String timestamp = response.getFirstHeader(WECHAT_PAY_TIMESTAMP).getValue(); 107 | String nonce = response.getFirstHeader(WECHAT_PAY_NONCE).getValue(); 108 | String body = getResponseBody(response); 109 | return timestamp + "\n" 110 | + nonce + "\n" 111 | + body + "\n"; 112 | } 113 | 114 | protected final String getResponseBody(CloseableHttpResponse response) throws IOException { 115 | HttpEntity entity = response.getEntity(); 116 | return (entity != null && entity.isRepeatable()) ? EntityUtils.toString(entity) : ""; 117 | } 118 | 119 | } 120 | -------------------------------------------------------------------------------- /src/main/java/com/wechat/pay/contrib/apache/httpclient/cert/CertificatesManager.java: -------------------------------------------------------------------------------- 1 | package com.wechat.pay.contrib.apache.httpclient.cert; 2 | 3 | import static org.apache.http.HttpHeaders.ACCEPT; 4 | import static org.apache.http.HttpStatus.SC_OK; 5 | import static org.apache.http.entity.ContentType.APPLICATION_JSON; 6 | 7 | import com.wechat.pay.contrib.apache.httpclient.Credentials; 8 | import com.wechat.pay.contrib.apache.httpclient.Validator; 9 | import com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder; 10 | import com.wechat.pay.contrib.apache.httpclient.auth.Verifier; 11 | import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Validator; 12 | import com.wechat.pay.contrib.apache.httpclient.exception.HttpCodeException; 13 | import com.wechat.pay.contrib.apache.httpclient.exception.NotFoundException; 14 | import com.wechat.pay.contrib.apache.httpclient.proxy.HttpProxyFactory; 15 | import com.wechat.pay.contrib.apache.httpclient.util.CertSerializeUtil; 16 | import java.io.IOException; 17 | import java.math.BigInteger; 18 | import java.security.GeneralSecurityException; 19 | import java.security.InvalidKeyException; 20 | import java.security.NoSuchAlgorithmException; 21 | import java.security.PublicKey; 22 | import java.security.Signature; 23 | import java.security.SignatureException; 24 | import java.security.cert.CertificateExpiredException; 25 | import java.security.cert.CertificateNotYetValidException; 26 | import java.security.cert.X509Certificate; 27 | import java.time.Instant; 28 | import java.util.Base64; 29 | import java.util.Map; 30 | import java.util.NoSuchElementException; 31 | import java.util.Objects; 32 | import java.util.concurrent.ConcurrentHashMap; 33 | import java.util.concurrent.ScheduledExecutorService; 34 | import java.util.concurrent.TimeUnit; 35 | import org.apache.http.HttpHost; 36 | import org.apache.http.client.methods.CloseableHttpResponse; 37 | import org.apache.http.client.methods.HttpGet; 38 | import org.apache.http.impl.client.CloseableHttpClient; 39 | import org.apache.http.util.EntityUtils; 40 | import org.slf4j.Logger; 41 | import org.slf4j.LoggerFactory; 42 | 43 | /** 44 | * 平台证书管理器,定时更新证书(默认值为UPDATE_INTERVAL_MINUTE) 45 | * 46 | * @author lianup 47 | * @since 0.3.0 48 | */ 49 | public class CertificatesManager { 50 | 51 | protected static final int UPDATE_INTERVAL_MINUTE = 1440; 52 | private static final Logger log = LoggerFactory.getLogger(CertificatesManager.class); 53 | /** 54 | * 证书下载地址 55 | */ 56 | private static final String CERT_DOWNLOAD_PATH = "https://api.mch.weixin.qq.com/v3/certificates"; 57 | private static final String SCHEDULE_UPDATE_CERT_THREAD_NAME = "scheduled_update_cert_thread"; 58 | private volatile static CertificatesManager instance = null; 59 | private ConcurrentHashMap apiV3Keys = new ConcurrentHashMap<>(); 60 | 61 | private HttpProxyFactory proxyFactory; 62 | private HttpHost proxy; 63 | 64 | private ConcurrentHashMap> certificates = 65 | new ConcurrentHashMap<>(); 66 | 67 | private ConcurrentHashMap credentialsMap = new ConcurrentHashMap<>(); 68 | /** 69 | * 执行定时更新平台证书的线程池 70 | */ 71 | private ScheduledExecutorService executor; 72 | 73 | private static final Validator emptyValidator = 74 | new Validator() { 75 | @Override 76 | public boolean validate(CloseableHttpResponse response) throws IOException { 77 | return true; 78 | }; 79 | 80 | @Override 81 | public String getSerialNumber() { 82 | return ""; 83 | } 84 | }; 85 | 86 | private CertificatesManager() { 87 | } 88 | 89 | public static CertificatesManager getInstance() { 90 | if (instance == null) { 91 | synchronized (CertificatesManager.class) { 92 | if (instance == null) { 93 | instance = new CertificatesManager(); 94 | } 95 | } 96 | } 97 | return instance; 98 | } 99 | 100 | /** 101 | * 增加需要自动更新平台证书的商户信息 102 | * 103 | * @param merchantId 商户号 104 | * @param credentials 认证器 105 | * @param apiV3Key APIv3密钥 106 | * @throws IOException IO错误 107 | * @throws GeneralSecurityException 通用安全错误 108 | * @throws HttpCodeException HttpCode错误 109 | */ 110 | public synchronized void putMerchant(String merchantId, Credentials credentials, byte[] apiV3Key) 111 | throws IOException, GeneralSecurityException, HttpCodeException { 112 | if (merchantId == null || merchantId.isEmpty()) { 113 | throw new IllegalArgumentException("merchantId为空"); 114 | } 115 | if (credentials == null) { 116 | throw new IllegalArgumentException("credentials为空"); 117 | } 118 | if (apiV3Key.length == 0) { 119 | throw new IllegalArgumentException("apiV3Key为空"); 120 | } 121 | // 添加或更新商户信息 122 | if (certificates.get(merchantId) == null) { 123 | certificates.put(merchantId, new ConcurrentHashMap<>()); 124 | } 125 | initCertificates(merchantId, credentials, apiV3Key); 126 | credentialsMap.put(merchantId, credentials); 127 | apiV3Keys.put(merchantId, apiV3Key); 128 | // 若没有executor,则启动定时更新证书任务 129 | if (executor == null) { 130 | beginScheduleUpdate(); 131 | } 132 | } 133 | 134 | /*** 135 | * 代理配置 136 | * 137 | * @param proxy 代理host 138 | **/ 139 | public synchronized void setProxy(HttpHost proxy) { 140 | this.proxy = proxy; 141 | } 142 | 143 | /** 144 | * 设置代理工厂 145 | * 146 | * @param proxyFactory 代理工厂 147 | */ 148 | public synchronized void setProxyFactory(HttpProxyFactory proxyFactory) { 149 | this.proxyFactory = proxyFactory; 150 | } 151 | 152 | public synchronized HttpHost resolveProxy() { 153 | return Objects.nonNull(proxyFactory) ? proxyFactory.buildHttpProxy() : proxy; 154 | } 155 | 156 | /** 157 | * 停止自动更新平台证书,停止后无法再重新启动 158 | */ 159 | public void stop() { 160 | if (executor != null) { 161 | try { 162 | executor.shutdownNow(); 163 | } catch (Exception e) { 164 | log.error("Executor shutdown now failed", e); 165 | } 166 | } 167 | } 168 | 169 | private X509Certificate getLatestCertificate(String merchantId) 170 | throws NotFoundException { 171 | if (merchantId == null || merchantId.isEmpty()) { 172 | throw new IllegalArgumentException("merchantId为空"); 173 | } 174 | Map merchantCertificates = certificates.get(merchantId); 175 | if (merchantCertificates == null || merchantCertificates.isEmpty()) { 176 | throw new NotFoundException("没有最新的平台证书,merchantId:" + merchantId); 177 | } 178 | X509Certificate latestCert = null; 179 | for (X509Certificate x509Cert : merchantCertificates.values()) { 180 | // 若latestCert为空或x509Cert的证书有效开始时间在latestCert之后,则更新latestCert 181 | if (latestCert == null || x509Cert.getNotBefore().after(latestCert.getNotBefore())) { 182 | latestCert = x509Cert; 183 | } 184 | } 185 | try { 186 | latestCert.checkValidity(); 187 | return latestCert; 188 | } catch (CertificateExpiredException | CertificateNotYetValidException e) { 189 | log.error("平台证书未生效或已过期,merchantId:{}", merchantId); 190 | } 191 | throw new NotFoundException("没有最新的平台证书,merchantId:" + merchantId); 192 | } 193 | 194 | /** 195 | * 获取商户号为merchantId的验签器 196 | * 197 | * @param merchantId 商户号 198 | * @return 验签器 199 | * @throws NotFoundException merchantId/merchantCertificates/apiV3Key/credentials为空 200 | */ 201 | public Verifier getVerifier(String merchantId) throws NotFoundException { 202 | // 若商户信息不存在,返回错误 203 | ConcurrentHashMap merchantCertificates = certificates.get(merchantId); 204 | byte[] apiV3Key = apiV3Keys.get(merchantId); 205 | Credentials credentials = credentialsMap.get(merchantId); 206 | if (merchantId == null || merchantId.isEmpty()) { 207 | throw new IllegalArgumentException("merchantId为空"); 208 | } 209 | if (merchantCertificates == null || merchantCertificates.size() == 0) { 210 | throw new NotFoundException("平台证书为空,merchantId:" + merchantId); 211 | } 212 | if (apiV3Key.length == 0) { 213 | throw new NotFoundException("apiV3Key为空,merchantId:" + merchantId); 214 | 215 | } 216 | if (credentials == null) { 217 | throw new NotFoundException("credentials为空,merchantId:" + merchantId); 218 | } 219 | return new DefaultVerifier(merchantId); 220 | } 221 | 222 | private void beginScheduleUpdate() { 223 | executor = new SafeSingleScheduleExecutor(); 224 | Runnable runnable = () -> { 225 | try { 226 | Thread.currentThread().setName(SCHEDULE_UPDATE_CERT_THREAD_NAME); 227 | log.info("Begin update Certificates.Date:{}", Instant.now()); 228 | updateCertificates(); 229 | log.info("Finish update Certificates.Date:{}", Instant.now()); 230 | } catch (Throwable t) { 231 | log.error("Update Certificates failed", t); 232 | } 233 | }; 234 | executor.scheduleAtFixedRate(runnable, 0, UPDATE_INTERVAL_MINUTE, TimeUnit.MINUTES); 235 | } 236 | 237 | /** 238 | * 下载和更新平台证书 239 | * 240 | * @param merchantId 商户号 241 | * @param verifier 验签器 242 | * @param credentials 认证器 243 | * @param apiV3Key apiv3密钥 244 | * @throws HttpCodeException Http返回码异常 245 | * @throws IOException IO异常 246 | * @throws GeneralSecurityException 通用安全性异常 247 | */ 248 | private synchronized void downloadAndUpdateCert(String merchantId, Verifier verifier, Credentials credentials, 249 | byte[] apiV3Key) throws HttpCodeException, IOException, GeneralSecurityException { 250 | proxy = resolveProxy(); 251 | try (CloseableHttpClient httpClient = WechatPayHttpClientBuilder.create() 252 | .withCredentials(credentials) 253 | .withValidator(verifier == null ? emptyValidator : new WechatPay2Validator(verifier)) 254 | .withProxy(proxy) 255 | .build()) { 256 | HttpGet httpGet = new HttpGet(CERT_DOWNLOAD_PATH); 257 | httpGet.addHeader(ACCEPT, APPLICATION_JSON.toString()); 258 | try (CloseableHttpResponse response = httpClient.execute(httpGet)) { 259 | int statusCode = response.getStatusLine().getStatusCode(); 260 | String body = EntityUtils.toString(response.getEntity()); 261 | if (statusCode == SC_OK) { 262 | Map newCertList = CertSerializeUtil.deserializeToCerts(apiV3Key, body); 263 | if (newCertList.isEmpty()) { 264 | log.warn("Cert list is empty"); 265 | return; 266 | } 267 | ConcurrentHashMap merchantCertificates = certificates.get(merchantId); 268 | merchantCertificates.clear(); 269 | merchantCertificates.putAll(newCertList); 270 | } else { 271 | log.error("Auto update cert failed, statusCode = {}, body = {}", statusCode, body); 272 | throw new HttpCodeException("下载平台证书返回状态码异常,状态码为:" + statusCode); 273 | } 274 | } 275 | } 276 | } 277 | 278 | /** 279 | * 初始化平台证书,商户信息第一次被添加时调用 280 | * 281 | * @param merchantId 商户号 282 | * @param credentials 认证器 283 | * @param apiV3Key apiv3密钥 284 | * @throws HttpCodeException Http返回码异常 285 | * @throws IOException IO异常 286 | * @throws GeneralSecurityException 通用安全性异常 287 | */ 288 | private void initCertificates(String merchantId, Credentials credentials, byte[] apiV3Key) 289 | throws HttpCodeException, IOException, GeneralSecurityException { 290 | downloadAndUpdateCert(merchantId, null, credentials, apiV3Key); 291 | } 292 | 293 | /** 294 | * 更新平台证书,每UPDATE_INTERVAL_MINUTE调用一次 295 | */ 296 | private void updateCertificates() { 297 | for (Map.Entry entry : credentialsMap.entrySet()) { 298 | String merchantId = entry.getKey(); 299 | Credentials credentials = entry.getValue(); 300 | byte[] apiv3Key = apiV3Keys.get(merchantId); 301 | Verifier verifier = new DefaultVerifier(merchantId); 302 | try { 303 | downloadAndUpdateCert(merchantId, verifier, credentials, apiv3Key); 304 | } catch (Exception e) { 305 | log.error("downloadAndUpdateCert Failed.merchantId:{}, e:{}", merchantId, e); 306 | } 307 | } 308 | } 309 | 310 | /** 311 | * 内部验签器 312 | */ 313 | private class DefaultVerifier implements Verifier { 314 | private String merchantId; 315 | 316 | private DefaultVerifier(String merchantId) { 317 | this.merchantId = merchantId; 318 | } 319 | 320 | @Override 321 | public boolean verify(String serialNumber, byte[] message, String signature) { 322 | if (serialNumber.isEmpty() || message.length == 0 || signature.isEmpty()) { 323 | throw new IllegalArgumentException("serialNumber或message或signature为空"); 324 | } 325 | BigInteger serialNumber16Radix = new BigInteger(serialNumber, 16); 326 | ConcurrentHashMap merchantCertificates = certificates.get(merchantId); 327 | X509Certificate certificate = merchantCertificates.get(serialNumber16Radix); 328 | if (certificate == null) { 329 | log.error("商户证书为空,serialNumber:{}", serialNumber); 330 | return false; 331 | } 332 | try { 333 | Signature sign = Signature.getInstance("SHA256withRSA"); 334 | sign.initVerify(certificate); 335 | sign.update(message); 336 | return sign.verify(Base64.getDecoder().decode(signature)); 337 | } catch (NoSuchAlgorithmException e) { 338 | throw new RuntimeException("当前Java环境不支持SHA256withRSA", e); 339 | } catch (SignatureException e) { 340 | throw new RuntimeException("签名验证过程发生了错误", e); 341 | } catch (InvalidKeyException e) { 342 | throw new RuntimeException("无效的证书", e); 343 | } 344 | } 345 | 346 | public X509Certificate getValidCertificate() { 347 | X509Certificate certificate; 348 | try { 349 | certificate = CertificatesManager.this.getLatestCertificate(merchantId); 350 | } catch (NotFoundException e) { 351 | throw new NoSuchElementException("没有有效的微信支付平台证书"); 352 | } 353 | return certificate; 354 | } 355 | 356 | @Override 357 | public PublicKey getValidPublicKey() { 358 | return getValidCertificate().getPublicKey(); 359 | } 360 | 361 | @Override 362 | public String getSerialNumber() { 363 | return getValidCertificate().getSerialNumber().toString(16).toUpperCase(); 364 | } 365 | } 366 | } 367 | -------------------------------------------------------------------------------- /src/main/java/com/wechat/pay/contrib/apache/httpclient/cert/SafeSingleScheduleExecutor.java: -------------------------------------------------------------------------------- 1 | package com.wechat.pay.contrib.apache.httpclient.cert; 2 | 3 | import java.util.concurrent.RejectedExecutionException; 4 | import java.util.concurrent.ScheduledFuture; 5 | import java.util.concurrent.ScheduledThreadPoolExecutor; 6 | import java.util.concurrent.TimeUnit; 7 | 8 | /** 9 | * @author lianup 10 | * @since 0.3.0 11 | */ 12 | public class SafeSingleScheduleExecutor extends ScheduledThreadPoolExecutor { 13 | 14 | private static final int MAX_QUEUE_CAPACITY = 1; 15 | private static final int MAXIMUM_POOL_SIZE = 1; 16 | private static final int CORE_POOL_SIZE = 1; 17 | 18 | public SafeSingleScheduleExecutor() { 19 | super(CORE_POOL_SIZE); 20 | super.setMaximumPoolSize(MAXIMUM_POOL_SIZE); 21 | } 22 | 23 | 24 | @Override 25 | public ScheduledFuture scheduleAtFixedRate(Runnable command, long initialDelay, long period, 26 | TimeUnit unit) { 27 | if (getQueue().size() < MAX_QUEUE_CAPACITY) { 28 | return super.scheduleAtFixedRate(command, initialDelay, period, unit); 29 | } else { 30 | throw new RejectedExecutionException("当前任务数量超过最大队列最大数量"); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/wechat/pay/contrib/apache/httpclient/constant/WechatPayHttpHeaders.java: -------------------------------------------------------------------------------- 1 | package com.wechat.pay.contrib.apache.httpclient.constant; 2 | 3 | /** 4 | * 微信支付HTTP请求头相关常量 5 | * 6 | * @author Eric.Lee 7 | */ 8 | public final class WechatPayHttpHeaders { 9 | 10 | public static final String REQUEST_ID = "Request-ID"; 11 | public static final String WECHAT_PAY_SERIAL = "Wechatpay-Serial"; 12 | public static final String WECHAT_PAY_SIGNATURE = "Wechatpay-Signature"; 13 | public static final String WECHAT_PAY_TIMESTAMP = "Wechatpay-Timestamp"; 14 | public static final String WECHAT_PAY_NONCE = "Wechatpay-Nonce"; 15 | 16 | private WechatPayHttpHeaders() { 17 | // Don't allow instantiation 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/wechat/pay/contrib/apache/httpclient/exception/HttpCodeException.java: -------------------------------------------------------------------------------- 1 | package com.wechat.pay.contrib.apache.httpclient.exception; 2 | 3 | /** 4 | * @author lianup 5 | */ 6 | public class HttpCodeException extends WechatPayException { 7 | 8 | 9 | private static final long serialVersionUID = -1981192537895425762L; 10 | 11 | public HttpCodeException(String message) { 12 | super(message); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/wechat/pay/contrib/apache/httpclient/exception/NotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.wechat.pay.contrib.apache.httpclient.exception; 2 | 3 | /** 4 | * @author lianup 5 | */ 6 | public class NotFoundException extends WechatPayException { 7 | 8 | 9 | private static final long serialVersionUID = -1981192537895425762L; 10 | 11 | public NotFoundException(String message) { 12 | super(message); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/wechat/pay/contrib/apache/httpclient/exception/ParseException.java: -------------------------------------------------------------------------------- 1 | package com.wechat.pay.contrib.apache.httpclient.exception; 2 | 3 | /** 4 | * @author lianup 5 | */ 6 | public class ParseException extends WechatPayException { 7 | 8 | private static final long serialVersionUID = 4300538230471368120L; 9 | 10 | public ParseException(String message) { 11 | super(message); 12 | } 13 | 14 | public ParseException(String message, Throwable cause) { 15 | super(message, cause); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/wechat/pay/contrib/apache/httpclient/exception/ValidationException.java: -------------------------------------------------------------------------------- 1 | package com.wechat.pay.contrib.apache.httpclient.exception; 2 | 3 | /** 4 | * @author lianup 5 | */ 6 | public class ValidationException extends WechatPayException { 7 | 8 | 9 | private static final long serialVersionUID = -3473204321736989263L; 10 | 11 | 12 | public ValidationException(String message) { 13 | super(message); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/wechat/pay/contrib/apache/httpclient/exception/WechatPayException.java: -------------------------------------------------------------------------------- 1 | package com.wechat.pay.contrib.apache.httpclient.exception; 2 | 3 | /** 4 | * @author lianup 5 | */ 6 | public abstract class WechatPayException extends Exception { 7 | 8 | private static final long serialVersionUID = -5059029681600588999L; 9 | 10 | public WechatPayException(String message) { 11 | super(message); 12 | } 13 | 14 | public WechatPayException(String message, Throwable cause) { 15 | super(message, cause); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/wechat/pay/contrib/apache/httpclient/notification/Notification.java: -------------------------------------------------------------------------------- 1 | package com.wechat.pay.contrib.apache.httpclient.notification; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | 6 | /** 7 | * 请求体解析结果 8 | * 9 | * @author lianup 10 | */ 11 | @JsonIgnoreProperties(ignoreUnknown = true) 12 | public class Notification { 13 | 14 | @JsonProperty("id") 15 | private String id; 16 | @JsonProperty("create_time") 17 | private String createTime; 18 | @JsonProperty("event_type") 19 | private String eventType; 20 | @JsonProperty("resource_type") 21 | private String resourceType; 22 | @JsonProperty("summary") 23 | private String summary; 24 | @JsonProperty("resource") 25 | private Resource resource; 26 | private String decryptData; 27 | 28 | @Override 29 | public String toString() { 30 | return "Notification{" + 31 | "id='" + id + '\'' + 32 | ", createTime='" + createTime + '\'' + 33 | ", eventType='" + eventType + '\'' + 34 | ", resourceType='" + resourceType + '\'' + 35 | ", decryptData='" + decryptData + '\'' + 36 | ", summary='" + summary + '\'' + 37 | ", resource=" + resource + 38 | '}'; 39 | } 40 | 41 | public String getId() { 42 | return id; 43 | } 44 | 45 | public String getCreateTime() { 46 | return createTime; 47 | } 48 | 49 | public String getEventType() { 50 | return eventType; 51 | } 52 | 53 | public String getDecryptData() { 54 | return decryptData; 55 | } 56 | 57 | public String getSummary() { 58 | return summary; 59 | } 60 | 61 | public String getResourceType() { 62 | return resourceType; 63 | } 64 | 65 | public Resource getResource() { 66 | return resource; 67 | } 68 | 69 | public void setDecryptData(String decryptData) { 70 | this.decryptData = decryptData; 71 | } 72 | 73 | @JsonIgnoreProperties(ignoreUnknown = true) 74 | public class Resource { 75 | 76 | @JsonProperty("algorithm") 77 | private String algorithm; 78 | @JsonProperty("ciphertext") 79 | private String ciphertext; 80 | @JsonProperty("associated_data") 81 | private String associatedData; 82 | @JsonProperty("nonce") 83 | private String nonce; 84 | @JsonProperty("original_type") 85 | private String originalType; 86 | 87 | public String getAlgorithm() { 88 | return algorithm; 89 | } 90 | 91 | public String getCiphertext() { 92 | return ciphertext; 93 | } 94 | 95 | public String getAssociatedData() { 96 | return associatedData; 97 | } 98 | 99 | public String getNonce() { 100 | return nonce; 101 | } 102 | 103 | public String getOriginalType() { 104 | return originalType; 105 | } 106 | 107 | @Override 108 | public String toString() { 109 | return "Resource{" + 110 | "algorithm='" + algorithm + '\'' + 111 | ", ciphertext='" + ciphertext + '\'' + 112 | ", associatedData='" + associatedData + '\'' + 113 | ", nonce='" + nonce + '\'' + 114 | ", originalType='" + originalType + '\'' + 115 | '}'; 116 | } 117 | } 118 | 119 | } 120 | -------------------------------------------------------------------------------- /src/main/java/com/wechat/pay/contrib/apache/httpclient/notification/NotificationHandler.java: -------------------------------------------------------------------------------- 1 | package com.wechat.pay.contrib.apache.httpclient.notification; 2 | 3 | 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import com.fasterxml.jackson.databind.ObjectReader; 6 | import com.wechat.pay.contrib.apache.httpclient.auth.Verifier; 7 | import com.wechat.pay.contrib.apache.httpclient.exception.ParseException; 8 | import com.wechat.pay.contrib.apache.httpclient.exception.ValidationException; 9 | import com.wechat.pay.contrib.apache.httpclient.notification.Notification.Resource; 10 | import com.wechat.pay.contrib.apache.httpclient.util.AesUtil; 11 | import java.io.IOException; 12 | import java.nio.charset.StandardCharsets; 13 | import java.security.GeneralSecurityException; 14 | 15 | /** 16 | * @author lianup 17 | */ 18 | public class NotificationHandler { 19 | 20 | private final Verifier verifier; 21 | private final byte[] apiV3Key; 22 | private static final ObjectMapper objectMapper = new ObjectMapper(); 23 | 24 | public NotificationHandler(Verifier verifier, byte[] apiV3Key) { 25 | if (verifier == null) { 26 | throw new IllegalArgumentException("verifier为空"); 27 | } 28 | if (apiV3Key == null || apiV3Key.length == 0) { 29 | throw new IllegalArgumentException("apiV3Key为空"); 30 | } 31 | this.verifier = verifier; 32 | this.apiV3Key = apiV3Key; 33 | } 34 | 35 | /** 36 | * 解析微信支付通知请求结果 37 | * 38 | * @param request 微信支付通知请求 39 | * @return 微信支付通知报文解密结果 40 | * @throws ValidationException 1.输入参数不合法 2.参数被篡改导致验签失败 3.请求和验证的平台证书不一致导致验签失败 41 | * @throws ParseException 1.解析请求体为Json失败 2.请求体无对应参数 3.AES解密失败 42 | */ 43 | public Notification parse(Request request) 44 | throws ValidationException, ParseException { 45 | // 验签 46 | validate(request); 47 | // 解析请求体 48 | return parseBody(request.getBody()); 49 | } 50 | 51 | private void validate(Request request) throws ValidationException { 52 | if (request == null) { 53 | throw new ValidationException("request为空"); 54 | } 55 | String serialNumber = request.getSerialNumber(); 56 | byte[] message = request.getMessage(); 57 | String signature = request.getSignature(); 58 | if (serialNumber == null || serialNumber.isEmpty()) { 59 | throw new ValidationException("serialNumber为空"); 60 | } 61 | if (message == null || message.length == 0) { 62 | throw new ValidationException("message为空"); 63 | } 64 | if (signature == null || signature.isEmpty()) { 65 | throw new ValidationException("signature为空"); 66 | } 67 | if (!verifier.verify(serialNumber, message, signature)) { 68 | String errorMessage = String 69 | .format("验签失败:serial=[%s] message=[%s] sign=[%s]", serialNumber, new String(message), signature); 70 | throw new ValidationException(errorMessage); 71 | } 72 | } 73 | 74 | /** 75 | * 解析请求体 76 | * 77 | * @param body 请求体 78 | * @return 解析结果 79 | * @throws ParseException 解析body失败 80 | */ 81 | private Notification parseBody(String body) throws ParseException { 82 | ObjectReader objectReader = objectMapper.reader(); 83 | Notification notification; 84 | try { 85 | notification = objectReader.readValue(body, Notification.class); 86 | } catch (IOException ioException) { 87 | throw new ParseException("解析body失败,body:" + body, ioException); 88 | } 89 | validateNotification(notification); 90 | setDecryptData(notification); 91 | return notification; 92 | } 93 | 94 | /** 95 | * 校验解析后的通知结果 96 | * 97 | * @param notification 通知结果 98 | * @throws ParseException 参数不合法 99 | */ 100 | private void validateNotification(Notification notification) throws ParseException { 101 | if (notification == null) { 102 | throw new ParseException("body解析为空"); 103 | } 104 | String id = notification.getId(); 105 | if (id == null || id.isEmpty()) { 106 | throw new ParseException("body不合法,id为空。body:" + notification.toString()); 107 | } 108 | String createTime = notification.getCreateTime(); 109 | if (createTime == null || createTime.isEmpty()) { 110 | throw new ParseException("body不合法,createTime为空。body:" + notification.toString()); 111 | } 112 | String eventType = notification.getEventType(); 113 | if (eventType == null || eventType.isEmpty()) { 114 | throw new ParseException("body不合法,eventType为空。body:" + notification.toString()); 115 | } 116 | String summary = notification.getSummary(); 117 | if (summary == null || summary.isEmpty()) { 118 | throw new ParseException("body不合法,summary为空。body:" + notification.toString()); 119 | } 120 | String resourceType = notification.getResourceType(); 121 | if (resourceType == null || resourceType.isEmpty()) { 122 | throw new ParseException("body不合法,resourceType为空。body:" + notification.toString()); 123 | } 124 | Resource resource = notification.getResource(); 125 | if (resource == null) { 126 | throw new ParseException("body不合法,resource为空。notification:" + notification.toString()); 127 | } 128 | String algorithm = resource.getAlgorithm(); 129 | if (algorithm == null || algorithm.isEmpty()) { 130 | throw new ParseException("body不合法,algorithm为空。body:" + notification.toString()); 131 | } 132 | String originalType = resource.getOriginalType(); 133 | if (originalType == null || originalType.isEmpty()) { 134 | throw new ParseException("body不合法,original_type为空。body:" + notification.toString()); 135 | } 136 | String ciphertext = resource.getCiphertext(); 137 | if (ciphertext == null || ciphertext.isEmpty()) { 138 | throw new ParseException("body不合法,ciphertext为空。body:" + notification.toString()); 139 | } 140 | String nonce = resource.getNonce(); 141 | if (nonce == null || nonce.isEmpty()) { 142 | throw new ParseException("body不合法,nonce为空。body:" + notification.toString()); 143 | } 144 | } 145 | 146 | /** 147 | * 获取解密数据 148 | * 149 | * @param notification 解析body得到的通知结果 150 | * @throws ParseException 解析body失败 151 | */ 152 | private void setDecryptData(Notification notification) throws ParseException { 153 | 154 | Resource resource = notification.getResource(); 155 | String getAssociateddData = ""; 156 | if (resource.getAssociatedData() != null) { 157 | getAssociateddData = resource.getAssociatedData(); 158 | } 159 | byte[] associatedData = getAssociateddData.getBytes(StandardCharsets.UTF_8); 160 | byte[] nonce = resource.getNonce().getBytes(StandardCharsets.UTF_8); 161 | String ciphertext = resource.getCiphertext(); 162 | AesUtil aesUtil = new AesUtil(apiV3Key); 163 | String decryptData; 164 | try { 165 | decryptData = aesUtil.decryptToString(associatedData, nonce, ciphertext); 166 | } catch (GeneralSecurityException e) { 167 | throw new ParseException("AES解密失败,resource:" + resource.toString(), e); 168 | } 169 | notification.setDecryptData(decryptData); 170 | } 171 | 172 | } 173 | -------------------------------------------------------------------------------- /src/main/java/com/wechat/pay/contrib/apache/httpclient/notification/NotificationRequest.java: -------------------------------------------------------------------------------- 1 | package com.wechat.pay.contrib.apache.httpclient.notification; 2 | 3 | import java.nio.charset.StandardCharsets; 4 | 5 | /** 6 | * @author lianup 7 | */ 8 | public class NotificationRequest implements Request { 9 | 10 | private final String serialNumber; 11 | private final String signature; 12 | private final byte[] message; 13 | private final String body; 14 | 15 | private NotificationRequest(String serialNumber, String signature, byte[] message, String body) { 16 | this.serialNumber = serialNumber; 17 | this.signature = signature; 18 | this.message = message; 19 | this.body = body; 20 | } 21 | 22 | @Override 23 | public String getSerialNumber() { 24 | return serialNumber; 25 | } 26 | 27 | @Override 28 | public byte[] getMessage() { 29 | return message; 30 | } 31 | 32 | @Override 33 | public String getSignature() { 34 | return signature; 35 | } 36 | 37 | @Override 38 | public String getBody() { 39 | return body; 40 | } 41 | 42 | public static class Builder { 43 | 44 | private String serialNumber; 45 | private String timestamp; 46 | private String nonce; 47 | private String signature; 48 | private String body; 49 | 50 | public Builder() { 51 | } 52 | 53 | public Builder withSerialNumber(String serialNumber) { 54 | this.serialNumber = serialNumber; 55 | return this; 56 | } 57 | 58 | public Builder withTimestamp(String timestamp) { 59 | this.timestamp = timestamp; 60 | return this; 61 | } 62 | 63 | public Builder withNonce(String nonce) { 64 | this.nonce = nonce; 65 | return this; 66 | } 67 | 68 | public Builder withSignature(String signature) { 69 | this.signature = signature; 70 | return this; 71 | } 72 | 73 | public Builder withBody(String body) { 74 | this.body = body; 75 | return this; 76 | } 77 | 78 | public NotificationRequest build() { 79 | byte[] message = buildMessage(); 80 | return new NotificationRequest(serialNumber, signature, message, body); 81 | } 82 | 83 | private byte[] buildMessage() { 84 | String verifyMessage = timestamp + "\n" + nonce + "\n" + body + "\n"; 85 | return verifyMessage.getBytes(StandardCharsets.UTF_8); 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/main/java/com/wechat/pay/contrib/apache/httpclient/notification/Request.java: -------------------------------------------------------------------------------- 1 | package com.wechat.pay.contrib.apache.httpclient.notification; 2 | 3 | /** 4 | * 通知请求体,包含验签所需信息和报文体 5 | * 6 | * @author lianup 7 | */ 8 | public interface Request { 9 | 10 | /** 11 | * 获取请求头Wechatpay-Serial 12 | * 13 | * @return serialNumber 14 | */ 15 | String getSerialNumber(); 16 | 17 | /** 18 | * 获取验签串 19 | * 20 | * @return message 21 | */ 22 | byte[] getMessage(); 23 | 24 | /** 25 | * 获取请求头Wechatpay-Signature 26 | * 27 | * @return signature 28 | */ 29 | String getSignature(); 30 | 31 | /** 32 | * 获取请求体 33 | * 34 | * @return body 35 | */ 36 | String getBody(); 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/wechat/pay/contrib/apache/httpclient/proxy/HttpProxyFactory.java: -------------------------------------------------------------------------------- 1 | package com.wechat.pay.contrib.apache.httpclient.proxy; 2 | 3 | import org.apache.http.HttpHost; 4 | 5 | /** 6 | * HttpProxyFactory 代理工厂 7 | * 8 | * @author ramzeng 9 | */ 10 | public interface HttpProxyFactory { 11 | 12 | /** 13 | * 构建代理 14 | * 15 | * @return 代理 16 | */ 17 | HttpHost buildHttpProxy(); 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/wechat/pay/contrib/apache/httpclient/util/AesUtil.java: -------------------------------------------------------------------------------- 1 | package com.wechat.pay.contrib.apache.httpclient.util; 2 | 3 | import java.nio.charset.StandardCharsets; 4 | import java.security.GeneralSecurityException; 5 | import java.security.InvalidAlgorithmParameterException; 6 | import java.security.InvalidKeyException; 7 | import java.security.NoSuchAlgorithmException; 8 | import java.util.Base64; 9 | import javax.crypto.Cipher; 10 | import javax.crypto.NoSuchPaddingException; 11 | import javax.crypto.spec.GCMParameterSpec; 12 | import javax.crypto.spec.SecretKeySpec; 13 | 14 | /** 15 | * @author xy-peng 16 | */ 17 | public class AesUtil { 18 | 19 | private static final String TRANSFORMATION = "AES/GCM/NoPadding"; 20 | 21 | private static final int KEY_LENGTH_BYTE = 32; 22 | private static final int TAG_LENGTH_BIT = 128; 23 | 24 | private final byte[] aesKey; 25 | 26 | public AesUtil(byte[] key) { 27 | if (key.length != KEY_LENGTH_BYTE) { 28 | throw new IllegalArgumentException("无效的ApiV3Key,长度必须为32个字节"); 29 | } 30 | this.aesKey = key; 31 | } 32 | 33 | public String decryptToString(byte[] associatedData, byte[] nonce, String ciphertext) 34 | throws GeneralSecurityException { 35 | try { 36 | SecretKeySpec key = new SecretKeySpec(aesKey, "AES"); 37 | GCMParameterSpec spec = new GCMParameterSpec(TAG_LENGTH_BIT, nonce); 38 | 39 | Cipher cipher = Cipher.getInstance(TRANSFORMATION); 40 | cipher.init(Cipher.DECRYPT_MODE, key, spec); 41 | cipher.updateAAD(associatedData); 42 | return new String(cipher.doFinal(Base64.getDecoder().decode(ciphertext)), StandardCharsets.UTF_8); 43 | 44 | } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { 45 | throw new IllegalStateException(e); 46 | } catch (InvalidKeyException | InvalidAlgorithmParameterException e) { 47 | throw new IllegalArgumentException(e); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/wechat/pay/contrib/apache/httpclient/util/CertSerializeUtil.java: -------------------------------------------------------------------------------- 1 | package com.wechat.pay.contrib.apache.httpclient.util; 2 | 3 | import com.fasterxml.jackson.databind.JsonNode; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import java.io.ByteArrayInputStream; 6 | import java.io.IOException; 7 | import java.math.BigInteger; 8 | import java.nio.charset.StandardCharsets; 9 | import java.security.GeneralSecurityException; 10 | import java.security.cert.CertificateExpiredException; 11 | import java.security.cert.CertificateFactory; 12 | import java.security.cert.CertificateNotYetValidException; 13 | import java.security.cert.X509Certificate; 14 | import java.util.HashMap; 15 | import java.util.Map; 16 | 17 | /** 18 | * @author lianup 19 | * @since 0.3.0 20 | */ 21 | public class CertSerializeUtil { 22 | 23 | /** 24 | * 反序列化证书并解密 25 | * 26 | * @param apiV3Key APIv3密钥 27 | * @param body 下载证书的请求返回体 28 | * @return 证书list 29 | * @throws GeneralSecurityException 当证书过期或尚未生效时 30 | * @throws IOException 当body不合法时 31 | */ 32 | public static Map deserializeToCerts(byte[] apiV3Key, String body) 33 | throws GeneralSecurityException, IOException { 34 | AesUtil aesUtil = new AesUtil(apiV3Key); 35 | ObjectMapper mapper = new ObjectMapper(); 36 | JsonNode dataNode = mapper.readTree(body).get("data"); 37 | Map newCertList = new HashMap<>(); 38 | if (dataNode != null) { 39 | for (int i = 0, count = dataNode.size(); i < count; i++) { 40 | JsonNode node = dataNode.get(i).get("encrypt_certificate"); 41 | //解密 42 | String cert = aesUtil.decryptToString( 43 | node.get("associated_data").toString().replace("\"", "") 44 | .getBytes(StandardCharsets.UTF_8), 45 | node.get("nonce").toString().replace("\"", "") 46 | .getBytes(StandardCharsets.UTF_8), 47 | node.get("ciphertext").toString().replace("\"", "")); 48 | 49 | CertificateFactory cf = CertificateFactory.getInstance("X509"); 50 | X509Certificate x509Cert = (X509Certificate) cf.generateCertificate( 51 | new ByteArrayInputStream(cert.getBytes(StandardCharsets.UTF_8)) 52 | ); 53 | try { 54 | x509Cert.checkValidity(); 55 | } catch (CertificateExpiredException | CertificateNotYetValidException ignored) { 56 | continue; 57 | } 58 | newCertList.put(x509Cert.getSerialNumber(), x509Cert); 59 | } 60 | } 61 | return newCertList; 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/com/wechat/pay/contrib/apache/httpclient/util/PemUtil.java: -------------------------------------------------------------------------------- 1 | package com.wechat.pay.contrib.apache.httpclient.util; 2 | 3 | import java.io.ByteArrayOutputStream; 4 | import java.io.IOException; 5 | import java.io.InputStream; 6 | import java.security.KeyFactory; 7 | import java.security.NoSuchAlgorithmException; 8 | import java.security.PrivateKey; 9 | import java.security.PublicKey; 10 | import java.security.cert.CertificateException; 11 | import java.security.cert.CertificateExpiredException; 12 | import java.security.cert.CertificateFactory; 13 | import java.security.cert.CertificateNotYetValidException; 14 | import java.security.cert.X509Certificate; 15 | import java.security.spec.InvalidKeySpecException; 16 | import java.security.spec.PKCS8EncodedKeySpec; 17 | import java.security.spec.X509EncodedKeySpec; 18 | import java.util.Base64; 19 | 20 | /** 21 | * @author xy-peng 22 | */ 23 | public class PemUtil { 24 | 25 | public static PrivateKey loadPrivateKey(String privateKey) { 26 | privateKey = privateKey 27 | .replace("-----BEGIN PRIVATE KEY-----", "") 28 | .replace("-----END PRIVATE KEY-----", "") 29 | .replaceAll("\\s+", ""); 30 | 31 | try { 32 | KeyFactory kf = KeyFactory.getInstance("RSA"); 33 | return kf.generatePrivate(new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKey))); 34 | 35 | } catch (NoSuchAlgorithmException e) { 36 | throw new RuntimeException("当前Java环境不支持RSA", e); 37 | } catch (InvalidKeySpecException e) { 38 | throw new RuntimeException("无效的密钥格式"); 39 | } 40 | } 41 | 42 | public static PrivateKey loadPrivateKey(InputStream inputStream) { 43 | ByteArrayOutputStream os = new ByteArrayOutputStream(2048); 44 | byte[] buffer = new byte[1024]; 45 | String privateKey; 46 | try { 47 | for (int length; (length = inputStream.read(buffer)) != -1; ) { 48 | os.write(buffer, 0, length); 49 | } 50 | privateKey = os.toString("UTF-8"); 51 | } catch (IOException e) { 52 | throw new IllegalArgumentException("无效的密钥", e); 53 | } 54 | return loadPrivateKey(privateKey); 55 | } 56 | 57 | public static PublicKey loadPublicKey(String publicKey) { 58 | String keyString = publicKey 59 | .replace("-----BEGIN PUBLIC KEY-----", "") 60 | .replace("-----END PUBLIC KEY-----", "") 61 | .replaceAll("\\s+", ""); 62 | 63 | try { 64 | return KeyFactory.getInstance("RSA") 65 | .generatePublic(new X509EncodedKeySpec(Base64.getDecoder().decode(keyString))); 66 | } catch (NoSuchAlgorithmException e) { 67 | throw new UnsupportedOperationException(e); 68 | } catch (InvalidKeySpecException e) { 69 | throw new IllegalArgumentException(e); 70 | } 71 | } 72 | 73 | public static PublicKey loadPublicKey(InputStream inputStream) { 74 | ByteArrayOutputStream os = new ByteArrayOutputStream(2048); 75 | byte[] buffer = new byte[1024]; 76 | String publicKey; 77 | try { 78 | for (int length; (length = inputStream.read(buffer)) != -1; ) { 79 | os.write(buffer, 0, length); 80 | } 81 | publicKey = os.toString("UTF-8"); 82 | } catch (IOException e) { 83 | throw new IllegalArgumentException("无效的公钥", e); 84 | } 85 | return loadPublicKey(publicKey); 86 | } 87 | 88 | public static X509Certificate loadCertificate(InputStream inputStream) { 89 | try { 90 | CertificateFactory cf = CertificateFactory.getInstance("X509"); 91 | X509Certificate cert = (X509Certificate) cf.generateCertificate(inputStream); 92 | cert.checkValidity(); 93 | return cert; 94 | } catch (CertificateExpiredException e) { 95 | throw new RuntimeException("证书已过期", e); 96 | } catch (CertificateNotYetValidException e) { 97 | throw new RuntimeException("证书尚未生效", e); 98 | } catch (CertificateException e) { 99 | throw new RuntimeException("无效的证书", e); 100 | } 101 | } 102 | 103 | } 104 | -------------------------------------------------------------------------------- /src/main/java/com/wechat/pay/contrib/apache/httpclient/util/RsaCryptoUtil.java: -------------------------------------------------------------------------------- 1 | package com.wechat.pay.contrib.apache.httpclient.util; 2 | 3 | import java.nio.charset.StandardCharsets; 4 | import java.security.InvalidKeyException; 5 | import java.security.NoSuchAlgorithmException; 6 | import java.security.PrivateKey; 7 | import java.security.PublicKey; 8 | import java.security.cert.X509Certificate; 9 | import java.util.Base64; 10 | import javax.crypto.BadPaddingException; 11 | import javax.crypto.Cipher; 12 | import javax.crypto.IllegalBlockSizeException; 13 | import javax.crypto.NoSuchPaddingException; 14 | 15 | /** 16 | * @author xy-peng 17 | */ 18 | public class RsaCryptoUtil { 19 | 20 | private static final String TRANSFORMATION = "RSA/ECB/OAEPWithSHA-1AndMGF1Padding"; 21 | 22 | public static String encryptOAEP(String message, X509Certificate certificate) throws IllegalBlockSizeException { 23 | return encrypt(message, certificate, TRANSFORMATION); 24 | } 25 | 26 | public static String encrypt(String message, X509Certificate certificate, String transformation) throws IllegalBlockSizeException { 27 | return encrypt(message, certificate.getPublicKey(), transformation); 28 | } 29 | 30 | public static String encryptOAEP(String message, PublicKey publicKey) throws IllegalBlockSizeException { 31 | return encrypt(message, publicKey, TRANSFORMATION); 32 | } 33 | 34 | public static String encrypt(String message, PublicKey publicKey, String transformation) throws IllegalBlockSizeException { 35 | try { 36 | Cipher cipher = Cipher.getInstance(transformation); 37 | cipher.init(Cipher.ENCRYPT_MODE, publicKey); 38 | byte[] data = message.getBytes(StandardCharsets.UTF_8); 39 | byte[] ciphertext = cipher.doFinal(data); 40 | return Base64.getEncoder().encodeToString(ciphertext); 41 | 42 | } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { 43 | throw new RuntimeException("当前Java环境不支持RSA v1.5/OAEP", e); 44 | } catch (InvalidKeyException e) { 45 | throw new IllegalArgumentException("无效的证书", e); 46 | } catch (IllegalBlockSizeException | BadPaddingException e) { 47 | throw new IllegalBlockSizeException("加密原串的长度不能超过214字节"); 48 | } 49 | } 50 | 51 | public static String decryptOAEP(String ciphertext, PrivateKey privateKey) throws BadPaddingException { 52 | return decrypt(ciphertext, privateKey, TRANSFORMATION); 53 | } 54 | 55 | public static String decrypt(String ciphertext, PrivateKey privateKey, String transformation) throws BadPaddingException { 56 | try { 57 | Cipher cipher = Cipher.getInstance(transformation); 58 | cipher.init(Cipher.DECRYPT_MODE, privateKey); 59 | byte[] data = Base64.getDecoder().decode(ciphertext); 60 | return new String(cipher.doFinal(data), StandardCharsets.UTF_8); 61 | 62 | } catch (NoSuchPaddingException | NoSuchAlgorithmException e) { 63 | throw new RuntimeException("当前Java环境不支持RSA v1.5/OAEP", e); 64 | } catch (InvalidKeyException e) { 65 | throw new IllegalArgumentException("无效的私钥", e); 66 | } catch (BadPaddingException | IllegalBlockSizeException e) { 67 | throw new BadPaddingException("解密失败"); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/test/java/com/wechat/pay/contrib/apache/httpclient/AutoUpdateVerifierTest.java: -------------------------------------------------------------------------------- 1 | package com.wechat.pay.contrib.apache.httpclient; 2 | 3 | import static org.apache.http.HttpHeaders.ACCEPT; 4 | import static org.apache.http.HttpStatus.SC_OK; 5 | import static org.apache.http.entity.ContentType.APPLICATION_JSON; 6 | import static org.junit.Assert.assertEquals; 7 | import static org.junit.Assert.assertTrue; 8 | 9 | import com.wechat.pay.contrib.apache.httpclient.auth.AutoUpdateCertificatesVerifier; 10 | import com.wechat.pay.contrib.apache.httpclient.auth.PrivateKeySigner; 11 | import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Credentials; 12 | import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Validator; 13 | import com.wechat.pay.contrib.apache.httpclient.util.PemUtil; 14 | import java.io.File; 15 | import java.io.FileInputStream; 16 | import java.io.IOException; 17 | import java.io.InputStream; 18 | import java.net.URI; 19 | import java.nio.charset.StandardCharsets; 20 | import java.security.PrivateKey; 21 | import org.apache.commons.codec.digest.DigestUtils; 22 | import org.apache.http.HttpEntity; 23 | import org.apache.http.client.methods.CloseableHttpResponse; 24 | import org.apache.http.client.methods.HttpGet; 25 | import org.apache.http.client.utils.URIBuilder; 26 | import org.apache.http.impl.client.CloseableHttpClient; 27 | import org.apache.http.util.EntityUtils; 28 | import org.junit.After; 29 | import org.junit.Before; 30 | import org.junit.Test; 31 | 32 | @Deprecated 33 | public class AutoUpdateVerifierTest { 34 | 35 | // 你的商户私钥 36 | private static final String privateKey = "-----BEGIN PRIVATE KEY-----\n" 37 | + "-----END PRIVATE KEY-----\n"; 38 | //测试AutoUpdateCertificatesVerifier的verify方法参数 39 | private static final String serialNumber = ""; 40 | private static final String message = ""; 41 | private static final String signature = ""; 42 | private static final String merchantId = ""; // 商户号 43 | private static final String merchantSerialNumber = ""; // 商户证书序列号 44 | private static final String apiV3Key = ""; // API V3密钥 45 | private CloseableHttpClient httpClient; 46 | private AutoUpdateCertificatesVerifier verifier; 47 | 48 | @Before 49 | public void setup() { 50 | PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(privateKey); 51 | 52 | //使用自动更新的签名验证器,不需要传入证书 53 | verifier = new AutoUpdateCertificatesVerifier( 54 | new WechatPay2Credentials(merchantId, new PrivateKeySigner(merchantSerialNumber, merchantPrivateKey)), 55 | apiV3Key.getBytes(StandardCharsets.UTF_8)); 56 | 57 | httpClient = WechatPayHttpClientBuilder.create() 58 | .withMerchant(merchantId, merchantSerialNumber, merchantPrivateKey) 59 | .withValidator(new WechatPay2Validator(verifier)) 60 | .build(); 61 | } 62 | 63 | @After 64 | public void after() throws IOException { 65 | httpClient.close(); 66 | } 67 | 68 | @Test 69 | public void autoUpdateVerifierTest() { 70 | assertTrue(verifier.verify(serialNumber, message.getBytes(StandardCharsets.UTF_8), signature)); 71 | } 72 | 73 | @Test 74 | public void getCertificateTest() throws Exception { 75 | URIBuilder uriBuilder = new URIBuilder("https://api.mch.weixin.qq.com/v3/certificates"); 76 | HttpGet httpGet = new HttpGet(uriBuilder.build()); 77 | httpGet.addHeader(ACCEPT, APPLICATION_JSON.toString()); 78 | CloseableHttpResponse response = httpClient.execute(httpGet); 79 | assertEquals(SC_OK, response.getStatusLine().getStatusCode()); 80 | try { 81 | HttpEntity entity = response.getEntity(); 82 | // do something useful with the response body 83 | // and ensure it is fully consumed 84 | EntityUtils.consume(entity); 85 | } finally { 86 | response.close(); 87 | } 88 | } 89 | 90 | @Test 91 | public void uploadImageTest() throws Exception { 92 | String filePath = "/your/home/hellokitty.png"; 93 | 94 | URI uri = new URI("https://api.mch.weixin.qq.com/v3/merchant/media/upload"); 95 | 96 | File file = new File(filePath); 97 | try (FileInputStream fileIs = new FileInputStream(file)) { 98 | String sha256 = DigestUtils.sha256Hex(fileIs); 99 | try (InputStream is = new FileInputStream(file)) { 100 | WechatPayUploadHttpPost request = new WechatPayUploadHttpPost.Builder(uri) 101 | .withImage(file.getName(), sha256, is) 102 | .build(); 103 | 104 | try (CloseableHttpResponse response = httpClient.execute(request)) { 105 | assertEquals(SC_OK, response.getStatusLine().getStatusCode()); 106 | HttpEntity entity = response.getEntity(); 107 | // do something useful with the response body 108 | // and ensure it is fully consumed 109 | String s = EntityUtils.toString(entity); 110 | System.out.println(s); 111 | } 112 | } 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/test/java/com/wechat/pay/contrib/apache/httpclient/CertificatesManagerTest.java: -------------------------------------------------------------------------------- 1 | package com.wechat.pay.contrib.apache.httpclient; 2 | 3 | import static org.apache.http.HttpHeaders.ACCEPT; 4 | import static org.apache.http.HttpStatus.SC_OK; 5 | import static org.apache.http.entity.ContentType.APPLICATION_JSON; 6 | import static org.junit.Assert.assertEquals; 7 | 8 | import com.wechat.pay.contrib.apache.httpclient.auth.PrivateKeySigner; 9 | import com.wechat.pay.contrib.apache.httpclient.auth.Verifier; 10 | import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Credentials; 11 | import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Validator; 12 | import com.wechat.pay.contrib.apache.httpclient.cert.CertificatesManager; 13 | import com.wechat.pay.contrib.apache.httpclient.proxy.HttpProxyFactory; 14 | import com.wechat.pay.contrib.apache.httpclient.util.PemUtil; 15 | import java.io.File; 16 | import java.io.FileInputStream; 17 | import java.io.IOException; 18 | import java.io.InputStream; 19 | import java.net.URI; 20 | import java.nio.charset.StandardCharsets; 21 | import java.security.PrivateKey; 22 | import org.apache.commons.codec.digest.DigestUtils; 23 | import org.apache.http.HttpEntity; 24 | import org.apache.http.HttpHost; 25 | import org.apache.http.client.methods.CloseableHttpResponse; 26 | import org.apache.http.client.methods.HttpGet; 27 | import org.apache.http.client.utils.URIBuilder; 28 | import org.apache.http.impl.client.CloseableHttpClient; 29 | import org.apache.http.util.EntityUtils; 30 | import org.junit.After; 31 | import org.junit.Assert; 32 | import org.junit.Before; 33 | import org.junit.Test; 34 | 35 | public class CertificatesManagerTest { 36 | 37 | // 你的商户私钥 38 | private static final String privateKey = "-----BEGIN PRIVATE KEY-----\n" 39 | + "-----END PRIVATE KEY-----\n"; 40 | private static final String merchantId = ""; // 商户号 41 | private static final String merchantSerialNumber = ""; // 商户证书序列号 42 | private static final String apiV3Key = ""; // API V3密钥 43 | private static final HttpHost proxy = null; 44 | CertificatesManager certificatesManager; 45 | Verifier verifier; 46 | private CloseableHttpClient httpClient; 47 | 48 | @Before 49 | public void setup() throws Exception { 50 | PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(privateKey); 51 | // 获取证书管理器实例 52 | certificatesManager = CertificatesManager.getInstance(); 53 | // 添加代理服务器 54 | certificatesManager.setProxy(proxy); 55 | // 向证书管理器增加需要自动更新平台证书的商户信息 56 | certificatesManager.putMerchant(merchantId, new WechatPay2Credentials(merchantId, 57 | new PrivateKeySigner(merchantSerialNumber, merchantPrivateKey)), 58 | apiV3Key.getBytes(StandardCharsets.UTF_8)); 59 | // 从证书管理器中获取verifier 60 | verifier = certificatesManager.getVerifier(merchantId); 61 | // 构造httpclient 62 | httpClient = WechatPayHttpClientBuilder.create() 63 | .withMerchant(merchantId, merchantSerialNumber, merchantPrivateKey) 64 | .withValidator(new WechatPay2Validator(verifier)) 65 | .build(); 66 | } 67 | 68 | @After 69 | public void after() throws IOException { 70 | httpClient.close(); 71 | } 72 | 73 | @Test 74 | public void getCertificateTest() throws Exception { 75 | URIBuilder uriBuilder = new URIBuilder("https://api.mch.weixin.qq.com/v3/certificates"); 76 | HttpGet httpGet = new HttpGet(uriBuilder.build()); 77 | httpGet.addHeader(ACCEPT, APPLICATION_JSON.toString()); 78 | CloseableHttpResponse response = httpClient.execute(httpGet); 79 | assertEquals(SC_OK, response.getStatusLine().getStatusCode()); 80 | try { 81 | HttpEntity entity = response.getEntity(); 82 | // do something useful with the response body 83 | // and ensure it is fully consumed 84 | EntityUtils.consume(entity); 85 | } finally { 86 | response.close(); 87 | } 88 | } 89 | 90 | @Test 91 | public void uploadImageTest() throws Exception { 92 | String filePath = "/your/home/test.png"; 93 | 94 | URI uri = new URI("https://api.mch.weixin.qq.com/v3/merchant/media/upload"); 95 | 96 | File file = new File(filePath); 97 | try (FileInputStream fileIs = new FileInputStream(file)) { 98 | String sha256 = DigestUtils.sha256Hex(fileIs); 99 | try (InputStream is = new FileInputStream(file)) { 100 | WechatPayUploadHttpPost request = new WechatPayUploadHttpPost.Builder(uri) 101 | .withImage(file.getName(), sha256, is) 102 | .build(); 103 | 104 | try (CloseableHttpResponse response = httpClient.execute(request)) { 105 | assertEquals(SC_OK, response.getStatusLine().getStatusCode()); 106 | HttpEntity entity = response.getEntity(); 107 | // do something useful with the response body 108 | // and ensure it is fully consumed 109 | String s = EntityUtils.toString(entity); 110 | System.out.println(s); 111 | } 112 | } 113 | } 114 | } 115 | 116 | @Test 117 | public void uploadFileTest() throws Exception { 118 | String filePath = "/your/home/test.png"; 119 | 120 | URI uri = new URI("https://api.mch.weixin.qq.com/v3/merchant/media/upload"); 121 | 122 | File file = new File(filePath); 123 | try (FileInputStream fileIs = new FileInputStream(file)) { 124 | String sha256 = DigestUtils.sha256Hex(fileIs); 125 | String meta = String.format("{\"filename\":\"%s\",\"sha256\":\"%s\"}", file.getName(), sha256); 126 | try (InputStream is = new FileInputStream(file)) { 127 | WechatPayUploadHttpPost request = new WechatPayUploadHttpPost.Builder(uri) 128 | .withFile(file.getName(), meta, is) 129 | .build(); 130 | try (CloseableHttpResponse response = httpClient.execute(request)) { 131 | assertEquals(SC_OK, response.getStatusLine().getStatusCode()); 132 | HttpEntity entity = response.getEntity(); 133 | // do something useful with the response body 134 | // and ensure it is fully consumed 135 | String s = EntityUtils.toString(entity); 136 | System.out.println(s); 137 | } 138 | } 139 | } 140 | } 141 | 142 | @Test 143 | public void proxyFactoryTest() { 144 | CertificatesManager certificatesManager = CertificatesManager.getInstance(); 145 | Assert.assertEquals(certificatesManager.resolveProxy(), proxy); 146 | certificatesManager.setProxyFactory(new MockHttpProxyFactory()); 147 | HttpHost httpProxy = certificatesManager.resolveProxy(); 148 | Assert.assertNotEquals(httpProxy, proxy); 149 | Assert.assertEquals(httpProxy.getHostName(), "127.0.0.1"); 150 | Assert.assertEquals(httpProxy.getPort(), 1087); 151 | } 152 | 153 | private static class MockHttpProxyFactory implements HttpProxyFactory { 154 | 155 | @Override 156 | public HttpHost buildHttpProxy() { 157 | return new HttpHost("127.0.0.1", 1087); 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/test/java/com/wechat/pay/contrib/apache/httpclient/HttpClientBuilderTest.java: -------------------------------------------------------------------------------- 1 | package com.wechat.pay.contrib.apache.httpclient; 2 | 3 | import static org.apache.http.HttpHeaders.ACCEPT; 4 | import static org.apache.http.HttpStatus.SC_OK; 5 | import static org.apache.http.HttpStatus.SC_UNAUTHORIZED; 6 | import static org.apache.http.entity.ContentType.APPLICATION_JSON; 7 | import static org.junit.Assert.assertEquals; 8 | import static org.junit.Assert.assertTrue; 9 | 10 | import com.wechat.pay.contrib.apache.httpclient.util.PemUtil; 11 | import java.io.ByteArrayInputStream; 12 | import java.io.IOException; 13 | import java.io.InputStream; 14 | import java.nio.charset.StandardCharsets; 15 | import java.security.PrivateKey; 16 | import java.security.PublicKey; 17 | import java.security.cert.X509Certificate; 18 | import java.util.ArrayList; 19 | import java.util.function.Consumer; 20 | import org.apache.http.HttpEntity; 21 | import org.apache.http.client.methods.CloseableHttpResponse; 22 | import org.apache.http.client.methods.HttpGet; 23 | import org.apache.http.client.methods.HttpPost; 24 | import org.apache.http.client.methods.HttpUriRequest; 25 | import org.apache.http.client.utils.URIBuilder; 26 | import org.apache.http.entity.InputStreamEntity; 27 | import org.apache.http.entity.StringEntity; 28 | import org.apache.http.impl.client.CloseableHttpClient; 29 | import org.apache.http.HttpHost; 30 | import org.junit.After; 31 | import org.junit.Before; 32 | import org.junit.Test; 33 | 34 | public class HttpClientBuilderTest { 35 | 36 | private static final String merchantId = "1900009191"; // 商户号 37 | private static final String merchantSerialNumber = "1DDE55AD98ED71D6EDD4A4A16996DE7B47773A8C"; // 商户证书序列号 38 | private static final String requestBody = "{\n" 39 | + " \"stock_id\": \"9433645\",\n" 40 | + " \"stock_creator_mchid\": \"1900006511\",\n" 41 | + " \"out_request_no\": \"20190522_001中文11\",\n" 42 | + " \"appid\": \"wxab8acb865bb1637e\"\n" 43 | + "}"; 44 | // 你的商户私钥 45 | private static final String privateKey = "-----BEGIN PRIVATE KEY-----\n" 46 | + "-----END PRIVATE KEY-----"; 47 | // 微信支付公钥 48 | private static final String wechatPayPublicKeyStr = "-----BEGIN PUBLIC KEY-----\n" 49 | + "-----END PUBLIC KEY-----"; 50 | // 微信支付公钥ID 51 | private static final String wechatpayPublicKeyId = "PUB_KEY_ID_"; 52 | private CloseableHttpClient httpClient; 53 | 54 | private static final HttpHost proxy = null; 55 | 56 | @Before 57 | public void setup() { 58 | PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(privateKey); 59 | PublicKey wechatPayPublicKey = PemUtil.loadPublicKey(wechatPayPublicKeyStr); 60 | 61 | httpClient = WechatPayHttpClientBuilder.create() 62 | .withMerchant(merchantId, merchantSerialNumber, merchantPrivateKey) 63 | .withWechatPay(wechatpayPublicKeyId, wechatPayPublicKey) 64 | .withProxy(proxy) 65 | .build(); 66 | } 67 | 68 | @After 69 | public void after() throws IOException { 70 | httpClient.close(); 71 | } 72 | 73 | @Test 74 | public void getCertificateTest() throws Exception { 75 | URIBuilder uriBuilder = new URIBuilder("https://api.mch.weixin.qq.com/v3/certificates"); 76 | uriBuilder.setParameter("p", "1&2"); 77 | uriBuilder.setParameter("q", "你好"); 78 | 79 | HttpGet httpGet = new HttpGet(uriBuilder.build()); 80 | 81 | doSend(httpGet, null, response -> assertEquals(SC_OK, response.getStatusLine().getStatusCode())); 82 | 83 | } 84 | 85 | @Test 86 | public void postNonRepeatableEntityTest() throws IOException { 87 | HttpPost httpPost = new HttpPost( 88 | "https://api.mch.weixin.qq.com/v3/marketing/favor/users/oHkLxt_htg84TUEbzvlMwQzVDBqo/coupons"); 89 | 90 | final byte[] bytes = requestBody.getBytes(StandardCharsets.UTF_8); 91 | final InputStream stream = new ByteArrayInputStream(bytes); 92 | doSend(httpPost, new InputStreamEntity(stream, bytes.length, APPLICATION_JSON), 93 | response -> assertTrue(response.getStatusLine().getStatusCode() != SC_UNAUTHORIZED)); 94 | } 95 | 96 | @Test 97 | public void postRepeatableEntityTest() throws IOException { 98 | HttpPost httpPost = new HttpPost( 99 | "https://api.mch.weixin.qq.com/v3/marketing/favor/users/oHkLxt_htg84TUEbzvlMwQzVDBqo/coupons"); 100 | 101 | doSend(httpPost, new StringEntity(requestBody, APPLICATION_JSON), 102 | response -> assertTrue(response.getStatusLine().getStatusCode() != SC_UNAUTHORIZED)); 103 | } 104 | 105 | protected void doSend(HttpUriRequest request, HttpEntity entity, Consumer responseCallback) 106 | throws IOException { 107 | if (entity != null && request instanceof HttpPost) { 108 | ((HttpPost) request).setEntity(entity); 109 | } 110 | request.addHeader(ACCEPT, APPLICATION_JSON.toString()); 111 | 112 | try (CloseableHttpResponse response = httpClient.execute(request)) { 113 | responseCallback.accept(response); 114 | } 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /src/test/java/com/wechat/pay/contrib/apache/httpclient/NotificationHandlerTest.java: -------------------------------------------------------------------------------- 1 | package com.wechat.pay.contrib.apache.httpclient; 2 | 3 | import com.wechat.pay.contrib.apache.httpclient.auth.MixVerifier; 4 | import com.wechat.pay.contrib.apache.httpclient.auth.PrivateKeySigner; 5 | import com.wechat.pay.contrib.apache.httpclient.auth.PublicKeyVerifier; 6 | import com.wechat.pay.contrib.apache.httpclient.auth.Verifier; 7 | import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Credentials; 8 | import com.wechat.pay.contrib.apache.httpclient.cert.CertificatesManager; 9 | import com.wechat.pay.contrib.apache.httpclient.notification.Notification; 10 | import com.wechat.pay.contrib.apache.httpclient.notification.NotificationHandler; 11 | import com.wechat.pay.contrib.apache.httpclient.notification.NotificationRequest; 12 | import com.wechat.pay.contrib.apache.httpclient.util.PemUtil; 13 | import java.nio.charset.StandardCharsets; 14 | import java.security.PrivateKey; 15 | import java.security.PublicKey; 16 | 17 | import org.junit.Assert; 18 | import org.junit.Before; 19 | import org.junit.Test; 20 | 21 | public class NotificationHandlerTest { 22 | 23 | private static final String privateKey = "-----BEGIN PRIVATE KEY-----\n" 24 | + "-----END PRIVATE KEY-----\n"; // 商户私钥 25 | private static final String merchantId = ""; // 商户号 26 | private static final String merchantSerialNumber = ""; // 商户证书序列号 27 | private static final String apiV3Key = ""; // apiV3密钥 28 | private static final String wechatPaySerial = ""; // 平台证书序列号 29 | private static final String wechatPayPublicKeyStr = "-----BEGIN PUBLIC KEY-----\n" 30 | + "-----END PUBLIC KEY-----"; // 微信支付公钥 31 | private static final String wechatpayPublicKeyId = "PUB_KEY_ID_"; // 微信支付公钥ID 32 | private static final String nonce = ""; // 请求头Wechatpay-Nonce 33 | private static final String timestamp = "";// 请求头Wechatpay-Timestamp 34 | private static final String signature = "";// 请求头Wechatpay-Signature 35 | private static final String body = ""; // 请求体 36 | private Verifier publicKeyVerifier; // 微信支付公钥验签器 37 | private Verifier certificateVerifier; // 平台证书验签器 38 | private static CertificatesManager certificatesManager; // 平台证书管理器 39 | 40 | @Before 41 | public void setup() throws Exception { 42 | PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(privateKey); 43 | // 获取证书管理器实例 44 | certificatesManager = CertificatesManager.getInstance(); 45 | // 向证书管理器增加需要自动更新平台证书的商户信息 46 | certificatesManager.putMerchant(merchantId, new WechatPay2Credentials(merchantId, 47 | new PrivateKeySigner(merchantSerialNumber, merchantPrivateKey)), 48 | apiV3Key.getBytes(StandardCharsets.UTF_8)); 49 | // 从证书管理器中获取verifier 50 | certificateVerifier = certificatesManager.getVerifier(merchantId); 51 | // 创建公钥验签器 52 | PublicKey wechatPayPublicKey = PemUtil.loadPublicKey(wechatPayPublicKeyStr); 53 | publicKeyVerifier = new PublicKeyVerifier(wechatpayPublicKeyId, wechatPayPublicKey); 54 | } 55 | 56 | private NotificationRequest buildNotificationRequest() { 57 | return new NotificationRequest.Builder().withSerialNumber(wechatPaySerial) 58 | .withNonce(nonce) 59 | .withTimestamp(timestamp) 60 | .withSignature(signature) 61 | .withBody(body) 62 | .build(); 63 | } 64 | 65 | @Test 66 | public void handleNotificationWithPublicKeyVerifier() throws Exception { 67 | NotificationRequest request = buildNotificationRequest(); 68 | 69 | // 使用微信支付公钥验签器:适用于已经完成「平台证书」-->「微信支付公钥」迁移的商户以及新申请的商户 70 | NotificationHandler handler = new NotificationHandler(certificateVerifier, apiV3Key.getBytes(StandardCharsets.UTF_8)); 71 | 72 | // 验签和解析请求体 73 | Notification notification = handler.parse(request); 74 | Assert.assertNotNull(notification); 75 | System.out.println(notification.toString()); 76 | } 77 | 78 | @Test 79 | public void handleNotificationWithMixVerifier() throws Exception { 80 | NotificationRequest request = buildNotificationRequest(); 81 | 82 | // 使用混合验签器:适用于正在进行「平台证书」-->「微信支付公钥」迁移的商户 83 | Verifier mixVerifier = new MixVerifier((PublicKeyVerifier) publicKeyVerifier, certificateVerifier); 84 | NotificationHandler handler = new NotificationHandler(mixVerifier, apiV3Key.getBytes(StandardCharsets.UTF_8)); 85 | 86 | // 验签和解析请求体 87 | Notification notification = handler.parse(request); 88 | Assert.assertNotNull(notification); 89 | System.out.println(notification.toString()); 90 | } 91 | 92 | @Test 93 | public void handleNotificationWithCertificateVerifier() throws Exception { 94 | NotificationRequest request = buildNotificationRequest(); 95 | 96 | // 使用平台证书验签器:适用于尚未开始「平台证书」-->「微信支付公钥」迁移的旧商户 97 | NotificationHandler handler = new NotificationHandler(certificateVerifier, apiV3Key.getBytes(StandardCharsets.UTF_8)); 98 | 99 | // 验签和解析请求体 100 | Notification notification = handler.parse(request); 101 | Assert.assertNotNull(notification); 102 | System.out.println(notification.toString()); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/test/java/com/wechat/pay/contrib/apache/httpclient/RsaCryptoTest.java: -------------------------------------------------------------------------------- 1 | package com.wechat.pay.contrib.apache.httpclient; 2 | 3 | import static com.wechat.pay.contrib.apache.httpclient.constant.WechatPayHttpHeaders.WECHAT_PAY_SERIAL; 4 | import static org.apache.http.HttpHeaders.ACCEPT; 5 | import static org.apache.http.HttpStatus.SC_BAD_REQUEST; 6 | import static org.apache.http.HttpStatus.SC_UNAUTHORIZED; 7 | import static org.apache.http.entity.ContentType.APPLICATION_JSON; 8 | import static org.junit.Assert.assertEquals; 9 | import static org.junit.Assert.assertTrue; 10 | 11 | import com.wechat.pay.contrib.apache.httpclient.auth.PrivateKeySigner; 12 | import com.wechat.pay.contrib.apache.httpclient.auth.Verifier; 13 | import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Credentials; 14 | import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Validator; 15 | import com.wechat.pay.contrib.apache.httpclient.cert.CertificatesManager; 16 | import com.wechat.pay.contrib.apache.httpclient.util.PemUtil; 17 | import com.wechat.pay.contrib.apache.httpclient.util.RsaCryptoUtil; 18 | import java.io.ByteArrayInputStream; 19 | import java.io.IOException; 20 | import java.nio.charset.StandardCharsets; 21 | import java.security.PrivateKey; 22 | import java.util.Base64; 23 | import javax.crypto.BadPaddingException; 24 | import javax.crypto.Cipher; 25 | import org.apache.http.HttpEntity; 26 | import org.apache.http.client.methods.CloseableHttpResponse; 27 | import org.apache.http.client.methods.HttpPost; 28 | import org.apache.http.entity.StringEntity; 29 | import org.apache.http.impl.client.CloseableHttpClient; 30 | import org.apache.http.util.EntityUtils; 31 | import org.junit.After; 32 | import org.junit.Before; 33 | import org.junit.Test; 34 | 35 | public class RsaCryptoTest { 36 | 37 | private static final String merchantId = ""; // 商户号 38 | private static final String merchantSerialNumber = ""; // 商户证书序列号 39 | private static final String apiV3Key = ""; // API V3密钥 40 | private static final String privateKey = ""; // 商户API V3私钥 41 | private static final String wechatPaySerial = ""; // 平台证书序列号 42 | 43 | private static final String certForEncrypt = "-----BEGIN CERTIFICATE-----\n" + 44 | "MIIC4jCCAcoCCQCtzUA6NgI3njANBgkqhkiG9w0BAQsFADAzMQswCQYDVQQGEwJD\n" + 45 | "TjERMA8GA1UECAwIc2hhbmdoYWkxETAPBgNVBAcMCHNoYW5naGFpMB4XDTIyMDUw\n" + 46 | "OTIwMjE1NloXDTIzMDUwOTIwMjE1NlowMzELMAkGA1UEBhMCQ04xETAPBgNVBAgM\n" + 47 | "CHNoYW5naGFpMREwDwYDVQQHDAhzaGFuZ2hhaTCCASIwDQYJKoZIhvcNAQEBBQAD\n" + 48 | "ggEPADCCAQoCggEBALMGZq4BnKaX/VXeg9rLkpE7LqQ5uxgIfKMKSvLzCHA3ZfOR\n" + 49 | "p9fl8DtD0/svTUJ0JNv/pFRjfNEmlzqSmAW922yBc4uGkDdqrgHmt4/fqsOXcdLt\n" + 50 | "foL5txTdgYutq/127HOhxwixAlJA0PHk6QMuLmG4GN+dwQHWAtQROufgupXoPe6y\n" + 51 | "B+w4y3GaCLXIoqgHJQDePFy4sYkNAeSlHFvomPz4RAivPemEiTh2AmJ+RTZa3qT7\n" + 52 | "8ZzJNqIM0UKHgcPSsMGTzchC7sV9WIDbQZseflz2ZDJIepJeGq/4TSIXBcyd1yUY\n" + 53 | "GWfQRb/l640C3Izj3nililXWFLCWW5dKBnUGqdMCAwEAATANBgkqhkiG9w0BAQsF\n" + 54 | "AAOCAQEAo4LkShFg+btEjQUxuShD7SQeNh2DDvdCtEQo5IUY7wtgm95fDGgR1QTA\n" + 55 | "9IElN0EpiyvHnPlsjisl8heCL/OnTvrvxJyOp64AiPO6l9j7/nbf9cMHXPOaZODa\n" + 56 | "hS4rdokqUAswRA7wkiK6+hOPw/90+P7EPw6xCNRYTfl2ii5jirisrkc6iOW2nbUd\n" + 57 | "MjFd3gRGBM/ks3oltGbQbTOwntrAb7wy5EYakdZoKix6CQlqZIdbDXJBEgdXPftt\n" + 58 | "80ReqYWTWYyffHCuALMzmFw0fd6gFb/md2oIb13tcKCwiAe1mQmnudRsDH5b5Zps\n" + 59 | "iSuewmex8WO7a4/lc2WWKpSb/8JwNQ==\n" + 60 | "-----END CERTIFICATE-----"; // 用于测试加密功能的证书 61 | private static final String privateKeyForDecrypt = "-----BEGIN PRIVATE KEY-----\n" + 62 | "MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCzBmauAZyml/1V\n" + 63 | "3oPay5KROy6kObsYCHyjCkry8whwN2XzkafX5fA7Q9P7L01CdCTb/6RUY3zRJpc6\n" + 64 | "kpgFvdtsgXOLhpA3aq4B5reP36rDl3HS7X6C+bcU3YGLrav9duxzoccIsQJSQNDx\n" + 65 | "5OkDLi5huBjfncEB1gLUETrn4LqV6D3usgfsOMtxmgi1yKKoByUA3jxcuLGJDQHk\n" + 66 | "pRxb6Jj8+EQIrz3phIk4dgJifkU2Wt6k+/GcyTaiDNFCh4HD0rDBk83IQu7FfViA\n" + 67 | "20GbHn5c9mQySHqSXhqv+E0iFwXMndclGBln0EW/5euNAtyM4954pYpV1hSwlluX\n" + 68 | "SgZ1BqnTAgMBAAECggEAUjhnYhVFb9GwPQbEAfGq796BblVBUylarLqmb2wk/PzE\n" + 69 | "axgDQQnOyjk9m0g/MH0NDKkdPNCwW5JgtDrtbP2kT/IoMfVsOLdbEW538bDkyY29\n" + 70 | "bgU7LEYpyoBs5cyuh+tdb0HmmlxJV6ODEwVx6s8D6EdXzSOzp/c1N1Zuel5g80V/\n" + 71 | "oE5pTb6XBObrCq4ZmMT5y59pSroZDV+RlYZqYtXeCdni+9jzVb+7AM50wqp3D17M\n" + 72 | "P9OZnYVyiKS9GEM68klXt3dCnp5P80WVLLupin3DODGdkU0kDFWZE+Hw8Xype5iP\n" + 73 | "jgJMZwieOsniveAsIjwtRjh0yZ6xJe47G1JOGppK8QKBgQDuM5eyIJhhwkxbQ1ov\n" + 74 | "PbRQbeHrdWUuWuruggg2BMXI3RQH5yELyysOY7rrxSob0L90ww7QLtvaw4GNAFsV\n" + 75 | "/OpXs1bkn3QD3lCh4jskbe816iHnYpLKcdkewcIove3wVAaT5/VYeyW9R1mXZLFr\n" + 76 | "sXAYef1Fys1yg6eM8GuiFGu7yQKBgQDAZtue4T1JNR4IEMXU4wRUmU2itu+7A2W6\n" + 77 | "GskyKaXNvKt0g8ZawDIYEl+B35mRJ29O+8rGKIpqqMEfy+En9/aphouMu9S0cFfS\n" + 78 | "n/H1M1B9cfscqyXnS/Ed1kCC9SlfkRXuJ+HhZQ7Zt95vHwf2ugYeg6GDtghC4JHA\n" + 79 | "NIdntlOOuwKBgQCi8IvN/1n9VUmiDBp+wji7485soGtMIEkgSbaQLQeWdRQkq8gB\n" + 80 | "J0MWnsXYTZCWYl704hEZ+1PM+3t9Fkc4bT9oKndAAIr9sm95rSVDsCe3u6bhfp5m\n" + 81 | "+SXKUkQcVn+SrAer2ToNAoA4T7xLQUfUIRZKx/embCnJMaHFWRhnUIy5cQKBgQCG\n" + 82 | "tHz3E8OQybuo8fVQQ1D42gxc66+UQ6CpV6+di0Mmc/2mqcvqJb3s1JBBoYcm9XEc\n" + 83 | "33Tsn92pJ1VvKZMOJLFxp110vt0BJ9aVBJ6mibLE4VRqkfkLo0PBHAw2o+a/nhi4\n" + 84 | "kPu4jsSC8hStwBAXUc6O9qHSUVQfXpMs+poCpsiBmQKBgQDO+B/xX6V6GQIrPgiF\n" + 85 | "nKpSi566ouXcNxiMIb8w7nu4r/0mJ91roVD0N1TyCOVKTrY/R/4KsQV4pp2bQfV7\n" + 86 | "3tYnrSVgBhPHfWkWQG+7sUXWRR5/c8jszKM+no/bsxmqsAJdK2ih/crHD7XrGgXL\n" + 87 | "XWU1WCYDnWGKm3byXlLY1tFO/Q==\n" + 88 | "-----END PRIVATE KEY-----"; // 用于测试解密功能的私钥 89 | 90 | private CloseableHttpClient httpClient; 91 | private CertificatesManager certificatesManager; 92 | private Verifier verifier; 93 | 94 | @Before 95 | public void setup() throws Exception { 96 | PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(privateKey); 97 | // 获取证书管理器实例 98 | certificatesManager = CertificatesManager.getInstance(); 99 | // 向证书管理器增加需要自动更新平台证书的商户信息 100 | certificatesManager.putMerchant(merchantId, new WechatPay2Credentials(merchantId, 101 | new PrivateKeySigner(merchantSerialNumber, merchantPrivateKey)), 102 | apiV3Key.getBytes(StandardCharsets.UTF_8)); 103 | // 从证书管理器中获取verifier 104 | verifier = certificatesManager.getVerifier(merchantId); 105 | httpClient = WechatPayHttpClientBuilder.create() 106 | .withMerchant(merchantId, merchantSerialNumber, merchantPrivateKey) 107 | .withValidator(new WechatPay2Validator(certificatesManager.getVerifier(merchantId))) 108 | .build(); 109 | } 110 | 111 | @After 112 | public void after() throws IOException { 113 | httpClient.close(); 114 | } 115 | 116 | @Test 117 | public void encryptTest() throws Exception { 118 | String text = "helloworld"; 119 | String ciphertext = RsaCryptoUtil.encryptOAEP(text, verifier.getValidPublicKey()); 120 | System.out.println("ciphertext: " + ciphertext); 121 | } 122 | 123 | @Test 124 | public void encryptWithPkcs1TransformationTest() throws Exception { 125 | String text = "helloworld"; 126 | String transformation = "RSA/ECB/PKCS1Padding"; 127 | String encryptedText = RsaCryptoUtil.encrypt(text, PemUtil.loadCertificate(new ByteArrayInputStream(certForEncrypt.getBytes())), transformation); 128 | //utilize the standard lib to verify the correctness of the encrypted result. 129 | Cipher cipher = Cipher.getInstance(transformation); 130 | cipher.init(Cipher.DECRYPT_MODE, PemUtil.loadPrivateKey(privateKeyForDecrypt)); 131 | String secretText = new String((cipher.doFinal(Base64.getDecoder().decode(encryptedText)))); 132 | assert(text.equals(secretText)); 133 | } 134 | 135 | @Test 136 | public void decryptWithPkcs1TransformationTest() throws BadPaddingException { 137 | String encryptedText = "lmkkdBz5CH4Zk6KIEzbyenf+WtKe8nuU9j+t8HonOm4v1OfLRiYhvdcequOSuaz5vjdpX434XjV9Q5LGC8aOC" + 138 | "DZs/8LoyR3m/6JpYa0nkGOh6Le2JvSPNXlSq9HUEoElBJD5KsxbsRoif0kuioBGSKvKB0xwIvVtn+S0H2GYya7TC1L/ddhGhI/yx" + 139 | "ZgS/TI/Ppej3OzNmu0xA5RjpDR4rGAUrLvV7y/aM4mCN6WOaO6YsAnlGoSbK+P1sepeb0sCaJMClqbLE0Eoz2ve9FQ30w1Vgi5F0" + 140 | "2rpDwcZO8EXAkub0L12BN4QWBNK8FaKlS4UZPAGAwutLK6Gylig54Quig=="; 141 | String transformation = "RSA/ECB/PKCS1Padding"; 142 | assertEquals("helloworld", RsaCryptoUtil.decrypt(encryptedText, PemUtil.loadPrivateKey(privateKeyForDecrypt), transformation)); 143 | } 144 | 145 | @Test 146 | public void encryptWithOaepTransformationTest() throws Exception { 147 | String text = "helloworld"; 148 | String transformation = "RSA/ECB/OAEPWithSHA-1AndMGF1Padding"; 149 | String encryptedText = RsaCryptoUtil.encrypt(text, PemUtil.loadCertificate(new ByteArrayInputStream(certForEncrypt.getBytes())), transformation); 150 | //utilize the standard lib to verify the correctness of the encrypted result. 151 | Cipher cipher = Cipher.getInstance(transformation); 152 | cipher.init(Cipher.DECRYPT_MODE, PemUtil.loadPrivateKey(privateKeyForDecrypt)); 153 | String secretText = new String((cipher.doFinal(Base64.getDecoder().decode(encryptedText)))); 154 | assert(text.equals(secretText)); 155 | } 156 | 157 | @Test 158 | public void decryptWithOaepTransformationTest() throws BadPaddingException { 159 | String encryptedText = "FJ8/0ubyxnMZ0GN2YEUgJgDVPCwMrsTKuLFxycI3jvOAcVTDEEermn2F7+cUtmCYvD2TkHUMHvWeJB6/nSPBD" + 160 | "eGuxA4bCr584h2w9bRvVrwtQlnv1HpF2WRdGAuPcgrQcZvMpiH2ysxgPrGPMs9WOr8etxf1FifI0DkMb6w7wl2BDPPK+RfRdZq7T" + 161 | "9KBtH2IllVTLUbRSqDGIctgIxB7RMqd3s/eK0p2Qjui8AVgP4j5Spq6JjITgKn0VDOO4JwzU8Zl++BwveoJMkTN150XF5ot+ruZv" + 162 | "lNgjP1Hez0/rFxY7gQvxrSDwgL5A9up6JRI741psfs/3HrzBJOBdvO73A=="; 163 | String transformation = "RSA/ECB/OAEPWithSHA-1AndMGF1Padding"; 164 | assertEquals("helloworld", RsaCryptoUtil.decrypt(encryptedText, PemUtil.loadPrivateKey(privateKeyForDecrypt), transformation)); 165 | } 166 | 167 | @Test 168 | public void postEncryptDataTest() throws Exception { 169 | HttpPost httpPost = new HttpPost("https://api.mch.weixin.qq.com/v3/smartguide/guides"); 170 | 171 | String text = "helloworld"; 172 | String ciphertext = RsaCryptoUtil.encryptOAEP(text, verifier.getValidPublicKey()); 173 | 174 | String data = "{\n" 175 | + " \"store_id\" : 1234,\n" 176 | + " \"corpid\" : \"1234567890\",\n" 177 | + " \"name\" : \"" + ciphertext + "\",\n" 178 | + " \"mobile\" : \"" + ciphertext + "\",\n" 179 | + " \"qr_code\" : \"https://open.work.weixin.qq.com/wwopen/userQRCode?vcode=xxx\",\n" 180 | + " \"sub_mchid\" : \"1234567890\",\n" 181 | + " \"avatar\" : \"logo\",\n" 182 | + " \"userid\" : \"robert\"\n" 183 | + "}"; 184 | StringEntity reqEntity = new StringEntity(data, APPLICATION_JSON); 185 | httpPost.setEntity(reqEntity); 186 | httpPost.addHeader(ACCEPT, APPLICATION_JSON.toString()); 187 | httpPost.addHeader(WECHAT_PAY_SERIAL, wechatPaySerial); 188 | 189 | CloseableHttpResponse response = httpClient.execute(httpPost); 190 | assertTrue(response.getStatusLine().getStatusCode() != SC_UNAUTHORIZED); 191 | assertTrue(response.getStatusLine().getStatusCode() != SC_BAD_REQUEST); 192 | try { 193 | HttpEntity entity = response.getEntity(); 194 | // do something useful with the response body 195 | // and ensure it is fully consumed 196 | EntityUtils.consume(entity); 197 | } finally { 198 | response.close(); 199 | } 200 | } 201 | } 202 | --------------------------------------------------------------------------------