Spring Boot 为Apple API请求生成令牌

4ktjp1zp  于 2024-01-06  发布在  Spring
关注(0)|答案(3)|浏览(188)

我创建这个类来访问Apple API请求

  1. @Transactional(readOnly = true)
  2. public class AppleAPIService {
  3. public static void main(String[] args) {
  4. Path privateKeyPath = Paths.get("/Users/ricardolle/IdeaProjects/mystic-planets-api/src/main/resources/cert/AuthKey_5425KFDYSC.p8");
  5. String keyContent = new String(Files.readAllBytes(privateKeyPath), StandardCharsets.UTF_8);
  6. System.out.println("Original Key Content: " + keyContent); // Logging the original content
  7. keyContent = keyContent.replace("-----BEGIN PRIVATE KEY-----", "")
  8. .replace("-----END PRIVATE KEY-----", "")
  9. .replaceAll("\\s+", ""); // Remove all whitespaces and newlines, more robust than just replacing \n
  10. System.out.println("Processed Key Content: " + keyContent); // Logging processed content
  11. byte[] decodedKey = Base64.getDecoder().decode(keyContent);
  12. PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(decodedKey);
  13. KeyFactory kf = KeyFactory.getInstance("EC");
  14. PrivateKey pk = kf.generatePrivate(spec);
  15. Map<String, Object> headerMap = new HashMap<>();
  16. headerMap.put("alg", "ES256"); // Algorithm, e.g., RS256 for asymmetric signing
  17. headerMap.put("kid", "5425KFDYSC"); // Algorithm, e.g., RS256 for asymmetric signing
  18. headerMap.put("typ", "JWT"); //
  19. String issuer = "68a6Se82-111e-47e3-e053-5b8c7c11a4d1"; // Replace with your issuer
  20. //String subject = "subject"; // Replace with your subject
  21. long nowMillis = System.currentTimeMillis();
  22. Date issuedAt = new Date(nowMillis);
  23. Date expiration = new Date(nowMillis + 3600000); // Expiration time (1 hour in this example)
  24. JwtBuilder jwtBuilder = Jwts.builder()
  25. .setHeader(headerMap)
  26. .setIssuer(issuer)
  27. .setAudience("appstoreconnect-v1")
  28. .setIssuedAt(issuedAt)
  29. .signWith(pk)
  30. .setExpiration(expiration);
  31. // Print the JWT header as a JSON string
  32. String headerJson = jwtBuilder.compact();
  33. System.out.println("JWT Header: " + headerJson);
  34. String apiUrl = "https://api.appstoreconnect.apple.com/v1/apps";
  35. // Create headers with Authorization
  36. HttpHeaders headers = new HttpHeaders();
  37. headers.set("Authorization", "Bearer " + headerJson);
  38. headers.setContentType(MediaType.APPLICATION_JSON);
  39. // Create HttpEntity with headers
  40. HttpEntity<String> entity = new HttpEntity<>(headers);
  41. // Make GET request using RestTemplate
  42. RestTemplate restTemplate = new RestTemplate();
  43. ResponseEntity<String> response = restTemplate.exchange(
  44. apiUrl,
  45. HttpMethod.GET,
  46. entity,
  47. String.class
  48. );
  49. // Handle the response
  50. if (response.getStatusCode() == HttpStatus.OK) {
  51. String responseBody = response.getBody();
  52. System.out.println("Response: " + responseBody);
  53. } else {
  54. System.out.println("Error: " + response.getStatusCodeValue());
  55. }
  56. // Print the JWT payload as a JSON string
  57. String payloadJson = jwtBuilder.compact();
  58. System.out.println("JWT Payload: " + payloadJson);

字符串
但我犯了个错误

  1. Exception in thread "main" org.springframework.web.client.HttpClientErrorException$Unauthorized: 401 Unauthorized: "{<EOL>?"errors": [{<EOL>??"status": "401",<EOL>??"code": "NOT_AUTHORIZED",<EOL>??"title": "Authentication credentials are missing or invalid.",<EOL>??"detail": "Provide a properly configured and signed bearer token, and make sure that it has not expired. Learn more about Generating Tokens for API Requests https://developer.apple.com/go/?id=api-generating-tokens"<EOL>?}]<EOL>}"
  2. at org.springframework.web.client.HttpClientErrorException.create(HttpClientErrorException.java:106)
  3. at org.springframework.web.client.DefaultResponseErrorHandler.handleError(DefaultResponseErrorHandler.java:183)
  4. at org.springframework.web.client.DefaultResponseErrorHandler.handleError(DefaultResponseErrorHandler.java:137)
  5. at org.springframework.web.client.ResponseErrorHandler.handleError(ResponseErrorHandler.java:63)
  6. at org.springframework.web.client.RestTemplate.handleResponse(RestTemplate.java:932)
  7. at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:881)
  8. at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:781)
  9. at org.springframework.web.client.RestTemplate.exchange(RestTemplate.java:663)
  10. at com.mysticriver.service.AppleAPIService.main(AppleAPIService.java:77)


用编辑器打开文件会得到这样的结果:

  1. -----BEGIN PRIVATE KEY-----
  2. MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQg5Fu6zyvQDhgGvevK
  3. pe4OYs32cFSz1oxLd/YCYWJSOPagCgYIKoZIzj0DAQehRANCAATrJf+q7/nieM4y
  4. V9/v71e/Xl/aS+LF4riW5lkcld8lFQB5ekivp5T7w57t6nqp8rCqtq79nEhIyzDr
  5. hCMnmLEk
  6. -----END PRIVATE KEY-----

n3ipq98p

n3ipq98p1#

原始问题(编辑前):401 Unauthorized:这应该意味着您使用的authentication token (JWT)不正确,签名不正确,或者过期。
从你的代码中,你可能会错过“用你的私钥对JWT进行签名”的步骤。JWT必须用你的私钥进行签名,Apple API才能对其进行身份验证。
请参阅“Creating API Keys for App Store Connect API“以创建密钥。
然后,使用所述私钥对JWT进行签名:

  1. PrivateKey privateKey = // Load your private key here
  2. JwtBuilder jwtBuilder = Jwts.builder()
  3. .setHeader(headerMap)
  4. .setIssuer(issuer)
  5. .setAudience("appstoreconnect-v1")
  6. .setIssuedAt(issuedAt)
  7. .setExpiration(expiration)
  8. .signWith(SignatureAlgorithm.RS256, privateKey); // Signing the JWT

字符串
参见Dejan Milosevic中的“REST Security With JWT Using Java and Spring Security“。
此外,检查JWT结构(头部,有效载荷,签名)是否符合Apple的指导方针。
并且,像往常一样,添加一些错误处理,以便在失败时提供更多信息反馈。

  1. try {
  2. ResponseEntity<String> response = restTemplate.exchange(
  3. apiUrl,
  4. HttpMethod.GET,
  5. entity,
  6. String.class
  7. );
  8. // Rest of the code
  9. } catch (HttpClientErrorException ex) {
  10. System.out.println("HTTP Error: " + ex.getStatusCode());
  11. System.out.println("Error Body: " + ex.getResponseBodyAsString());
  12. }


您的更新代码现在包括:

  • .p8文件加载私钥,该文件通常用于Apple API身份验证。它使用KeyFactory和“RSA”来生成PrivateKey
  • 使用加载的私钥对JWT进行签名,这是使用Apple API进行身份验证的必要步骤。

您现在有一个InvalidKeySpecException,表示密钥规范或格式有问题。
.p8文件可能包含EC (Elliptic Curve) private key,而不是RSA。因此KeyFactory示例应该使用"EC"而不是"RSA"
确保.p8文件未损坏且格式正确。

  1. KeyFactory keyFactory = KeyFactory.getInstance("EC"); // Change from "RSA" to "EC"
  2. PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(privateKeyBytes);
  3. PrivateKey privateKey = keyFactory.generatePrivate(privateKeySpec);


再次,添加加载私钥的错误处理以查明问题。

  1. try {
  2. PrivateKey privateKey = keyFactory.generatePrivate(privateKeySpec);
  3. // Rest of the code
  4. } catch (InvalidKeySpecException e) {
  5. e.printStackTrace(); // More informative error output
  6. return;
  7. }


第三次编辑:
jdk.crypto.ec/sun.security.ec.ECKeyFactory.engineGeneratePrivate(ECKeyFactory.java:168)应该表示从.p8文件加载私钥时出现问题。堆栈跟踪表明问题在于PKCS8EncodedKeySpec,它无法解码密钥。
您已经更新了KeyFactory示例以使用“EC”而不是“RSA”,这对于Apple的.p8私钥是正确的。但是尽管进行了此更改,InvalidKeySpecException仍然存在,这表明问题不在于密钥工厂算法(RSA或EC)的选择,而在于密钥文件的格式或内容。
错误消息“Unable to decode key“和“extra data at the end“表明.p8文件可能不在正确的PKCS#8 format中。
因此,请尝试并确保.p8文件是正确的PKCS#8格式私钥。它应该以“-----BEGIN PRIVATE KEY-----“开头,以“-----END PRIVATE KEY-----“结尾。任何其他数据或格式问题都可能导致错误。
如果.p8文件包含任何页眉或页脚(如“BEGIN PRIVATE KEY“),则需要在传递给PKCS8EncodedKeySpec之前将其删除。
.p8文件中的密钥数据通常是Base64编码的。在PKCS8EncodedKeySpec中使用它之前,请确保将其正确解码为二进制形式。

  1. String keyContent = new String(Files.readAllBytes(privateKeyPath), StandardCharsets.UTF_8)
  2. .replaceAll("\\n", "")
  3. .replace("-----BEGIN PRIVATE KEY-----", "")
  4. .replace("-----END PRIVATE KEY-----", "");
  5. byte[] decodedKey = Base64.getDecoder().decode(keyContent);
  6. PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(decodedKey);


一些错误处理:

  1. try {
  2. PrivateKey privateKey = keyFactory.generatePrivate(privateKeySpec);
  3. // Rest of the code
  4. } catch (InvalidKeySpecException e) {
  5. e.printStackTrace(); // More detailed error information
  6. return;
  7. }


如果在格式化和Base64解码内容之后,加载私钥仍然有问题,那就意味着密钥本身或处理方式有问题。
我假设你的.p8文件确实以-----BEGIN PRIVATE KEY-----开头,以-----END PRIVATE KEY-----结尾,并且它是完整的,没有被截断。你已经从Apple Developer帐户生成了一个新的密钥,所以你可以与以前的密钥进行比较。
确保Base64解码正确。不正确的解码可能导致无效的密钥规范。
并确保文件阅读过程不会以任何方式改变密钥内容。例如,确保用于读取文件的字符编码与文件的编码匹配。

  1. public static PrivateKey loadPrivateKey(Path privateKeyPath) throws IOException, GeneralSecurityException {
  2. String keyContent = new String(Files.readAllBytes(privateKeyPath), StandardCharsets.UTF_8);
  3. System.out.println("Original Key Content: " + keyContent); // Logging the original content
  4. keyContent = keyContent.replace("-----BEGIN PRIVATE KEY-----", "")
  5. .replace("-----END PRIVATE KEY-----", "")
  6. .replaceAll("\\s+", ""); // Remove all whitespaces and newlines, more robust than just replacing \n
  7. System.out.println("Processed Key Content: " + keyContent); // Logging processed content
  8. byte[] decodedKey = Base64.getDecoder().decode(keyContent);
  9. PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(decodedKey);
  10. KeyFactory kf = KeyFactory.getInstance("EC");
  11. return kf.generatePrivate(spec);
  12. }


这基本上是你的代码,但是在一个函数中设置为在错误的情况下抛出异常,使用\s+代替\s+的replaceAll,并使用一些日志记录(System.out.println
或者,为了测试,尝试更直接的解码方法:

  1. String keyContent = new String(Files.readAllBytes(privateKeyPath), StandardCharsets.UTF_8);
  2. keyContent = keyContent.replace("-----BEGIN PRIVATE KEY-----", "")
  3. .replace("-----END PRIVATE KEY-----", "")
  4. .replaceAll("\\s+", ""); // Remove all whitespaces and newlines
  5. byte[] decodedKey = Base64.getDecoder().decode(keyContent);


在任何情况下,尝试使用OpenSSL之类的工具来检查密钥的有效性。这可以帮助确定问题是在于密钥本身还是Java代码。

  1. openssl pkcs8 -in [path-to-your-key].p8 -nocrypt -topk8


如果文件格式正确,该命令应该输出密钥详细信息。如果它失败,问题可能是密钥文件本身。

展开查看全部
pwuypxnk

pwuypxnk2#

最简单的解决方案是使用BountyCastleLibrary。
这个库将负责删除不必要的头和解码Base64 PEM数据。

注意:BountyCastle对椭圆曲线密码算法解析有很好的支持。

另外,您可以尝试使用com.auth0:java-jwt依赖项,因为它提供的功能比io.jsonwebtoken:jjwt依赖项多得多。
在pom.xml中添加此依赖项:

  1. <dependency>
  2. <groupId>org.bouncycastle</groupId>
  3. <artifactId>bcpkix-jdk18on</artifactId>
  4. <version>1.76</version>
  5. </dependency>
  6. <dependency>
  7. <groupId>com.auth0</groupId>
  8. <artifactId>java-jwt</artifactId>
  9. <version>4.4.0</version>
  10. </dependency>

字符串
更改/更新您的逻辑:

  1. import com.auth0.jwt.JWT;
  2. import com.auth0.jwt.algorithms.Algorithm;
  3. import org.bouncycastle.util.io.pem.PemObject;
  4. import org.bouncycastle.util.io.pem.PemReader;
  5. import org.springframework.http.*;
  6. import org.springframework.web.client.RestTemplate;
  7. import java.io.FileReader;
  8. import java.security.KeyFactory;
  9. import java.security.interfaces.ECPrivateKey;
  10. import java.security.spec.PKCS8EncodedKeySpec;
  11. import java.util.Date;
  12. import java.util.concurrent.TimeUnit;
  13. @Transactional(readOnly = true)
  14. public class AppleAPIService {
  15. public static void main(String[] args) {
  16. try (PemReader pemReader = new PemReader(
  17. new FileReader("/Users/ricardolle/IdeaProjects/mystic-planets-api/src/main/resources/cert/AuthKey_5425KFDYSC.p8"))) {
  18. KeyFactory keyFactory = KeyFactory.getInstance("EC");
  19. PemObject pemObj = pemReader.readPemObject();
  20. byte[] content = pemObj.getContent();
  21. PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(content);
  22. ECPrivateKey privateKey = (ECPrivateKey) keyFactory.generatePrivate(privateKeySpec);
  23. String token = JWT.create()
  24. .withKeyId("5525KFDYSC")
  25. .withIssuer("69a6de82-121e-48e3-e053-5b8c7c11a4d1")
  26. .withExpiresAt(new Date(System.currentTimeMillis() + TimeUnit.HOURS.toMillis(1)))
  27. .withClaim("scope", Collections.singletonList("GET /v1/apps"))
  28. .withAudience("appstoreconnect-v1")
  29. .sign(Algorithm.ECDSA256(privateKey));
  30. System.out.println("JWT token: " + token);
  31. // Create headers with Authorization
  32. HttpHeaders headers = new HttpHeaders();
  33. headers.setBearerAuth(token);
  34. headers.setContentType(MediaType.APPLICATION_JSON);
  35. // Create HttpEntity with headers
  36. HttpEntity<String> entity = new HttpEntity<>(headers);
  37. // Make GET request using RestTemplate
  38. ResponseEntity<String> response = new RestTemplate().exchange(
  39. "https://api.appstoreconnect.apple.com/v1/apps",
  40. HttpMethod.GET, entity, String.class);
  41. // Handle the response
  42. if (response.getStatusCode() == HttpStatus.OK) {
  43. String responseBody = response.getBody();
  44. System.out.println("Response: " + responseBody);
  45. } else {
  46. System.out.println("Error: " + response.getStatusCodeValue());
  47. }
  48. } catch (Exception e) {
  49. System.out.println(e.getMessage());
  50. }
  51. }
  52. }


请查看此Apple开发者主题论坛问题以解决您的问题-https://developer.apple.com/forums/thread/707220
如果上面的线程仍然无法解决您的问题,请尝试通过withClaims("bid", "put your bundle id")添加bid(apple bundle id)
仅此而已

展开查看全部
qzlgjiam

qzlgjiam3#

tl;干

尝试提供不大于20分钟的过期时间,例如15(尽管文档中声明不大于,但恐怕应该小于20):

  1. Date expiration = new Date(nowMillis + 15 * 60 * 1000);

字符串

详情

你的答案中提供的最后一个版本的代码基本上是好的。
我认为问题与您指定的令牌的生命周期有关,一个小时。
正如Apple Developer文档在描述exp JWT payload字段时所解释的那样:
令牌的过期时间(以Unix纪元时间表示)。过期时间超过20分钟的令牌将无效,确定适当的令牌生存期中列出的资源除外。
参考的确定适当的令牌生命周期文档指出,App Store Connect在以下情况下接受生命周期大于20分钟的令牌:

  • 令牌定义了一个范围。
  • 范围仅包括GET请求。
  • 作用域中的资源允许长寿命令牌。

您的Java代码满足前两个条件,但不满足第三个条件:前面提到的文档列出了可以接受长期令牌的资源,而您正在使用的List Apps端点,通常是Apps资源,并不包括在其中。
如上所述,要解决此问题,请在执行请求时尝试定义小于20分钟的过期时间。例如:

  1. Date expiration = new Date(nowMillis + 15 * 60 * 1000);


代码的其余部分看起来很好:请注意,您在生成JWT令牌时提供的所有信息都是正确的,并且密钥没有被撤销,并且它已经被分配了一个被授权执行请求的角色。
请参考this related article,我认为它很好地说明了如何执行操作。

展开查看全部

相关问题