diff --git a/.gitignore b/.gitignore index 33f4513..b4d5d24 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,4 @@ build/ .vscode/ /CLAUDE.md /AGENTS.md +/src/test/ diff --git a/src/main/java/com/yolo/keyborad/config/SaTokenConfigure.java b/src/main/java/com/yolo/keyborad/config/SaTokenConfigure.java index c79bcd8..a91191b 100644 --- a/src/main/java/com/yolo/keyborad/config/SaTokenConfigure.java +++ b/src/main/java/com/yolo/keyborad/config/SaTokenConfigure.java @@ -81,7 +81,6 @@ public class SaTokenConfigure implements WebMvcConfigurer { "/character/listByTagWithNotLogin", "/character/detailWithNotLogin", "/character/addUserCharacter", - "/api/apple/validate-receipt", "/character/list", "/user/resetPassWord", "/chat/talk", @@ -100,7 +99,8 @@ public class SaTokenConfigure implements WebMvcConfigurer { "/products/subscription/list", "/purchase/handle", "/apple/notification", - "/apple/receipt" + "/apple/receipt", + "/apple/validate-receipt" }; } diff --git a/src/main/java/com/yolo/keyborad/controller/AppleReceiptController.java b/src/main/java/com/yolo/keyborad/controller/AppleReceiptController.java index e964350..999d8ff 100644 --- a/src/main/java/com/yolo/keyborad/controller/AppleReceiptController.java +++ b/src/main/java/com/yolo/keyborad/controller/AppleReceiptController.java @@ -6,9 +6,11 @@ import com.yolo.keyborad.common.ErrorCode; import com.yolo.keyborad.common.ResultUtils; import com.yolo.keyborad.exception.BusinessException; import com.yolo.keyborad.model.dto.AppleReceiptValidationResult; +import com.yolo.keyborad.model.dto.AppleServerNotification; import com.yolo.keyborad.service.ApplePurchaseService; import com.yolo.keyborad.service.AppleReceiptService; import jakarta.servlet.http.HttpServletRequest; +import com.yolo.keyborad.utils.JwtParser; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -16,6 +18,8 @@ import org.springframework.web.bind.annotation.RestController; import lombok.extern.slf4j.Slf4j; import java.util.Map; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; @RestController @RequestMapping("/apple") @@ -50,10 +54,79 @@ public class AppleReceiptController { } @PostMapping("/notification") - public BaseResponse receiveNotification(@RequestBody Map body, HttpServletRequest request) { - log.info(request.getQueryString()); - log.info("Apple server notification: {}", body); + public BaseResponse receiveNotification(@RequestBody Map body, HttpServletRequest request) { + String signedPayload = body.get("signedPayload"); + log.warn(body.toString()); + if (signedPayload == null || signedPayload.isBlank()) { + throw new BusinessException(ErrorCode.PARAMS_ERROR, "signedPayload 不能为空"); + } + AppleServerNotification notification = decodeSignedPayload(signedPayload); + log.info("Apple server notification decoded: {}, query: {}", notification, request.getQueryString()); + if (notification != null && notification.getNotificationType() != null + && notification.getNotificationType().toUpperCase().contains("RENEW")) { + applePurchaseService.processRenewNotification(notification); + } return ResultUtils.success(Boolean.TRUE); } + + + private AppleServerNotification decodeSignedPayload(String signedPayload) { + try { + JsonNode root = JwtParser.parsePayload(signedPayload); + AppleServerNotification notification = new AppleServerNotification(); + notification.setNotificationType(text(root, "notificationType", "notification_type")); + JsonNode data = root.get("data"); + if (data != null && !data.isNull()) { + notification.setEnvironment(text(data, "environment")); + notification.setProductId(text(data, "productId", "product_id")); + notification.setOriginalTransactionId(text(data, "originalTransactionId", "original_transaction_id")); + notification.setSignedTransactionInfo(text(data, "signedTransactionInfo", "signed_transaction_info")); + } + if (notification.getSignedTransactionInfo() != null) { + JsonNode txNode = JwtParser.parsePayload(notification.getSignedTransactionInfo()); + notification.setTransactionId(text(txNode, "transactionId", "transaction_id")); + notification.setProductId(firstNonBlank(notification.getProductId(), text(txNode, "productId", "product_id"))); + notification.setOriginalTransactionId(firstNonBlank(notification.getOriginalTransactionId(), text(txNode, "originalTransactionId", "original_transaction_id"))); + notification.setPurchaseDate(epochToIso(txNode, "purchaseDate", "purchase_date")); + notification.setExpiresDate(epochToIso(txNode, "expiresDate", "expires_date")); + } + return notification; + } catch (IllegalArgumentException e) { + throw new BusinessException(ErrorCode.PARAMS_ERROR, e.getMessage()); + } catch (Exception e) { + log.error("Failed to decode signedPayload", e); + throw new BusinessException(ErrorCode.OPERATION_ERROR, "signedPayload 解析失败"); + } + } + + private String text(JsonNode node, String... keys) { + for (String k : keys) { + if (node != null && node.has(k) && !node.get(k).isNull()) { + return node.get(k).asText(); + } + } + return null; + } + + private String firstNonBlank(String a, String b) { + if (a != null && !a.isBlank()) { + return a; + } + return (b != null && !b.isBlank()) ? b : a; + } + + private String epochToIso(JsonNode node, String... keys) { + String val = text(node, keys); + if (val == null) { + return null; + } + try { + long epochMillis = Long.parseLong(val); + return java.time.Instant.ofEpochMilli(epochMillis).toString(); + } catch (NumberFormatException e) { + return val; + } + } + } diff --git a/src/main/java/com/yolo/keyborad/model/dto/AppleServerNotification.java b/src/main/java/com/yolo/keyborad/model/dto/AppleServerNotification.java new file mode 100644 index 0000000..f267c58 --- /dev/null +++ b/src/main/java/com/yolo/keyborad/model/dto/AppleServerNotification.java @@ -0,0 +1,41 @@ +package com.yolo.keyborad.model.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +/** + * Apple 服务器通知(精简字段) + */ +@Data +public class AppleServerNotification { + + @JsonProperty("notification_type") + private String notificationType; + + @JsonProperty("auto_renew_status") + private String autoRenewStatus; + + @JsonProperty("app_account_token") + private String appAccountToken; + + @JsonProperty("original_transaction_id") + private String originalTransactionId; + + @JsonProperty("product_id") + private String productId; + + @JsonProperty("purchase_date") + private String purchaseDate; + + @JsonProperty("expires_date") + private String expiresDate; + + @JsonProperty("environment") + private String environment; + + @JsonProperty("transaction_id") + private String transactionId; + + @JsonProperty("signed_transaction_info") + private String signedTransactionInfo; +} diff --git a/src/main/java/com/yolo/keyborad/service/ApplePurchaseService.java b/src/main/java/com/yolo/keyborad/service/ApplePurchaseService.java index 3380b66..b5598fd 100644 --- a/src/main/java/com/yolo/keyborad/service/ApplePurchaseService.java +++ b/src/main/java/com/yolo/keyborad/service/ApplePurchaseService.java @@ -14,5 +14,11 @@ public interface ApplePurchaseService { * @param validationResult 苹果验签结果 */ void processPurchase(Long userId, AppleReceiptValidationResult validationResult); -} + /** + * 处理苹果服务器续订通知(无收据,仅基于原始交易号和商品信息) + * + * @param notification 通知内容 + */ + void processRenewNotification(com.yolo.keyborad.model.dto.AppleServerNotification notification); +} diff --git a/src/main/java/com/yolo/keyborad/service/impl/ApplePurchaseServiceImpl.java b/src/main/java/com/yolo/keyborad/service/impl/ApplePurchaseServiceImpl.java index 78e7499..8dad823 100644 --- a/src/main/java/com/yolo/keyborad/service/impl/ApplePurchaseServiceImpl.java +++ b/src/main/java/com/yolo/keyborad/service/impl/ApplePurchaseServiceImpl.java @@ -3,6 +3,7 @@ package com.yolo.keyborad.service.impl; import com.yolo.keyborad.common.ErrorCode; import com.yolo.keyborad.exception.BusinessException; import com.yolo.keyborad.model.dto.AppleReceiptValidationResult; +import com.yolo.keyborad.model.dto.AppleServerNotification; import com.yolo.keyborad.model.entity.KeyboardProductItems; import com.yolo.keyborad.model.entity.KeyboardUser; import com.yolo.keyborad.model.entity.KeyboardUserPurchaseRecords; @@ -23,6 +24,7 @@ import java.math.BigDecimal; import java.time.Instant; import java.time.ZoneOffset; import java.time.temporal.ChronoUnit; +import java.time.format.DateTimeParseException; import java.util.Date; import java.util.List; import java.util.Objects; @@ -53,6 +55,7 @@ public class ApplePurchaseServiceImpl implements ApplePurchaseService { @Transactional(rollbackFor = Exception.class) public void processPurchase(Long userId, AppleReceiptValidationResult validationResult) { if (validationResult == null || !validationResult.isValid()) { + log.error("Apple receipt validation failed.{}", validationResult.getReason()); throw new BusinessException(ErrorCode.RECEIPT_INVALID); } String productId = resolveProductId(validationResult.getProductIds()); @@ -85,6 +88,54 @@ public class ApplePurchaseServiceImpl implements ApplePurchaseService { } } + @Override + @Transactional(rollbackFor = Exception.class) + public void processRenewNotification(AppleServerNotification notification) { + if (notification == null || notification.getOriginalTransactionId() == null) { + throw new BusinessException(ErrorCode.PARAMS_ERROR, "缺少原始交易ID"); + } + // 找到用户:原始交易ID可能对应多条记录,取最新的一条 + List records = purchaseRecordsService.lambdaQuery() + .eq(KeyboardUserPurchaseRecords::getOriginalTransactionId, notification.getOriginalTransactionId()) + .orderByDesc(KeyboardUserPurchaseRecords::getId) + .list(); + if (records == null || records.isEmpty()) { + log.warn("Renewal notification without matching purchase record, originalTransactionId={}", notification.getOriginalTransactionId()); + return; + } + KeyboardUserPurchaseRecords record = records.get(0); + + KeyboardProductItems product = productItemsService.getProductEntityByProductId(notification.getProductId()); + if (product == null || !"subscription".equalsIgnoreCase(product.getType())) { + log.warn("Renewal notification ignored, product not subscription or not found. productId={}", notification.getProductId()); + return; + } + + // 写一条续订记录 + KeyboardUserPurchaseRecords renewRecord = new KeyboardUserPurchaseRecords(); + renewRecord.setUserId(record.getUserId()); + renewRecord.setProductId(product.getProductId()); + renewRecord.setPurchaseQuantity(product.getDurationValue()); + renewRecord.setPrice(product.getPrice()); + renewRecord.setCurrency(product.getCurrency()); + renewRecord.setPurchaseTime(toDate(parseInstant(notification.getPurchaseDate()))); + renewRecord.setPurchaseType(product.getType()); + renewRecord.setStatus("PAID"); + renewRecord.setPaymentMethod("APPLE"); + renewRecord.setTransactionId(notification.getTransactionId()); + renewRecord.setOriginalTransactionId(notification.getOriginalTransactionId()); + renewRecord.setProductIds(new String[]{product.getProductId()}); + Instant expiresInstant = parseInstant(notification.getExpiresDate()); + if (expiresInstant != null) { + renewRecord.setExpiresDate(Date.from(expiresInstant)); + } + renewRecord.setEnvironment(notification.getEnvironment()); + renewRecord.setPurchaseDate(toDate(parseInstant(notification.getPurchaseDate()))); + purchaseRecordsService.save(renewRecord); + + extendVip(record.getUserId().longValue(), product, expiresInstant); + } + private void handleSubscription(Long userId, KeyboardProductItems product, AppleReceiptValidationResult validationResult) { KeyboardUser user = userService.getById(userId); if (user == null) { @@ -158,6 +209,27 @@ public class ApplePurchaseServiceImpl implements ApplePurchaseService { return record; } + private void extendVip(Long userId, KeyboardProductItems product, Instant targetExpiry) { + KeyboardUser user = userService.getById(userId); + if (user == null) { + throw new BusinessException(ErrorCode.USER_NOT_FOUND); + } + long durationDays = resolveDurationDays(product); + Instant base = resolveBaseExpiry(user.getVipExpiry()); + Instant newExpiry = targetExpiry != null ? targetExpiry : base.plus(durationDays, ChronoUnit.DAYS); + // 如果目标时间早于当前,则基于当前时间加时长 + if (newExpiry.isBefore(Instant.now())) { + newExpiry = Instant.now().plus(durationDays, ChronoUnit.DAYS); + } + user.setIsVip(true); + user.setVipExpiry(Date.from(newExpiry)); + boolean updated = userService.updateById(user); + if (!updated) { + throw new BusinessException(ErrorCode.OPERATION_ERROR, "更新用户VIP失败"); + } + log.info("Extend VIP by notification, user {} to {}", userId, newExpiry); + } + private String resolveProductId(List productIds) { if (productIds == null || productIds.isEmpty()) { throw new BusinessException(ErrorCode.PARAMS_ERROR, "productId 缺失"); @@ -213,4 +285,20 @@ public class ApplePurchaseServiceImpl implements ApplePurchaseService { return null; } } + + private Instant parseInstant(String iso) { + if (iso == null || iso.isBlank()) { + return null; + } + try { + return Instant.parse(iso); + } catch (DateTimeParseException e) { + log.warn("Failed to parse expiresDate: {}", iso); + return null; + } + } + + private Date toDate(Instant instant) { + return instant == null ? null : Date.from(instant); + } } diff --git a/src/main/java/com/yolo/keyborad/utils/ApplePayUtil.java b/src/main/java/com/yolo/keyborad/utils/ApplePayUtil.java deleted file mode 100644 index f65e2fe..0000000 --- a/src/main/java/com/yolo/keyborad/utils/ApplePayUtil.java +++ /dev/null @@ -1,85 +0,0 @@ -package com.yolo.keyborad.utils; - -import javax.net.ssl.*; -import java.io.BufferedOutputStream; -import java.io.BufferedReader; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.net.URL; -import java.security.cert.CertificateException; -import java.security.cert.X509Certificate; -import java.util.Locale; - - -public class ApplePayUtil { - - private static class TrustAnyTrustManager implements X509TrustManager { - - @Override - public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { - } - - @Override - public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { - } - - @Override - public X509Certificate[] getAcceptedIssuers() { - return new X509Certificate[]{}; - } - } - - private static class TrustAnyHostnameVerifier implements HostnameVerifier { - @Override - public boolean verify(String hostname, SSLSession session) { - return true; - } - } - - private static final String url_sandbox = "https://sandbox.itunes.apple.com/verifyReceipt"; - private static final String url_verify = "https://buy.itunes.apple.com/verifyReceipt"; - - /** - * 苹果服务器验证 - * - * @param receipt 账单 - * @return null 或返回结果 沙盒 https://sandbox.itunes.apple.com/verifyReceipt - * @url 要验证的地址 - */ - public static String buyAppVerify(String receipt, int type) throws Exception { - //环境判断 线上/开发环境用不同的请求链接 - String url = ""; - if (type == 0) { - url = url_sandbox; //沙盒测试 - } else { - url = url_verify; //线上测试 - } - SSLContext sc = SSLContext.getInstance("SSL"); - sc.init(null, new TrustManager[]{new TrustAnyTrustManager()}, new java.security.SecureRandom()); - URL console = new URL(url); - HttpsURLConnection conn = (HttpsURLConnection) console.openConnection(); - conn.setSSLSocketFactory(sc.getSocketFactory()); - conn.setHostnameVerifier(new TrustAnyHostnameVerifier()); - conn.setRequestMethod("POST"); - conn.setRequestProperty("content-type", "text/json"); - conn.setRequestProperty("Proxy-Connection", "Keep-Alive"); - conn.setDoInput(true); - conn.setDoOutput(true); - BufferedOutputStream hurlBufOus = new BufferedOutputStream(conn.getOutputStream()); - //拼成固定的格式传给平台 - String str = String.format(Locale.CHINA, "{\"receipt-data\":\"" + receipt + "\"}"); - hurlBufOus.write(str.getBytes()); - hurlBufOus.flush(); - - InputStream is = conn.getInputStream(); - BufferedReader reader = new BufferedReader(new InputStreamReader(is)); - String line = null; - StringBuffer sb = new StringBuffer(); - while ((line = reader.readLine()) != null) { - sb.append(line); - } - - return sb.toString(); - } - -} \ No newline at end of file diff --git a/src/main/java/com/yolo/keyborad/utils/JwtParser.java b/src/main/java/com/yolo/keyborad/utils/JwtParser.java new file mode 100644 index 0000000..b6fc4d8 --- /dev/null +++ b/src/main/java/com/yolo/keyborad/utils/JwtParser.java @@ -0,0 +1,70 @@ +package com.yolo.keyborad.utils; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jws; +import io.jsonwebtoken.Jwts; +import org.json.JSONArray; +import org.json.JSONObject; + +import java.io.ByteArrayInputStream; +import java.security.PublicKey; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.Base64; + +public class JwtParser { + private static final ObjectMapper objectMapper = new ObjectMapper(); + + /** + * 解析 JWT 并返回 JsonNode + */ + public static JsonNode parsePayload(String signedPayload) throws Exception { + // 从 JWT header 中提取公钥 + PublicKey publicKey = extractPublicKeyFromJWT(signedPayload); + // 解码 JWT(使用公钥验证) + Jws claimsJws = Jwts.parserBuilder() + .setSigningKey(publicKey) + .build() + .parseClaimsJws(signedPayload); + Claims claims = claimsJws.getBody(); + + // 将 Claims 转换为 JsonNode + return objectMapper.valueToTree(claims); + } + + /** + * 从 JWT 的 x5c header 中提取公钥 + */ + private static PublicKey extractPublicKeyFromJWT(String jwt) throws Exception { + // 解析 JWT header(不验证签名) + String[] parts = jwt.split("\\."); + if (parts.length < 2) { + throw new IllegalArgumentException("Invalid JWT format"); + } + + // 解码 header + String headerJson = new String(Base64.getUrlDecoder().decode(parts[0])); // 使用 URL 安全的 Base64 解码 + JSONObject header = new JSONObject(headerJson); + + // 获取 x5c 证书链(第一个证书包含公钥) + JSONArray x5cArray = header.getJSONArray("x5c"); + if (x5cArray.length() == 0) { + throw new IllegalArgumentException("No x5c certificates found in JWT header"); + } + + // 获取第一个证书(Base64 编码,标准格式非URL安全格式) + String certBase64 = x5cArray.getString(0); + + // x5c 中的证书使用标准 Base64 编码(非 URL 安全编码) + byte[] certBytes = Base64.getDecoder().decode(certBase64); + + // 生成 X509 证书并提取公钥 + CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); + X509Certificate cert = (X509Certificate) certFactory.generateCertificate( + new ByteArrayInputStream(certBytes) + ); + return cert.getPublicKey(); + } +} diff --git a/src/test/java/com/yolo/keyborad/AppStoreJWT.java b/src/test/java/com/yolo/keyborad/AppStoreJWT.java new file mode 100644 index 0000000..863f2f4 --- /dev/null +++ b/src/test/java/com/yolo/keyborad/AppStoreJWT.java @@ -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 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(); + } + } +} diff --git a/src/test/java/com/yolo/keyborad/JwtParser.java b/src/test/java/com/yolo/keyborad/JwtParser.java new file mode 100644 index 0000000..aad4da8 --- /dev/null +++ b/src/test/java/com/yolo/keyborad/JwtParser.java @@ -0,0 +1,88 @@ +package com.yolo.keyborad; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jws; +import io.jsonwebtoken.Jwts; +import org.json.JSONArray; +import org.json.JSONObject; + +import java.io.ByteArrayInputStream; +import java.security.PublicKey; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.Base64; + +import static org.bouncycastle.asn1.x509.ObjectDigestInfo.publicKey; + +public class JwtParser { + + public static void main(String[] args) throws Exception { + String signedPayload = "eyJhbGciOiJFUzI1NiIsIng1YyI6WyJNSUlFTVRDQ0E3YWdBd0lCQWdJUVI4S0h6ZG41NTRaL1VvcmFkTng5dHpBS0JnZ3Foa2pPUFFRREF6QjFNVVF3UWdZRFZRUURERHRCY0hCc1pTQlhiM0pzWkhkcFpHVWdSR1YyWld4dmNHVnlJRkpsYkdGMGFXOXVjeUJEWlhKMGFXWnBZMkYwYVc5dUlFRjFkR2h2Y21sMGVURUxNQWtHQTFVRUN3d0NSell4RXpBUkJnTlZCQW9NQ2tGd2NHeGxJRWx1WXk0eEN6QUpCZ05WQkFZVEFsVlRNQjRYRFRJMU1Ea3hPVEU1TkRRMU1Wb1hEVEkzTVRBeE16RTNORGN5TTFvd2daSXhRREErQmdOVkJBTU1OMUJ5YjJRZ1JVTkRJRTFoWXlCQmNIQWdVM1J2Y21VZ1lXNWtJR2xVZFc1bGN5QlRkRzl5WlNCU1pXTmxhWEIwSUZOcFoyNXBibWN4TERBcUJnTlZCQXNNSTBGd2NHeGxJRmR2Y214a2QybGtaU0JFWlhabGJHOXdaWElnVW1Wc1lYUnBiMjV6TVJNd0VRWURWUVFLREFwQmNIQnNaU0JKYm1NdU1Rc3dDUVlEVlFRR0V3SlZVekJaTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEEwSUFCTm5WdmhjdjdpVCs3RXg1dEJNQmdyUXNwSHpJc1hSaTBZeGZlazdsdjh3RW1qL2JIaVd0TndKcWMyQm9IenNRaUVqUDdLRklJS2c0WTh5MC9ueW51QW1qZ2dJSU1JSUNCREFNQmdOVkhSTUJBZjhFQWpBQU1COEdBMVVkSXdRWU1CYUFGRDh2bENOUjAxREptaWc5N2JCODVjK2xrR0taTUhBR0NDc0dBUVVGQndFQkJHUXdZakF0QmdnckJnRUZCUWN3QW9ZaGFIUjBjRG92TDJObGNuUnpMbUZ3Y0d4bExtTnZiUzkzZDJSeVp6WXVaR1Z5TURFR0NDc0dBUVVGQnpBQmhpVm9kSFJ3T2k4dmIyTnpjQzVoY0hCc1pTNWpiMjB2YjJOemNEQXpMWGQzWkhKbk5qQXlNSUlCSGdZRFZSMGdCSUlCRlRDQ0FSRXdnZ0VOQmdvcWhraUc5Mk5rQlFZQk1JSCtNSUhEQmdnckJnRUZCUWNDQWpDQnRneUJzMUpsYkdsaGJtTmxJRzl1SUhSb2FYTWdZMlZ5ZEdsbWFXTmhkR1VnWW5rZ1lXNTVJSEJoY25SNUlHRnpjM1Z0WlhNZ1lXTmpaWEIwWVc1alpTQnZaaUIwYUdVZ2RHaGxiaUJoY0hCc2FXTmhZbXhsSUhOMFlXNWtZWEprSUhSbGNtMXpJR0Z1WkNCamIyNWthWFJwYjI1eklHOW1JSFZ6WlN3Z1kyVnlkR2xtYVdOaGRHVWdjRzlzYVdONUlHRnVaQ0JqWlhKMGFXWnBZMkYwYVc5dUlIQnlZV04wYVdObElITjBZWFJsYldWdWRITXVNRFlHQ0NzR0FRVUZCd0lCRmlwb2RIUndPaTh2ZDNkM0xtRndjR3hsTG1OdmJTOWpaWEowYVdacFkyRjBaV0YxZEdodmNtbDBlUzh3SFFZRFZSME9CQllFRklGaW9HNHdNTVZBMWt1OXpKbUdOUEFWbjNlcU1BNEdBMVVkRHdFQi93UUVBd0lIZ0RBUUJnb3Foa2lHOTJOa0Jnc0JCQUlGQURBS0JnZ3Foa2pPUFFRREF3TnBBREJtQWpFQStxWG5SRUM3aFhJV1ZMc0x4em5qUnBJelBmN1ZIejlWL0NUbTgrTEpsclFlcG5tY1B2R0xOY1g2WFBubGNnTEFBakVBNUlqTlpLZ2c1cFE3OWtuRjRJYlRYZEt2OHZ1dElETVhEbWpQVlQzZEd2RnRzR1J3WE95d1Iya1pDZFNyZmVvdCIsIk1JSURGakNDQXB5Z0F3SUJBZ0lVSXNHaFJ3cDBjMm52VTRZU3ljYWZQVGp6Yk5jd0NnWUlLb1pJemowRUF3TXdaekViTUJrR0ExVUVBd3dTUVhCd2JHVWdVbTl2ZENCRFFTQXRJRWN6TVNZd0pBWURWUVFMREIxQmNIQnNaU0JEWlhKMGFXWnBZMkYwYVc5dUlFRjFkR2h2Y21sMGVURVRNQkVHQTFVRUNnd0tRWEJ3YkdVZ1NXNWpMakVMTUFrR0ExVUVCaE1DVlZNd0hoY05NakV3TXpFM01qQXpOekV3V2hjTk16WXdNekU1TURBd01EQXdXakIxTVVRd1FnWURWUVFERER0QmNIQnNaU0JYYjNKc1pIZHBaR1VnUkdWMlpXeHZjR1Z5SUZKbGJHRjBhVzl1Y3lCRFpYSjBhV1pwWTJGMGFXOXVJRUYxZEdodmNtbDBlVEVMTUFrR0ExVUVDd3dDUnpZeEV6QVJCZ05WQkFvTUNrRndjR3hsSUVsdVl5NHhDekFKQmdOVkJBWVRBbFZUTUhZd0VBWUhLb1pJemowQ0FRWUZLNEVFQUNJRFlnQUVic1FLQzk0UHJsV21aWG5YZ3R4emRWSkw4VDBTR1luZ0RSR3BuZ24zTjZQVDhKTUViN0ZEaTRiQm1QaENuWjMvc3E2UEYvY0djS1hXc0w1dk90ZVJoeUo0NXgzQVNQN2NPQithYW85MGZjcHhTdi9FWkZibmlBYk5nWkdoSWhwSW80SDZNSUgzTUJJR0ExVWRFd0VCL3dRSU1BWUJBZjhDQVFBd0h3WURWUjBqQkJnd0ZvQVV1N0Rlb1ZnemlKcWtpcG5ldnIzcnI5ckxKS3N3UmdZSUt3WUJCUVVIQVFFRU9qQTRNRFlHQ0NzR0FRVUZCekFCaGlwb2RIUndPaTh2YjJOemNDNWhjSEJzWlM1amIyMHZiMk56Y0RBekxXRndjR3hsY205dmRHTmhaek13TndZRFZSMGZCREF3TGpBc29DcWdLSVltYUhSMGNEb3ZMMk55YkM1aGNIQnNaUzVqYjIwdllYQndiR1Z5YjI5MFkyRm5NeTVqY213d0hRWURWUjBPQkJZRUZEOHZsQ05SMDFESm1pZzk3YkI4NWMrbGtHS1pNQTRHQTFVZER3RUIvd1FFQXdJQkJqQVFCZ29xaGtpRzkyTmtCZ0lCQkFJRkFEQUtCZ2dxaGtqT1BRUURBd05vQURCbEFqQkFYaFNxNUl5S29nTUNQdHc0OTBCYUI2NzdDYUVHSlh1ZlFCL0VxWkdkNkNTamlDdE9udU1UYlhWWG14eGN4ZmtDTVFEVFNQeGFyWlh2TnJreFUzVGtVTUkzM3l6dkZWVlJUNHd4V0pDOTk0T3NkY1o0K1JHTnNZRHlSNWdtZHIwbkRHZz0iLCJNSUlDUXpDQ0FjbWdBd0lCQWdJSUxjWDhpTkxGUzVVd0NnWUlLb1pJemowRUF3TXdaekViTUJrR0ExVUVBd3dTUVhCd2JHVWdVbTl2ZENCRFFTQXRJRWN6TVNZd0pBWURWUVFMREIxQmNIQnNaU0JEWlhKMGFXWnBZMkYwYVc5dUlFRjFkR2h2Y21sMGVURVRNQkVHQTFVRUNnd0tRWEJ3YkdVZ1NXNWpMakVMTUFrR0ExVUVCaE1DVlZNd0hoY05NVFF3TkRNd01UZ3hPVEEyV2hjTk16a3dORE13TVRneE9UQTJXakJuTVJzd0dRWURWUVFEREJKQmNIQnNaU0JTYjI5MElFTkJJQzBnUnpNeEpqQWtCZ05WQkFzTUhVRndjR3hsSUVObGNuUnBabWxqWVhScGIyNGdRWFYwYUc5eWFYUjVNUk13RVFZRFZRUUtEQXBCY0hCc1pTQkpibU11TVFzd0NRWURWUVFHRXdKVlV6QjJNQkFHQnlxR1NNNDlBZ0VHQlN1QkJBQWlBMklBQkpqcEx6MUFjcVR0a3lKeWdSTWMzUkNWOGNXalRuSGNGQmJaRHVXbUJTcDNaSHRmVGpqVHV4eEV0WC8xSDdZeVlsM0o2WVJiVHpCUEVWb0EvVmhZREtYMUR5eE5CMGNUZGRxWGw1ZHZNVnp0SzUxN0lEdll1VlRaWHBta09sRUtNYU5DTUVBd0hRWURWUjBPQkJZRUZMdXczcUZZTTRpYXBJcVozcjY5NjYvYXl5U3JNQThHQTFVZEV3RUIvd1FGTUFNQkFmOHdEZ1lEVlIwUEFRSC9CQVFEQWdFR01Bb0dDQ3FHU000OUJBTURBMmdBTUdVQ01RQ0Q2Y0hFRmw0YVhUUVkyZTN2OUd3T0FFWkx1Tit5UmhIRkQvM21lb3locG12T3dnUFVuUFdUeG5TNGF0K3FJeFVDTUcxbWloREsxQTNVVDgyTlF6NjBpbU9sTTI3amJkb1h0MlFmeUZNbStZaGlkRGtMRjF2TFVhZ002QmdENTZLeUtBPT0iXX0.eyJ0cmFuc2FjdGlvbklkIjoiMjAwMDAwMTA4MDM4MDQ2NSIsIm9yaWdpbmFsVHJhbnNhY3Rpb25JZCI6IjIwMDAwMDEwODAzODA0NjUiLCJ3ZWJPcmRlckxpbmVJdGVtSWQiOiIyMDAwMDAwMTIxNzI0MTU4IiwiYnVuZGxlSWQiOiJjb20ubG92ZUtleS5ueXgiLCJwcm9kdWN0SWQiOiJjb20ubG92ZUtleS5ueXguMm1vbnRoIiwic3Vic2NyaXB0aW9uR3JvdXBJZGVudGlmaWVyIjoiMjE4MzA1OTYiLCJwdXJjaGFzZURhdGUiOjE3NjU1MjgwNDkwMDAsIm9yaWdpbmFsUHVyY2hhc2VEYXRlIjoxNzY1NTI4MDUwMDAwLCJleHBpcmVzRGF0ZSI6MTc2NTUyODY0OTAwMCwicXVhbnRpdHkiOjEsInR5cGUiOiJBdXRvLVJlbmV3YWJsZSBTdWJzY3JpcHRpb24iLCJpbkFwcE93bmVyc2hpcFR5cGUiOiJQVVJDSEFTRUQiLCJzaWduZWREYXRlIjoxNzY1NTQ1Njg4ODk3LCJlbnZpcm9ubWVudCI6IlNhbmRib3giLCJ0cmFuc2FjdGlvblJlYXNvbiI6IlBVUkNIQVNFIiwic3RvcmVmcm9udCI6IlVTQSIsInN0b3JlZnJvbnRJZCI6IjE0MzQ0MSIsInByaWNlIjoyOTkwLCJjdXJyZW5jeSI6IlVTRCIsImFwcFRyYW5zYWN0aW9uSWQiOiI3MDUxMDE2OTUxMDg3NDA0MjcifQ.ANGQBobIroeZfZnBPiOSDJonND8-7PnPRbW29G1Nhfj_-BxLMvRu-Qu6SzmVDEJ9NrE-2EPH1R1tk-yV-TuPbg"; + // 从 JWT header 中提取公钥 + PublicKey publicKey = extractPublicKeyFromJWT(signedPayload); + // 解码 JWT(使用公钥验证) + Jws claimsJws = Jwts.parserBuilder() + .setSigningKey(publicKey) + .build() + .parseClaimsJws(signedPayload); + Claims claims = claimsJws.getBody(); + claims.forEach((key, value) -> System.out.println(key + ": " + value)); + String notificationType = claims.get("notificationType", String.class); + String notificationUuid = claims.get("notificationUUID", String.class); + + System.out.println("Notification Type: " + notificationType); + System.out.println("Notification UUID: " + notificationUuid); + + + + + // 解码 JWT(使用公钥验证) + // 注意,Jwts.parserBuilder() 代码没有包含,实际解码时应该使用你原来的方法 +// System.out.println("Extracted Public Key: " + publicKey); + } + + /** + * 从 JWT 的 x5c header 中提取公钥 + */ + private static PublicKey extractPublicKeyFromJWT(String jwt) throws Exception { + // 解析 JWT header(不验证签名) + String[] parts = jwt.split("\\."); + if (parts.length < 2) { + throw new IllegalArgumentException("Invalid JWT format"); + } + + // 解码 header + String headerJson = new String(Base64.getUrlDecoder().decode(parts[0])); // 使用 URL 安全的 Base64 解码 + JSONObject header = new JSONObject(headerJson); + + // 获取 x5c 证书链(第一个证书包含公钥) + JSONArray x5cArray = header.getJSONArray("x5c"); + if (x5cArray.length() == 0) { + throw new IllegalArgumentException("No x5c certificates found in JWT header"); + } + + // 获取第一个证书(Base64 编码,标准格式非URL安全格式) + String certBase64 = x5cArray.getString(0); + + // 调试信息 + System.out.println("Cert Base64 length: " + certBase64.length()); + System.out.println("First 50 chars: " + certBase64.substring(0, Math.min(50, certBase64.length()))); + + // x5c 中的证书使用标准 Base64 编码(非 URL 安全编码) + byte[] certBytes; + try { + certBytes = Base64.getDecoder().decode(certBase64); // 使用标准 Base64 解码 + System.out.println("Decoded cert bytes length: " + certBytes.length); + } catch (IllegalArgumentException e) { + System.err.println("Base64 decode error: " + e.getMessage()); + throw new Exception("Failed to decode certificate from x5c", e); + } + + // 生成 X509 证书并提取公钥 + CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); + X509Certificate cert = (X509Certificate) certFactory.generateCertificate( + new ByteArrayInputStream(certBytes) + ); + return cert.getPublicKey(); + } +} diff --git a/src/test/java/com/yolo/keyborad/SendAttemptsParser.java b/src/test/java/com/yolo/keyborad/SendAttemptsParser.java new file mode 100644 index 0000000..36f72cd --- /dev/null +++ b/src/test/java/com/yolo/keyborad/SendAttemptsParser.java @@ -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); + } + } +} diff --git a/src/test/java/com/yolo/keyborad/test.java b/src/test/java/com/yolo/keyborad/test.java new file mode 100644 index 0000000..49cf1b5 --- /dev/null +++ b/src/test/java/com/yolo/keyborad/test.java @@ -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(); + } + } +}