자동화를 위해서 애플 앱스토어의 여러 기능을 개발할 수 있습니다.(애플에서 API일부를 제공해줘서)
그 중 앱의 IAP 리스트를 조회하는 API샘플입니다.
- 주의: API는 호출양 제한이 걸려있습니다.(링크 참고)
List All In-App Purchases for an App
- https://developer.apple.com/documentation/appstoreconnectapi/list_all_in-app_purchases_for_an_app
java 샘플 소스
- git 주소: https://github.com/oshnew/spring-boot-ver2-study/commit/b04e4293e983bf7425be17a97aaac6c80f70a26b
import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import sample.apple.AppStoreConnectApi.AppStoreConnectApiJWT;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
/**
* 특정 애플 앱의 IAP로 등록된 상품 리스트 조회
* - 참고: https://developer.apple.com/documentation/appstoreconnectapi/list_all_in-app_purchases_for_an_app
*
* @author
*/
public class ListInAppPurchasesForAnApp {
public static void main(String[] args) throws IOException {
final String issuer = "입력필요";
final String keyIdentifier = "입력필요"; //전달받은 사설키의 키 ID
final String privateKeyFilePath = "입력필요"; //사설키 경로(제품개발 권한 액세스). 애플 앱스토어 웹 콘솔에서 다운로드 받음. 키에 허용권한(ACCOUNT_HOLDER, ADMIN, APP_MANAGER 중 1가지) 필요
int expireAfterMinutes = 20; //20분 넘으면 안됨
AppStoreConnectApiJWT clzJWT = new AppStoreConnectApiJWT();
final String jwt = clzJWT.makeApiAuthJwt(issuer, keyIdentifier, expireAfterMinutes, privateKeyFilePath); //인증용 JWT생성
System.out.println("인증용 JWT: " + jwt);
OkHttpClient client = new OkHttpClient.Builder().readTimeout(4, TimeUnit.SECONDS).connectTimeout(1, TimeUnit.SECONDS).writeTimeout(5, TimeUnit.SECONDS).build();
final String id = "입력필요"; //애플 앱스토어 웹 콘솔에서 'Apple ID'로 표시되는 부분, 앱의 구분용ID로 추정됨
String prefixUri = String.format("https://api.appstoreconnect.apple.com/v1/apps/%s/inAppPurchasesV2", id);
HttpUrl.Builder httpBuilder = HttpUrl.get(prefixUri).newBuilder();
httpBuilder.addQueryParameter("limit", "200"); //최대 200건까지 API에서 제약됨
httpBuilder.addQueryParameter("filter[inAppPurchaseType]", "NON_CONSUMABLE"); //비 소모성만 조회하기 필터링
final String fullUrl = httpBuilder.build().toString();
System.out.println("fullrUrl: " + fullUrl);
Request request = new Request.Builder().addHeader("Authorization", "Bearer " + jwt).url(fullUrl).build();
Response response = client.newCall(request).execute();
if (response.isSuccessful()) {
String rtnMsg = response.body().string();
System.out.println(String.format("성공. 응답 msg => '%s", rtnMsg));
} else {
System.out.println(String.format("응답에러(200이 아님). 응답 code:'%s'\n errMsg:'%s'", response.code(), response.body().string()));
}
}
}
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.apache.tomcat.util.codec.binary.Base64;
import java.io.BufferedReader;
import java.io.FileReader;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* 애플 App Store Connect API에서 사용할 인증용 JWT토큰을 생성
*
* @author
*/
public class AppStoreConnectApiJWT {
/**
* 애플 App Store Connect API에서 사용할 인증용 JWT토큰을 생성하는 샘플 소스
* - 참고: https://developer.apple.com/documentation/appstoreconnectapi/generating_tokens_for_api_requests
*
* @param issuer Your issuer ID from the API Keys page in App Store Connect; for example, 57246542-96fe-1a63-e053-0824d011072a.
* @param keyIdentifier
* @param expireAfterMinutes
* @param privateKeyFilePath
* @return
*/
public String makeApiAuthJwt(String issuer, String keyIdentifier, int expireAfterMinutes, String privateKeyFilePath) {
if (expireAfterMinutes > 20) {
throw new IllegalArgumentException(
"expireAfterMinutes는 20보다 작아야합니다. 참고: https://developer.apple.com/documentation/appstoreconnectapi/generating_tokens_for_api_requests");
}
final String strPrivateKey = readPrivateKeyBody(privateKeyFilePath);
PrivateKey privateKeyObj = convertToPrivateKeyObj(strPrivateKey);
Map<String, Object> header = new HashMap<>();
header.put("alg", SignatureAlgorithm.ES256);
header.put("kid", keyIdentifier);
header.put("typ", "JWT");
final Date now = new Date();
//주의: 일반적으로 만료시간이 20분보다 크면 애플에서 거부함. https://developer.apple.com/documentation/appstoreconnectapi/generating_tokens_for_api_requests#3878467
LocalDateTime expLdt = LocalDateTime.ofInstant(now.toInstant(), ZoneId.systemDefault()).plusMinutes(10);
Date expiredAt = java.sql.Timestamp.valueOf(expLdt);
// @formatter:off
String jwt = Jwts.builder()
.setHeader(header)
.setIssuer(issuer)
.setAudience("appstoreconnect-v1") //애플 API정의서
.setIssuedAt(now) //발행 시간
.setExpiration(expiredAt) //만료시간
.signWith(SignatureAlgorithm.ES256, privateKeyObj)
.compact();
// @formatter:on
return jwt;
}
private static PrivateKey convertToPrivateKeyObj(String strPrivateKey) {
try {
byte[] encodedKey = Base64.decodeBase64(strPrivateKey);
return KeyFactory.getInstance("EC").generatePrivate(new PKCS8EncodedKeySpec(encodedKey));
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
throw new RuntimeException(e.getMessage(), e);
}
}
/**
* private key 내용을 얻어옴
* - -----BEGIN PRIVATE KEY----- 또는 -----END PRIVATE KEY----- 와 같은 가이드라인 줄은 제외하고 실제 사용하는 부분만 파일에서 가져옴
*
* @param filePath
* @return
*/
private String readPrivateKeyBody(String filePath) {
try (BufferedReader br = new BufferedReader(new FileReader(filePath))) {
String line;
StringBuilder sb = new StringBuilder();
while ((line = br.readLine()) != null) {
if (line.contains("PRIVATE KEY")) { //guard line은 pass
continue;
}
sb.append(line);
}
return sb.toString();
} catch (Exception e) {
throw new RuntimeException(e.getMessage(), e);
}
}
}