Google Script调用App Store Connect API遇401签名认证错误
问题根源
你遇到的401错误核心是算法不匹配导致签名无效:
- 代码中JWT头部声明使用
ES256(ECDSA-SHA256)算法,但实际用Google Apps Script原生的Utilities.computeHmacSha256Signature生成了HMAC-SHA256签名,算法与声明不符 - Google Apps Script原生仅支持RSA-SHA256和HMAC-SHA256,不支持App Store Connect API要求的ECDSA签名算法,导致JWT无法通过认证
解决方案
由于Google Apps Script原生不支持ES256,我们需要引入ECDSA签名的实现代码,替换原有的HMAC签名逻辑。以下是完整修改方案:
1. 添加ECDSA签名工具代码
将以下适配Apps Script环境的ECDSA-SHA256签名实现添加到脚本中:
// ECDSA-SHA256签名工具,适配App Store Connect的.p8密钥 function ecdsaSign(message, privateKeyPem) { const keyContent = privateKeyPem.replace(/-----BEGIN PRIVATE KEY-----|-----END PRIVATE KEY-----|\n/g, ''); const keyBytes = Utilities.base64Decode(keyContent); const reader = new ByteReader(keyBytes); reader.readUint32(); const algorithm = reader.readOID(); if (algorithm !== '1.2.840.10045.2.1') throw new Error('不是EC私钥'); reader.readOID(); const privateKeyOctet = reader.readOctetString(); const d = privateKeyOctet.slice(1); const messageHash = Utilities.computeDigest(Utilities.DigestAlgorithm.SHA_256, message); const curve = new secp256r1(); const signature = curve.sign(d, messageHash); const derSignature = curve.encodeSignature(signature); return Utilities.base64EncodeWebSafe(derSignature).replace(/=+$/, ''); } class ByteReader { constructor(bytes) { this.bytes = new Uint8Array(bytes); this.pos = 0; } readUint32() { const val = (this.bytes[this.pos] << 24) | (this.bytes[this.pos+1] << 16) | (this.bytes[this.pos+2] << 8) | this.bytes[this.pos+3]; this.pos +=4; return val; } readOID() { let oid = ''; let first = this.bytes[this.pos++]; oid += Math.floor(first / 40) + '.' + (first % 40); while (this.pos < this.bytes.length) { let val = 0; let b; do { b = this.bytes[this.pos++]; val = (val << 7) | (b & 0x7F); } while (b & 0x80); oid += '.' + val; if (oid === '1.2.840.10045.2.1') break; } return oid; } readOctetString() { let len = this.bytes[this.pos++]; if (len & 0x80) { const lenBytes = len & 0x7F; len = 0; for (let i=0; i<lenBytes; i++) { len = (len <<8) | this.bytes[this.pos++]; } } const octet = this.bytes.slice(this.pos, this.pos+len); this.pos += len; return octet; } } class secp256r1 { constructor() { this.p = BigInt('0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF'); this.a = BigInt('0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFC'); this.b = BigInt('0x5AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B'); this.n = BigInt('0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551'); this.Gx = BigInt('0x6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296'); this.Gy = BigInt('0x4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F'); } sign(d, hash) { const hashBig = this.bytesToBigInt(hash); let k; let r, s; do { k = this.generateRandomK(); const [Rx, _] = this.multiply(this.Gx, this.Gy, k); r = Rx % this.n; if (r === BigInt(0)) continue; s = (this.modInverse(k, this.n) * (hashBig + this.bytesToBigInt(d) * r)) % this.n; if (s === BigInt(0)) continue; if (s > this.n / BigInt(2)) s = this.n - s; } while (r === BigInt(0) || s === BigInt(0)); return { r, s }; } encodeSignature(sig) { const rBytes = this.bigIntToBytes(sig.r); const sBytes = this.bigIntToBytes(sig.s); const der = []; der.push(0x30); const rLen = 2 + rBytes.length; const sLen = 2 + sBytes.length; der.push(rLen + sLen); der.push(0x02); der.push(rBytes.length); der.push(...rBytes); der.push(0x02); der.push(sBytes.length); der.push(...sBytes); return new Uint8Array(der); } multiply(x, y, scalar) { let resultX = BigInt(0); let resultY = BigInt(0); let currentX = x; let currentY = y; while (scalar > BigInt(0)) { if (scalar % BigInt(2) === BigInt(1)) { [resultX, resultY] = this.add(resultX, resultY, currentX, currentY); } [currentX, currentY] = this.double(currentX, currentY); scalar = scalar >> BigInt(1); } return [resultX, resultY]; } add(x1, y1, x2, y2) { if (x1 === BigInt(0) && y1 === BigInt(0)) return [x2, y2]; if (x2 === BigInt(0) && y2 === BigInt(0)) return [x1, y1]; if (x1 === x2 && y1 === this.p - y2) return [BigInt(0), BigInt(0)]; let lambda; if (x1 === x2 && y1 === y2) { lambda = (BigInt(3)*x1*x1 + this.a) * this.modInverse(BigInt(2)*y1, this.p); } else { lambda = (y2 - y1) * this.modInverse(x2 - x1, this.p); } lambda = lambda % this.p; const x3 = (lambda*lambda - x1 - x2) % this.p; const y3 = (lambda*(x1 - x3) - y1) % this.p; return [x3 < BigInt(0) ? x3 + this.p : x3, y3 < BigInt(0) ? y3 + this.p : y3]; } double(x, y) { return this.add(x, y, x, y); } modInverse(a, m) { let m0 = m; let y = BigInt(0), x = BigInt(1); if (m === BigInt(1)) return BigInt(0); while (a > BigInt(1)) { let q = a / m; let t = m; m = a % m; a = t; t = y; y = x - q * y; x = t; } if (x < BigInt(0)) x += m0; return x; } generateRandomK() { let randomBytes = Utilities.getRandomBytes(32); let k = this.bytesToBigInt(randomBytes); while (k >= this.n || k === BigInt(0)) { randomBytes = Utilities.getRandomBytes(32); k = this.bytesToBigInt(randomBytes); } return k; } bytesToBigInt(bytes) { let bigInt = BigInt(0); for (const b of bytes) { bigInt = (bigInt << BigInt(8)) | BigInt(b); } return bigInt; } bigIntToBytes(bigInt) { let bytes = []; if (bigInt === BigInt(0)) return [0]; while (bigInt > BigInt(0)) { bytes.unshift(Number(bigInt & BigInt(0xFF))); bigInt = bigInt >> BigInt(8); } if (bytes[0] & 0x80) bytes.unshift(0); return bytes; } }
2. 修改JWT生成函数
替换原有的createJwt函数,使用ECDSA签名:
const createJwt = ({ privateKey, expiresInMins, data = {} }) => { const header = { alg: 'ES256', kid: '你的密钥ID', // 替换为App Store Connect密钥的kid typ: 'JWT' }; const now = Date.now(); const expires = new Date(now); expires.setMinutes(expires.getMinutes() + expiresInMins); const payload = { exp: Math.round(expires.getTime() / 1000), iat: Math.round(now / 1000), ...data }; const base64Encode = (text, json = true) => { const data = json ? JSON.stringify(text) : text; return Utilities.base64EncodeWebSafe(data).replace(/=+$/, ''); }; const toSign = `${base64Encode(header)}.${base64Encode(payload)}`; const signature = ecdsaSign(toSign, privateKey); return `${toSign}.${signature}`; };
3. 更新主函数与API调用
确保私钥、Team ID等参数正确:
const generateAccessToken = () => { // 替换为你的App Store Connect .p8私钥 const privateKey = `-----BEGIN PRIVATE KEY----- MIGTAgEAMBMDByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgnUaBH3umCqbyl6w4 NBE9YxD42J2AFBXjfd8+tvVOQrOgCgYIKoZIzj1OPQehASVSAAS4fh1BhWSzj0PV vgIIllp9BQZKTNopsesddJ5ofmKYO/m7al9Pk1KAvN0vQRJKyHJ8A0sJUc0bljDSTm GwBK4LPo -----END PRIVATE KEY-----`; const accessToken = createJwt({ privateKey, expiresInMins: 10, // Apple要求不超过20分钟 data: { iss: '你的Team ID', // 替换为Apple Developer团队ID aud: 'appstoreconnect-v1' }, }); Logger.log(accessToken); getUsers(accessToken); }; function getUsers(accessToken){ const url = 'https://api.appstoreconnect.apple.com/v1/users'; const options = { method: 'GET', muteHttpExceptions: true, contentType: "application/json", headers: { 'Authorization': `Bearer ${accessToken}` } }; const response = UrlFetchApp.fetch(url, options); if (response.getResponseCode() !== 200) { Logger.log(`请求失败,状态码:${response.getResponseCode()}`); Logger.log(response.getContentText()); return; } const data = JSON.parse(response.getContentText()); Logger.log(JSON.stringify(data, null, 2)); }
注意事项
- 私钥必须保留
-----BEGIN PRIVATE KEY-----和-----END PRIVATE KEY-----头尾,代码会自动处理换行 kid是App Store Connect密钥页面显示的密钥ID,iss是你的Apple Developer团队ID- JWT有效期不要超过20分钟,建议设置为10分钟避免超时
- 首次运行脚本需授权
UrlFetchApp的调用权限
内容的提问来源于stack exchange,提问作者Stan
相关产品推荐
相关产品推荐

