|
| 1 | +package cn.binarywang.wx.miniapp.api.impl; |
| 2 | + |
| 3 | +import static org.testng.Assert.assertFalse; |
| 4 | +import static org.testng.Assert.assertTrue; |
| 5 | + |
| 6 | +import java.nio.charset.StandardCharsets; |
| 7 | +import java.security.KeyPair; |
| 8 | +import java.security.KeyPairGenerator; |
| 9 | +import java.security.Signature; |
| 10 | +import java.security.interfaces.RSAPrivateKey; |
| 11 | +import java.security.interfaces.RSAPublicKey; |
| 12 | +import java.security.spec.MGF1ParameterSpec; |
| 13 | +import java.security.spec.PSSParameterSpec; |
| 14 | +import java.util.Base64; |
| 15 | +import org.testng.annotations.Test; |
| 16 | + |
| 17 | +/** |
| 18 | + * 验证同城配送 API 签名 payload 格式的单元测试。 |
| 19 | + * |
| 20 | + * <p>根据微信官方文档,待签名串格式为:<br> |
| 21 | + * {@code urlpath\nappid\ntimestamp\npostdata}<br> |
| 22 | + * 字段之间使用换行符 {@code \n} 分隔,末尾无额外回车符。 |
| 23 | + * |
| 24 | + * @author GitHub Copilot |
| 25 | + * @see <a |
| 26 | + * href="https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/getting_started/api_signature.html"> |
| 27 | + * 微信服务端API签名指南</a> |
| 28 | + */ |
| 29 | +public class WxMaSignaturePayloadTest { |
| 30 | + |
| 31 | + /** |
| 32 | + * 验证正确的签名 payload 格式(不含 rsaKeySn)可以通过签名验证, |
| 33 | + * 即格式为:urlpath\nappid\ntimestamp\npostdata |
| 34 | + */ |
| 35 | + @Test |
| 36 | + public void testCorrectSignaturePayloadFormat() throws Exception { |
| 37 | + KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA"); |
| 38 | + keyGen.initialize(2048); |
| 39 | + KeyPair keyPair = keyGen.generateKeyPair(); |
| 40 | + RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate(); |
| 41 | + RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); |
| 42 | + |
| 43 | + String urlPath = "https://api.weixin.qq.com/cgi-bin/express/intracity/createstore"; |
| 44 | + String appId = "wx1234567890abcdef"; |
| 45 | + long timestamp = 1700000000L; |
| 46 | + String requestJson = "{\"iv\":\"abc\",\"data\":\"xyz\",\"authtag\":\"tag\"}"; |
| 47 | + |
| 48 | + // 正确格式:urlpath\nappid\ntimestamp\npostdata(不含 rsaKeySn) |
| 49 | + String correctPayload = urlPath + "\n" + appId + "\n" + timestamp + "\n" + requestJson; |
| 50 | + byte[] dataBuffer = correctPayload.getBytes(StandardCharsets.UTF_8); |
| 51 | + |
| 52 | + Signature signer = Signature.getInstance("RSASSA-PSS"); |
| 53 | + PSSParameterSpec pssSpec = new PSSParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 32, 1); |
| 54 | + signer.setParameter(pssSpec); |
| 55 | + signer.initSign(privateKey); |
| 56 | + signer.update(dataBuffer); |
| 57 | + byte[] sigBytes = signer.sign(); |
| 58 | + String signatureStr = Base64.getEncoder().encodeToString(sigBytes); |
| 59 | + |
| 60 | + // 使用公钥验证签名 |
| 61 | + Signature verifier = Signature.getInstance("RSASSA-PSS"); |
| 62 | + verifier.setParameter(pssSpec); |
| 63 | + verifier.initVerify(publicKey); |
| 64 | + verifier.update(dataBuffer); |
| 65 | + assertTrue(verifier.verify(Base64.getDecoder().decode(signatureStr)), |
| 66 | + "正确格式的签名应该能通过验证"); |
| 67 | + } |
| 68 | + |
| 69 | + /** |
| 70 | + * 验证错误的签名 payload(含 rsaKeySn)签名后,用正确 payload 验证会失败。 |
| 71 | + * 这证明了原来代码中将 rsaKeySn 加入 payload 是错误的。 |
| 72 | + */ |
| 73 | + @Test |
| 74 | + public void testIncorrectPayloadWithRsaKeySnFails() throws Exception { |
| 75 | + KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA"); |
| 76 | + keyGen.initialize(2048); |
| 77 | + KeyPair keyPair = keyGen.generateKeyPair(); |
| 78 | + RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate(); |
| 79 | + RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); |
| 80 | + |
| 81 | + String urlPath = "https://api.weixin.qq.com/cgi-bin/express/intracity/createstore"; |
| 82 | + String appId = "wx1234567890abcdef"; |
| 83 | + long timestamp = 1700000000L; |
| 84 | + String rsaKeySn = "some_serial_number"; |
| 85 | + String requestJson = "{\"iv\":\"abc\",\"data\":\"xyz\",\"authtag\":\"tag\"}"; |
| 86 | + |
| 87 | + // 错误格式:payload 中包含了 rsaKeySn(修复前的代码逻辑) |
| 88 | + String incorrectPayload = urlPath + "\n" + appId + "\n" + timestamp + "\n" + rsaKeySn + "\n" + requestJson; |
| 89 | + byte[] incorrectData = incorrectPayload.getBytes(StandardCharsets.UTF_8); |
| 90 | + |
| 91 | + Signature signer = Signature.getInstance("RSASSA-PSS"); |
| 92 | + PSSParameterSpec pssSpec = new PSSParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 32, 1); |
| 93 | + signer.setParameter(pssSpec); |
| 94 | + signer.initSign(privateKey); |
| 95 | + signer.update(incorrectData); |
| 96 | + byte[] sigBytes = signer.sign(); |
| 97 | + String signatureStr = Base64.getEncoder().encodeToString(sigBytes); |
| 98 | + |
| 99 | + // 用正确格式的 payload 去验证签名,应该失败 |
| 100 | + String correctPayload = urlPath + "\n" + appId + "\n" + timestamp + "\n" + requestJson; |
| 101 | + byte[] correctData = correctPayload.getBytes(StandardCharsets.UTF_8); |
| 102 | + |
| 103 | + Signature verifier = Signature.getInstance("RSASSA-PSS"); |
| 104 | + verifier.setParameter(pssSpec); |
| 105 | + verifier.initVerify(publicKey); |
| 106 | + verifier.update(correctData); |
| 107 | + |
| 108 | + boolean verified = verifier.verify(Base64.getDecoder().decode(signatureStr)); |
| 109 | + assertFalse(verified, "用错误 payload 生成的签名不应该通过正确 payload 的验证," |
| 110 | + + "说明 rsaKeySn 不应该包含在签名 payload 中"); |
| 111 | + } |
| 112 | +} |
0 commit comments