我创建这个类来访问Apple API请求
@Transactional(readOnly = true)
public class AppleAPIService {
public static void main(String[] args) {
Path privateKeyPath = Paths.get("/Users/ricardolle/IdeaProjects/mystic-planets-api/src/main/resources/cert/AuthKey_5425KFDYSC.p8");
String keyContent = new String(Files.readAllBytes(privateKeyPath), StandardCharsets.UTF_8);
System.out.println("Original Key Content: " + keyContent); // Logging the original content
keyContent = keyContent.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replaceAll("\\s+", ""); // Remove all whitespaces and newlines, more robust than just replacing \n
System.out.println("Processed Key Content: " + keyContent); // Logging processed content
byte[] decodedKey = Base64.getDecoder().decode(keyContent);
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(decodedKey);
KeyFactory kf = KeyFactory.getInstance("EC");
PrivateKey pk = kf.generatePrivate(spec);
Map<String, Object> headerMap = new HashMap<>();
headerMap.put("alg", "ES256"); // Algorithm, e.g., RS256 for asymmetric signing
headerMap.put("kid", "5425KFDYSC"); // Algorithm, e.g., RS256 for asymmetric signing
headerMap.put("typ", "JWT"); //
String issuer = "68a6Se82-111e-47e3-e053-5b8c7c11a4d1"; // Replace with your issuer
//String subject = "subject"; // Replace with your subject
long nowMillis = System.currentTimeMillis();
Date issuedAt = new Date(nowMillis);
Date expiration = new Date(nowMillis + 3600000); // Expiration time (1 hour in this example)
JwtBuilder jwtBuilder = Jwts.builder()
.setHeader(headerMap)
.setIssuer(issuer)
.setAudience("appstoreconnect-v1")
.setIssuedAt(issuedAt)
.signWith(pk)
.setExpiration(expiration);
// Print the JWT header as a JSON string
String headerJson = jwtBuilder.compact();
System.out.println("JWT Header: " + headerJson);
String apiUrl = "https://api.appstoreconnect.apple.com/v1/apps";
// Create headers with Authorization
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "Bearer " + headerJson);
headers.setContentType(MediaType.APPLICATION_JSON);
// Create HttpEntity with headers
HttpEntity<String> entity = new HttpEntity<>(headers);
// Make GET request using RestTemplate
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> response = restTemplate.exchange(
apiUrl,
HttpMethod.GET,
entity,
String.class
);
// Handle the response
if (response.getStatusCode() == HttpStatus.OK) {
String responseBody = response.getBody();
System.out.println("Response: " + responseBody);
} else {
System.out.println("Error: " + response.getStatusCodeValue());
}
// Print the JWT payload as a JSON string
String payloadJson = jwtBuilder.compact();
System.out.println("JWT Payload: " + payloadJson);
字符串
但我犯了个错误
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>}"
at org.springframework.web.client.HttpClientErrorException.create(HttpClientErrorException.java:106)
at org.springframework.web.client.DefaultResponseErrorHandler.handleError(DefaultResponseErrorHandler.java:183)
at org.springframework.web.client.DefaultResponseErrorHandler.handleError(DefaultResponseErrorHandler.java:137)
at org.springframework.web.client.ResponseErrorHandler.handleError(ResponseErrorHandler.java:63)
at org.springframework.web.client.RestTemplate.handleResponse(RestTemplate.java:932)
at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:881)
at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:781)
at org.springframework.web.client.RestTemplate.exchange(RestTemplate.java:663)
at com.mysticriver.service.AppleAPIService.main(AppleAPIService.java:77)
型
用编辑器打开文件会得到这样的结果:
-----BEGIN PRIVATE KEY-----
MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQg5Fu6zyvQDhgGvevK
pe4OYs32cFSz1oxLd/YCYWJSOPagCgYIKoZIzj0DAQehRANCAATrJf+q7/nieM4y
V9/v71e/Xl/aS+LF4riW5lkcld8lFQB5ekivp5T7w57t6nqp8rCqtq79nEhIyzDr
hCMnmLEk
-----END PRIVATE KEY-----
型
3条答案
按热度按时间n3ipq98p1#
原始问题(编辑前):
401 Unauthorized
:这应该意味着您使用的authentication token (JWT)不正确,签名不正确,或者过期。从你的代码中,你可能会错过“用你的私钥对JWT进行签名”的步骤。JWT必须用你的私钥进行签名,Apple API才能对其进行身份验证。
请参阅“Creating API Keys for App Store Connect API“以创建密钥。
然后,使用所述私钥对JWT进行签名:
字符串
参见Dejan Milosevic中的“REST Security With JWT Using Java and Spring Security“。
此外,检查JWT结构(头部,有效载荷,签名)是否符合Apple的指导方针。
并且,像往常一样,添加一些错误处理,以便在失败时提供更多信息反馈。
型
您的更新代码现在包括:
.p8
文件加载私钥,该文件通常用于Apple API身份验证。它使用KeyFactory
和“RSA”来生成PrivateKey
。您现在有一个
InvalidKeySpecException
,表示密钥规范或格式有问题。.p8
文件可能包含EC (Elliptic Curve) private key,而不是RSA。因此KeyFactory
示例应该使用"EC"
而不是"RSA"
。确保
.p8
文件未损坏且格式正确。型
再次,添加加载私钥的错误处理以查明问题。
型
第三次编辑:
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
中使用它之前,请确保将其正确解码为二进制形式。型
一些错误处理:
型
如果在格式化和Base64解码内容之后,加载私钥仍然有问题,那就意味着密钥本身或处理方式有问题。
我假设你的
.p8
文件确实以-----BEGIN PRIVATE KEY-----
开头,以-----END PRIVATE KEY-----
结尾,并且它是完整的,没有被截断。你已经从Apple Developer帐户生成了一个新的密钥,所以你可以与以前的密钥进行比较。确保Base64解码正确。不正确的解码可能导致无效的密钥规范。
并确保文件阅读过程不会以任何方式改变密钥内容。例如,确保用于读取文件的字符编码与文件的编码匹配。
型
这基本上是你的代码,但是在一个函数中设置为在错误的情况下抛出异常,使用
\s+
代替\s+
的replaceAll,并使用一些日志记录(System.out.println
)或者,为了测试,尝试更直接的解码方法:
型
在任何情况下,尝试使用OpenSSL之类的工具来检查密钥的有效性。这可以帮助确定问题是在于密钥本身还是Java代码。
型
如果文件格式正确,该命令应该输出密钥详细信息。如果它失败,问题可能是密钥文件本身。
pwuypxnk2#
最简单的解决方案是使用BountyCastleLibrary。
这个库将负责删除不必要的头和解码Base64 PEM数据。
注意:BountyCastle对椭圆曲线密码算法解析有很好的支持。
另外,您可以尝试使用
com.auth0:java-jwt
依赖项,因为它提供的功能比io.jsonwebtoken:jjwt
依赖项多得多。在pom.xml中添加此依赖项:
字符串
更改/更新您的逻辑:
型
请查看此Apple开发者主题论坛问题以解决您的问题-https://developer.apple.com/forums/thread/707220
如果上面的线程仍然无法解决您的问题,请尝试通过
withClaims("bid", "put your bundle id")
添加bid(apple bundle id)仅此而已
qzlgjiam3#
tl;干
尝试提供不大于
20
分钟的过期时间,例如15
(尽管文档中声明不大于,但恐怕应该小于20
):字符串
详情
你的答案中提供的最后一个版本的代码基本上是好的。
我认为问题与您指定的令牌的生命周期有关,一个小时。
正如Apple Developer文档在描述
exp
JWT payload字段时所解释的那样:令牌的过期时间(以Unix纪元时间表示)。过期时间超过20分钟的令牌将无效,确定适当的令牌生存期中列出的资源除外。
参考的确定适当的令牌生命周期文档指出,App Store Connect在以下情况下接受生命周期大于20分钟的令牌:
GET
请求。您的Java代码满足前两个条件,但不满足第三个条件:前面提到的文档列出了可以接受长期令牌的资源,而您正在使用的List Apps端点,通常是Apps资源,并不包括在其中。
如上所述,要解决此问题,请在执行请求时尝试定义小于
20
分钟的过期时间。例如:型
代码的其余部分看起来很好:请注意,您在生成JWT令牌时提供的所有信息都是正确的,并且密钥没有被撤销,并且它已经被分配了一个被授权执行请求的角色。
请参考this related article,我认为它很好地说明了如何执行操作。