You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Google Script调用App Store Connect API遇401签名认证错误

解决Google Apps Script调用App Store Connect API的401 NOT_AUTHORIZED错误

问题根源

你遇到的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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.06 15:49:51