feat(apple): 新增服务器通知续订与JWT解析能力

- 支持解析Apple签名JWT并提取交易信息
- 新增processRenewNotification处理续订通知
- 添加测试用JWT生成、解析及发送重试记录示例
- 移除废弃ApplePayUtil,统一走新验证逻辑
This commit is contained in:
2025-12-15 14:56:38 +08:00
parent c1dd4faf0e
commit a70c1f4049
12 changed files with 528 additions and 91 deletions

View File

@@ -0,0 +1,88 @@
package com.yolo.keyborad;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.io.FileInputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.PrivateKey;
import java.security.interfaces.RSAPrivateKey;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.NoSuchAlgorithmException;
import java.io.FileNotFoundException;
import java.security.PublicKey;
import java.io.IOException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
public class AppStoreJWT {
// Apple JWT 的配置参数
private static final String ISSUER_ID = "178b442e-b7be-4526-bd13-ab293d019df0";
private static final String KEY_ID = "Y7TF7BV74G";
private static final String PRIVATE_KEY_PATH = "/Users/ziin/Desktop/keyborad-backend/src/main/resources/SubscriptionKey_Y7TF7BV74G.p8";
// 生成 JWT
public static String generateJWT() throws Exception {
// 读取私钥
PrivateKey privateKey = loadPrivateKey(PRIVATE_KEY_PATH);
// 当前时间
long now = System.currentTimeMillis();
Date issuedAt = new Date(now);
Date expiration = new Date(now + 30 * 60 * 1000); // 过期时间,通常是 20 分钟
Map<String, Object> headerMap = new HashMap<>();
headerMap.put("type", "JWT");
headerMap.put("kid", KEY_ID);
// 使用私钥签名生成 JWT
return Jwts.builder()
.setIssuer(ISSUER_ID)
.setAudience("appstoreconnect-v1")
.setIssuedAt(issuedAt)
.setExpiration(expiration)
.claim("bid", "com.loveKey.nyx") // 使用 claim() 方法添加自定义字段
.setHeader(headerMap)
.signWith(privateKey, SignatureAlgorithm.ES256) // ES256 签名算法
.compact();
}
// 加载私钥
static PrivateKey loadPrivateKey(String privateKeyPath) throws Exception {
try {
// 读取 p8 文件内容
String privateKeyContent = new String(Files.readAllBytes(Paths.get(privateKeyPath)));
// 移除 PEM 格式的头部和尾部标记,以及换行符
privateKeyContent = privateKeyContent
.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replaceAll("\\s", ""); // 移除所有空白字符(包括换行符)
// Base64 解码
byte[] encoded = Base64.getDecoder().decode(privateKeyContent);
// 生成私钥
KeyFactory keyFactory = KeyFactory.getInstance("EC");
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(encoded);
return keyFactory.generatePrivate(keySpec);
} catch (IOException | NoSuchAlgorithmException | InvalidKeySpecException e) {
throw new Exception("加载私钥失败", e);
}
}
// 调用生成的 JWT
public static void main(String[] args) {
try {
String jwt = generateJWT();
System.out.println("生成的 JWT: " + jwt);
} catch (Exception e) {
e.printStackTrace();
}
}
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,35 @@
package com.yolo.keyborad;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONException;
import cn.hutool.json.JSONObject;
public class SendAttemptsParser {
public static void main(String[] args) throws JSONException {
String jsonResponse = "{\n" +
" \"signedPayload\": \"eyJhbGciOiJFUzI1NiIsIng1YyI6WyJNSUlFTVRDQ0E3YWdBd0lCQWdJUVI4S0h6ZG41NTRaL1VvcmFkTng5dHpBS0JnZ3Foa2pPUFFRREF6QjFNVVF3UWdZRFZRUURERHRCY0hCc1pTQlhiM0pzWkhkcFpHVWdSR1YyWld4dmNHVnlJRkpsYkdGMGFXOXVjeUJEWlhKMGFXWnBZMkYwYVc5dUlFRjFkR2h2Y21sMGVURUxNQWtHQTFVRUN3d0NSell4RXpBUkJnTlZCQW9NQ2tGd2NHeGxJRWx1WXk0eEN6QUpCZ05WQkFZVEFsVlRNQjRYRFRJMU1Ea3hPVEU1TkRRMU1Wb1hEVEkzTVRBeE16RTNORGN5TTFvd2daSXhRREErQmdOVkJBTU1OMUJ5YjJRZ1JVTkRJRTFoWXlCQmNIQWdVM1J2Y21VZ1lXNWtJR2xVZFc1bGN5QlRkRzl5WlNCU1pXTmxhWEIwSUZOcFoyNXBibWN4TERBcUJnTlZCQXNNSTBGd2NHeGxJRmR2Y214a2QybGtaU0JFWlhabGJHOXdaWElnVW1Wc1lYUnBiMjV6TVJNd0VRWURWUVFLREFwQmNIQnNaU0JKYm1NdU1Rc3dDUVlEVlFRR0V3SlZVekJaTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEEwSUFCTm5WdmhjdjdpVCs3RXg1dEJNQmdyUXNwSHpJc1hSaTBZeGZlazdsdjh3RW1qL2JIaVd0TndKcWMyQm9IenNRaUVqUDdLRklJS2c0WTh5MC9ueW51QW1qZ2dJSU1JSUNCREFNQmdOVkhSTUJBZjhFQWpBQU1COEdBMVVkSXdRWU1CYUFGRDh2bENOUjAxREptaWc5N2JCODVjK2xrR0taTUhBR0NDc0dBUVVGQndFQkJHUXdZakF0QmdnckJnRUZCUWN3QW9ZaGFIUjBjRG92TDJObGNuUnpMbUZ3Y0d4bExtTnZiUzkzZDJSeVp6WXVaR1Z5TURFR0NDc0dBUVVGQnpBQmhpVm9kSFJ3T2k4dmIyTnpjQzVoY0hCc1pTNWpiMjB2YjJOemNEQXpMWGQzWkhKbk5qQXlNSUlCSGdZRFZSMGdCSUlCRlRDQ0FSRXdnZ0VOQmdvcWhraUc5Mk5rQlFZQk1JSCtNSUhEQmdnckJnRUZCUWNDQWpDQnRneUJzMUpsYkdsaGJtTmxJRzl1SUhSb2FYTWdZMlZ5ZEdsbWFXTmhkR1VnWW5rZ1lXNTVJSEJoY25SNUlHRnpjM1Z0WlhNZ1lXTmpaWEIwWVc1alpTQnZaaUIwYUdVZ2RHaGxiaUJoY0hCc2FXTmhZbXhsSUhOMFlXNWtZWEprSUhSbGNtMXpJR0Z1WkNCamIyNWthWFJwYjI1eklHOW1JSFZ6WlN3Z1kyVnlkR2xtYVdOaGRHVWdjRzlzYVdONUlHRnVaQ0JqWlhKMGFXWnBZMkYwYVc5dUlIQnlZV04wYVdObElITjBZWFJsYldWdWRITXVNRFlHQ0NzR0FRVUZCd0lCRmlwb2RIUndPaTh2ZDNkM0xtRndjR3hsTG1OdmJTOWpaWEowYVdacFkyRjBaV0YxZEdodmNtbDBlUzh3SFFZRFZSME9CQllFRklGaW9HNHdNTVZBMWt1OXpKbUdOUEFWbjNlcU1BNEdBMVVkRHdFQi93UUVBd0lIZ0RBUUJnb3Foa2lHOTJOa0Jnc0JCQUlGQURBS0JnZ3Foa2pPUFFRREF3TnBBREJtQWpFQStxWG5SRUM3aFhJV1ZMc0x4em5qUnBJelBmN1ZIejlWL0NUbTgrTEpsclFlcG5tY1B2R0xOY1g2WFBubGNnTEFBakVBNUlqTlpLZ2c1cFE3OWtuRjRJYlRYZEt2OHZ1dElETVhEbWpQVlQzZEd2RnRzR1J3WE95d1Iya1pDZFNyZmVvdCIsIk1JSURGakNDQXB5Z0F3SUJBZ0lVSXNHaFJ3cDBjMm52VTRZU3ljYWZQVGp6Yk5jd0NnWUlLb1pJemowRUF3TXdaekViTUJrR0ExVUVBd3dTUVhCd2JHVWdVbTl2ZENCRFFTQXRJRWN6TVNZd0pBWURWUVFMREIxQmNIQnNaU0JEWlhKMGFXWnBZMkYwYVc5dUlFRjFkR2h2Y21sMGVURVRNQkVHQTFVRUNnd0tRWEJ3YkdVZ1NXNWpMakVMTUFrR0ExVUVCaE1DVlZNd0hoY05NakV3TXpFM01qQXpOekV3V2hjTk16WXdNekU1TURBd01EQXdXakIxTVVRd1FnWURWUVFERER0QmNIQnNaU0JYYjNKc1pIZHBaR1VnUkdWMlpXeHZjR1Z5SUZKbGJHRjBhVzl1Y3lCRFpYSjBhV1pwWTJGMGFXOXVJRUYxZEdodmNtbDBlVEVMTUFrR0ExVUVDd3dDUnpZeEV6QVJCZ05WQkFvTUNrRndjR3hsSUVsdVl5NHhDekFKQmdOVkJBWVRBbFZUTUhZd0VBWUhLb1pJemowQ0FRWUZLNEVFQUNJRFlnQUVic1FLQzk0UHJsV21aWG5YZ3R4emRWSkw4VDBTR1luZ0RSR3BuZ24zTjZQVDhKTUViN0ZEaTRiQm1QaENuWjMvc3E2UEYvY0djS1hXc0w1dk90ZVJoeUo0NXgzQVNQN2NPQithYW85MGZjcHhTdi9FWkZibmlBYk5nWkdoSWhwSW80SDZNSUgzTUJJR0ExVWRFd0VCL3dRSU1BWUJBZjhDQVFBd0h3WURWUjBqQkJnd0ZvQVV1N0Rlb1ZnemlKcWtpcG5ldnIzcnI5ckxKS3N3UmdZSUt3WUJCUVVIQVFFRU9qQTRNRFlHQ0NzR0FRVUZCekFCaGlwb2RIUndPaTh2YjJOemNDNWhjSEJzWlM1amIyMHZiMk56Y0RBekxXRndjR3hsY205dmRHTmhaek13TndZRFZSMGZCREF3TGpBc29DcWdLSVltYUhSMGNEb3ZMMk55YkM1aGNIQnNaUzVqYjIwdllYQndiR1Z5YjI5MFkyRm5NeTVqY213d0hRWURWUjBPQkJZRUZEOHZsQ05SMDFESm1pZzk3YkI4NWMrbGtHS1pNQTRHQTFVZER3RUIvd1FFQXdJQkJqQVFCZ29xaGtpRzkyTmtCZ0lCQkFJRkFEQUtCZ2dxaGtqT1BRUURBd05vQURCbEFqQkFYaFNxNUl5S29nTUNQdHc0OTBCYUI2NzdDYUVHSlh1ZlFCL0VxWkdkNkNTamlDdE9udU1UYlhWWG14eGN4ZmtDTVFEVFNQeGFyWlh2TnJreFUzVGtVTUkzM3l6dkZWVlJUNHd4V0pDOTk0T3NkY1o0K1JHTnNZRHlSNWdtZHIwbkRHZz0iLCJNSUlDUXpDQ0FjbWdBd0lCQWdJSUxjWDhpTkxGUzVVd0NnWUlLb1pJemowRUF3TXdaekViTUJrR0ExVUVBd3dTUVhCd2JHVWdVbTl2ZENCRFFTQXRJRWN6TVNZd0pBWURWUVFMREIxQmNIQnNaU0JEWlhKMGFXWnBZMkYwYVc5dUlFRjFkR2h2Y21sMGVURVRNQkVHQTFVRUNnd0tRWEJ3YkdVZ1NXNWpMakVMTUFrR0ExVUVCaE1DVlZNd0hoY05NVFF3TkRNd01UZ3hPVEEyV2hjTk16a3dORE13TVRneE9UQTJXakJuTVJzd0dRWURWUVFEREJKQmNIQnNaU0JTYjI5MElFTkJJQzBnUnpNeEpqQWtCZ05WQkFzTUhVRndjR3hsSUVObGNuUnBabWxqWVhScGIyNGdRWFYwYUc5eWFYUjVNUk13RVFZRFZRUUtEQXBCY0hCc1pTQkpibU11TVFzd0NRWURWUVFHRXdKVlV6QjJNQkFHQnlxR1NNNDlBZ0VHQlN1QkJBQWlBMklBQkpqcEx6MUFjcVR0a3lKeWdSTWMzUkNWOGNXalRuSGNGQmJaRHVXbUJTcDNaSHRmVGpqVHV4eEV0WC8xSDdZeVlsM0o2WVJiVHpCUEVWb0EvVmhZREtYMUR5eE5CMGNUZGRxWGw1ZHZNVnp0SzUxN0lEdll1VlRaWHBta09sRUtNYU5DTUVBd0hRWURWUjBPQkJZRUZMdXczcUZZTTRpYXBJcVozcjY5NjYvYXl5U3JNQThHQTFVZEV3RUIvd1FGTUFNQkFmOHdEZ1lEVlIwUEFRSC9CQVFEQWdFR01Bb0dDQ3FHU000OUJBTURBMmdBTUdVQ01RQ0Q2Y0hFRmw0YVhUUVkyZTN2OUd3T0FFWkx1Tit5UmhIRkQvM21lb3locG12T3dnUFVuUFdUeG5TNGF0K3FJeFVDTUcxbWloREsxQTNVVDgyTlF6NjBpbU9sTTI3amJkb1h0MlFmeUZNbStZaGlkRGtMRjF2TFVhZ002QmdENTZLeUtBPT0iXX0.eyJub3RpZmljYXRpb25UeXBlIjoiVEVTVCIsIm5vdGlmaWNhdGlvblVVSUQiOiIxNjg5YjA5OS00MTMzLTQzYjctOGFiYi05YzdmMDNlZDkwN2YiLCJkYXRhIjp7ImJ1bmRsZUlkIjoiY29tLmxvdmVLZXkubnl4IiwiZW52aXJvbm1lbnQiOiJTYW5kYm94In0sInZlcnNpb24iOiIyLjAiLCJzaWduZWREYXRlIjoxNzY1NTM5Nzk3MjA2fQ.UhmtfQ5Bk2aqvGOKPdOBLQDpssZ7aT4SgnlR29talFerwtfXjBh3b1vqsc565a8U4g8NJNwOt_dkCqtMjUO-Vw\",\n" +
" \"firstSendAttemptResult\": \"SUCCESS\",\n" +
" \"sendAttempts\": [\n" +
" {\n" +
" \"attemptDate\": 1765539797242,\n" +
" \"sendAttemptResult\": \"SUCCESS\"\n" +
" }\n" +
" ]\n" +
"}"; // 将响应字符串传递进来
JSONObject jsonObject = new JSONObject(jsonResponse);
// 获取 sendAttempts 数组
JSONArray sendAttempts = jsonObject.getJSONArray("sendAttempts");
// 解析每个 sendAttempt
for (int i = 0; i < sendAttempts.size(); i++) {
JSONObject attempt = sendAttempts.getJSONObject(i);
long attemptDate = attempt.getLong("attemptDate"); // 发送尝试时间戳
String sendAttemptResult = attempt.getStr("sendAttemptResult"); // 发送尝试结果
System.out.println("Attempt Date: " + attemptDate);
System.out.println("Send Attempt Result: " + sendAttemptResult);
}
}
}

View File

@@ -0,0 +1,32 @@
package com.yolo.keyborad;
import com.apple.itunes.storekit.signature.JWSSignatureCreator;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.UUID;
/*
* @author: ziin
* @date: 2025/12/12 18:56
*/
public class test {
public static void main(String[] args) throws Exception {
try {
String jwt = AppStoreJWT.generateJWT();
URL url = new URL("https://api.storekit.itunes.apple.com/inApps/v1/subscriptions"); // 你需要访问的 Apple API URL
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setRequestProperty("Authorization", "Bearer " + jwt); // 设置 JWT 为 Bearer Token
// 处理响应
int responseCode = connection.getResponseCode();
System.out.println("Response Code: " + responseCode);
} catch (Exception e) {
e.printStackTrace();
}
}
}