All checks were successful
Build docker and publish / build (20.15.1) (push) Successful in 6m40s
当IssuerID缺失或为默认值时,使用硬编码值作为回退方案 更新测试文件中的IssuerID和BundleID为实际值
172 lines
4.5 KiB
Go
172 lines
4.5 KiB
Go
package main
|
||
|
||
import (
|
||
"crypto/ecdsa"
|
||
"crypto/rand"
|
||
"crypto/sha256"
|
||
"crypto/x509"
|
||
"encoding/base64"
|
||
"encoding/json"
|
||
"encoding/pem"
|
||
"fmt"
|
||
"io"
|
||
"log"
|
||
"net/http"
|
||
"time"
|
||
)
|
||
|
||
// 配置区域 - 请在此处填入您的真实信息进行测试
|
||
const (
|
||
// 必填:您的 Key ID (从 App Store Connect 获取)
|
||
KeyID = "2C4X3HVPM8"
|
||
|
||
// 必填:您的 Issuer ID (从 App Store Connect 获取,通常是一个 UUID)
|
||
IssuerID = "34f54810-5118-4b7f-8069-c8c1e012b7a9" // 请替换为您真实的 Issuer ID
|
||
|
||
// 必填:您的 Bundle ID (App 的包名)
|
||
BundleID = "com.taw.hifastvpn" // 请替换为您真实的 Bundle ID
|
||
|
||
// 必填:用于测试的 Transaction ID (任意一个真实的交易 ID)
|
||
TestTransactionID = "2000001083318819"
|
||
|
||
// 必填:是否为沙盒环境
|
||
IsSandbox = true
|
||
)
|
||
|
||
// P8 私钥内容 (硬编码用于测试)
|
||
const PrivateKeyPEM = `-----BEGIN PRIVATE KEY-----
|
||
MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgsVDj0g/D7uNCm8aC
|
||
E4TuaiDT4Pgb1IuuZ69YdGNvcAegCgYIKoZIzj0DAQehRANCAARObgGumaESbPMM
|
||
SIRDAVLcWemp0fMlnfDE4EHmqcD58arEJWsr3aWEhc4BHocOUIGjko0cVWGchrFa
|
||
/T/KG1tr
|
||
-----END PRIVATE KEY-----`
|
||
|
||
func main() {
|
||
log.Println("开始测试 Apple IAP API 连接...")
|
||
log.Printf("环境: %v (Sandbox=%v)\n", func() string {
|
||
if IsSandbox {
|
||
return "沙盒 (Sandbox)"
|
||
}
|
||
return "生产 (Production)"
|
||
}(), IsSandbox)
|
||
log.Printf("KeyID: %s\n", KeyID)
|
||
log.Printf("IssuerID: %s\n", IssuerID)
|
||
log.Printf("BundleID: %s\n", BundleID)
|
||
log.Printf("TransactionID: %s\n", TestTransactionID)
|
||
|
||
token, err := buildAPIToken()
|
||
if err != nil {
|
||
log.Fatalf("生成 JWT Token 失败: %v", err)
|
||
}
|
||
log.Println("JWT Token 生成成功")
|
||
|
||
// 发起请求
|
||
host := "https://api.storekit.itunes.apple.com"
|
||
if IsSandbox {
|
||
host = "https://api.storekit-sandbox.itunes.apple.com"
|
||
}
|
||
|
||
url := fmt.Sprintf("%s/inApps/v1/transactions/%s", host, TestTransactionID)
|
||
req, _ := http.NewRequest("GET", url, nil)
|
||
req.Header.Set("Authorization", "Bearer "+token)
|
||
|
||
log.Printf("正在请求: %s", url)
|
||
start := time.Now()
|
||
resp, err := http.DefaultClient.Do(req)
|
||
if err != nil {
|
||
log.Fatalf("请求失败: %v", err)
|
||
}
|
||
defer resp.Body.Close()
|
||
|
||
duration := time.Since(start)
|
||
body, _ := io.ReadAll(resp.Body)
|
||
|
||
log.Printf("请求耗时: %v", duration)
|
||
log.Printf("状态码: %d", resp.StatusCode)
|
||
|
||
if resp.StatusCode == 200 {
|
||
log.Println("✅ 测试成功!API 调用正常。")
|
||
log.Printf("响应内容: %s", string(body))
|
||
} else {
|
||
log.Println("❌ 测试失败!")
|
||
log.Printf("错误响应: %s", string(body))
|
||
if resp.StatusCode == 401 {
|
||
log.Println("原因分析: 401 Unauthorized 通常表示:")
|
||
log.Println("1. Key ID 或 Issuer ID 错误")
|
||
log.Println("2. Bundle ID 不匹配")
|
||
log.Println("3. 私钥错误")
|
||
log.Println("4. Token 格式错误 (如算法或 Claims)")
|
||
} else if resp.StatusCode == 404 {
|
||
log.Println("原因分析: 404 Not Found 通常表示 Transaction ID 不存在或环境(沙盒/生产)选错了")
|
||
}
|
||
}
|
||
}
|
||
|
||
// 下面是复制过来的工具函数
|
||
func buildAPIToken() (string, error) {
|
||
header := map[string]interface{}{
|
||
"alg": "ES256",
|
||
"kid": KeyID,
|
||
"typ": "JWT",
|
||
}
|
||
now := time.Now().Unix()
|
||
payload := map[string]interface{}{
|
||
"iss": IssuerID,
|
||
"iat": now,
|
||
"exp": now + 60, // 测试 Token 有效期短一点即可
|
||
"aud": "appstoreconnect-v1",
|
||
}
|
||
if BundleID != "" {
|
||
payload["bid"] = BundleID
|
||
}
|
||
|
||
hb, _ := json.Marshal(header)
|
||
pb, _ := json.Marshal(payload)
|
||
|
||
enc := func(b []byte) string {
|
||
return base64.RawURLEncoding.EncodeToString(b)
|
||
}
|
||
unsigned := fmt.Sprintf("%s.%s", enc(hb), enc(pb))
|
||
|
||
block, _ := pem.Decode([]byte(PrivateKeyPEM))
|
||
if block == nil {
|
||
return "", fmt.Errorf("invalid private key")
|
||
}
|
||
keyAny, err := x509.ParsePKCS8PrivateKey(block.Bytes)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
priv, ok := keyAny.(*ecdsa.PrivateKey)
|
||
if !ok {
|
||
return "", fmt.Errorf("private key is not ECDSA")
|
||
}
|
||
|
||
digest := sha256Sum([]byte(unsigned))
|
||
r, s, err := ecdsa.Sign(rand.Reader, priv, digest)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
|
||
curveBits := priv.Curve.Params().BitSize
|
||
keyBytes := curveBits / 8
|
||
if curveBits%8 > 0 {
|
||
keyBytes += 1
|
||
}
|
||
rBytes := r.Bytes()
|
||
rBytesPadded := make([]byte, keyBytes)
|
||
copy(rBytesPadded[keyBytes-len(rBytes):], rBytes)
|
||
|
||
sBytes := s.Bytes()
|
||
sBytesPadded := make([]byte, keyBytes)
|
||
copy(sBytesPadded[keyBytes-len(sBytes):], sBytes)
|
||
|
||
sig := append(rBytesPadded, sBytesPadded...)
|
||
return unsigned + "." + base64.RawURLEncoding.EncodeToString(sig), nil
|
||
}
|
||
|
||
func sha256Sum(b []byte) []byte {
|
||
h := sha256.New()
|
||
h.Write(b)
|
||
return h.Sum(nil)
|
||
}
|