通过凭证与 RAM 签名访问 AgentRun 数据链路

更新时间:
复制 MD 格式

AgentRun 的 Agent 运行时、沙箱、模型代理等服务统一通过 HTTP/HTTPS 协议对外开放。本文以 Agent 运行时为例,介绍两种访问方式:直接 HTTP 访问(可配合自定义凭证)和 AccessKey 签名访问(适用于 RAM 用户及程序化调用场景)。

重要

请求及返回结果均使用 UTF-8 字符集编码。

前提条件

  • 已部署 Agent 运行时,并获取 Agent 运行时名称和端点名称。

  • 如需签名访问,已通过创建 AccessKey获取 AccessKey ID 和 AccessKey Secret。

HTTP API 访问

以 Agent 运行时为例,向已部署的 Agent 发起 OpenAI 兼容的 Chat Completions 请求:

curl "https://12**********.agentrun-data.cn-hangzhou.aliyuncs.com/agent-runtimes/your-agent-name/endpoints/Default/invocations/openai/v1/chat/completions" \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{
    "messages": [{"role": "user", "content": "写一段代码,查询现在是几点?"}],
    "stream": true
  }'

URL 结构说明

请求 URL 由以下部分组成:

路径片段

含义

12**********

阿里云账号 UID

*.agentrun-data.cn-hangzhou.aliyuncs.com

AgentRun 系统域名,其中 cn-hangzhou 为 Agent 运行时所在地域

/agent-runtimes/

固定路径,表示资源类型为 Agent 运行时

your-agent-name

已部署的 Agent 运行时名称

/endpoints/Default/invocations/

固定路径。endpointsinvocations 为固定关键字,Default 为 Agent 运行时上的端点名称

/openai/v1/chat/completions

Agent Server 提供的有效路由,根据 Agent Server 的实际实现而定

凭证认证

若 Agent 运行时配置了访问凭证,需在请求头中携带对应的认证信息。AgentRun 支持多种凭证形式:

  • API Key:在请求头中携带配置的 Key。例如:

    -H "X-API-Key: ********"
  • JWT:需由调用方自行向 Issuer 获取 JWT。AgentRun 通过 JWKS 公钥验证 JWT 的合法性,不负责签发 JWT。

签名访问 HTTP API

AgentRun 支持通过阿里云标准签名方式访问资源,适用于 RAM 用户及需要 AccessKey (AK/SK) 鉴权的程序化调用场景。

curl "https://12**********-ram.agentrun-data.cn-hangzhou.aliyuncs.com/agent-runtimes/your-agent-name/endpoints/Default/invocations/openai/v1/chat/completions" \
  -X POST \
  -H "Content-Type: application/json" \
  -H "Agentrun-Authorization: AGENTRUN4-HMAC-SHA256 Credential=<AccessKeyId>/<SignDate>/<SignRegion>/agentrun/aliyun_v4_request, SignedHeaders=<AdditionalHeadersVal>, Signature=<SignatureVal>" \
  -d '{
    "messages": [{"role": "user", "content": "写一段代码,查询现在是几点?"}],
    "stream": true
  }'

与直接 HTTP 访问相比,签名访问有以下差异:

项目

直接 HTTP 访问

签名访问

域名

{uid}.agentrun-data.{region}.aliyuncs.com

{uid}-ram.agentrun-data.{region}.aliyuncs.com

鉴权方式

可选自定义凭证(API Key、JWT 等)

Agentrun-Authorization 请求头

适用场景

已配置凭证的 Agent 对外调用

RAM 用户、AK/SK 程序化调用

Agentrun-Authorization 请求头格式

AGENTRUN4-HMAC-SHA256 Credential=<AccessKeyId>/<date>/<region>/<product>/aliyun_v4_request,SignedHeaders=<SignedHeaders>,Signature=<Signature>

重要约定

  • 签名算法标识为 AGENTRUN4-HMAC-SHA256

  • 请求体参与签名的方式为 UNSIGNED-PAYLOAD:规范化请求最后一行及请求头 x-acs-content-sha256 均为字面量 UNSIGNED-PAYLOAD,不对请求体做 SHA256 十六进制摘要。

  • 授权头字段名为 Agentrun-Authorization(不是 Authorization)。

  • 密钥派生使用前缀 aliyun_v4,对 AccessKey Secret 做多段 HMAC-SHA256 派生得到 SigningKey,不能直接用 AccessKey Secret 对 StringToSign 做 HMAC。

签名机制

采用 AccessKey ID 和 AccessKey Secret(及可选的 STS SecurityToken)对 HTTP/HTTPS 请求签名。服务端按相同规则重算签名并与请求中的授权信息比对,以校验调用方身份与请求完整性。

步骤一:构造规范化请求

构造规范化请求 (CanonicalRequest) 的伪代码如下:

CanonicalRequest =
  HTTPRequestMethod + '\n' +
  CanonicalURI + '\n' +
  CanonicalQueryString + '\n' +
  CanonicalHeaders + '\n' +
  SignedHeaders + '\n' +
  RequestPayloadPlaceholder

其中 RequestPayloadPlaceholder 固定为字符串 UNSIGNED-PAYLOAD(与请求是否有 body 无关)。

HTTPRequestMethod

HTTP 方法名全大写,例如 GETPOST

CanonicalURI

取 URL 的路径部分(host 与 ? 查询串之间的片段,含首字符 /)。若为空则使用 /

说明

标准文档中对路径分段做 RFC 3986 百分号编码。实际调用方应保证 path 与网关期望一致,参考实现以 URL 解析结果为准。

CanonicalQueryString
  1. 无查询参数时,CanonicalQueryString 为空字符串 ""

  2. 有查询参数时:

    1. 将所有 query 参数按参数名字典序升序排列。

    2. 对每个参数的名称与值分别做百分号编码后,用 = 连接。若值为 null、空串或缺失,仍输出 编码后的key=(等号右侧为空)。

    3. 多个参数之间用 & 连接,不在整个字符串前加 ?

百分号编码规则(与 Python urllib.parse.quote(safe='')replace('%7E','~') 一致):使用 UTF-8 字节序列编码,encodeURIComponent 语义下再将 %7E 还原为 ~

CanonicalHeaders

仅包含同时满足以下条件的请求头(不含 Agentrun-Authorization 授权头本身):

  • 请求头名为 hostcontent-type,或以 x-acs- 为前缀。

  • 对应取值非 null、非缺失。

构造步骤:

  1. 将参与签名的头名统一转为小写。

  2. 将头值去除首尾空白 (Trim)。

  3. 若存在同名小写头(重复头):

    • host 外,多个值按出现顺序用英文逗号 , 拼接。

    • host 实际请求中应只保留一个,避免歧义。

  4. 按头名字典序升序排列,对每个头输出一行:lowercase_name:trimmed_value\n。多行头直接拼接成 CanonicalHeaders 字符串(每行末尾都有 \n,含最后一行)。

参考实现中参与签名的头通常包括:

请求头

host

请求域名

x-acs-date

UTC 时间,格式为 ISO 8601,例如 2023-10-26T10:22:32Z

x-acs-content-sha256

固定为 UNSIGNED-PAYLOAD

x-acs-security-token

使用 STS 时需要携带

若业务需要将 content-type 纳入签名,须确保该请求头存在且值非空,签名过程会自动将其纳入规范化头。

SignedHeaders

取 CanonicalHeaders 中实际参与签名的头名(小写),按字典序升序排列,用英文分号 ; 连接为单行字符串。

示例值:

host;x-acs-content-sha256;x-acs-date;x-acs-security-token
RequestPayloadPlaceholder

固定为:

UNSIGNED-PAYLOAD

请求头 x-acs-content-sha256 的值也必须为 UNSIGNED-PAYLOAD,与上述占位一致。

步骤二:构造待签名字符串

HashedCanonicalRequest

对步骤一得到的完整 CanonicalRequest 字符串(UTF-8 编码)做 SHA256 哈希,并转为小写十六进制字符串:

HashedCanonicalRequest = HexEncode( SHA256( CanonicalRequest ) )
StringToSign
StringToSign =
  SignatureAlgorithm + '\n' +
  HashedCanonicalRequest

其中 SignatureAlgorithm 固定为 AGENTRUN4-HMAC-SHA256

示例:

AGENTRUN4-HMAC-SHA256
7ea06492da5221eba5297e897ce16e55f964061054b7695beedaac1145b1e259

第二行为示例哈希值,以实际计算为准。

步骤三:计算签名

派生 SigningKey

使用以下常量:

  • SIGN_PREFIX = "aliyun_v4"

  • 日期戳 date:取签名时刻 UTC 的 YYYYMMDD(与 x-acs-date 对应同一天),例如 20231026

派生过程(均为 HMAC-SHA256,数据为 UTF-8 字符串,密钥为二进制上一段输出):

k0 = HMAC-SHA256( key = UTF8(SIGN_PREFIX + AccessKeySecret), data = date )
k1 = HMAC-SHA256( key = k0, data = region )
k2 = HMAC-SHA256( key = k1, data = product )
SigningKey = HMAC-SHA256( key = k2, data = SIGN_PREFIX + "_request" )
  • region:例如 cn-hangzhou,须与网关约定一致。

  • product:固定为 agentrun

计算 Signature
Signature = HexEncode( HMAC-SHA256( key = SigningKey, data = StringToSign ) )

HexEncode 为小写十六进制。

说明

此处使用的是 SigningKey(经过多段派生的密钥),不是原始 AccessKey Secret。

步骤四:将签名添加到请求中

构造 Agentrun-Authorization 请求头:

AGENTRUN4-HMAC-SHA256 Credential=<AccessKeyId>/<date>/<region>/<product>/aliyun_v4_request,SignedHeaders=<SignedHeaders>,Signature=<Signature>
  • Credential<date> 与步骤三中派生密钥使用的 YYYYMMDD 日期一致。

  • SignedHeaders 与步骤一中的 SignedHeaders 字符串一致。

  • Signature 为步骤三计算得到的小写十六进制串。

完整请求中至少应携带以下头部字段:

请求头

说明

host

请求域名

x-acs-date

UTC 时间,ISO 8601 格式

x-acs-content-sha256

固定为 UNSIGNED-PAYLOAD

Agentrun-Authorization

签名授权头

x-acs-security-token

使用 STS 临时凭证时必须携带

与标准 ACS3-HMAC-SHA256 的对比

AgentRun 的签名机制基于阿里云 V4 签名,但存在以下差异:

项目

标准 ACS3-HMAC-SHA256

AgentRun AGENTRUN4-HMAC-SHA256

算法名

ACS3-HMAC-SHA256

AGENTRUN4-HMAC-SHA256

body 摘要

x-acs-content-sha256 为请求体的 SHA256 十六进制值

固定为 UNSIGNED-PAYLOAD

CanonicalRequest 最后一行

body 哈希或空 body 的哈希

固定为 UNSIGNED-PAYLOAD

StringToSign

算法\n + Hex(SHA256(CanonicalRequest))

相同形式,但算法名与 CanonicalRequest 内容不同

签名密钥

常用 Secret 直接 HMAC

aliyun_v4 + Secret 多段派生 SigningKey

授权头

Authorization: ...

Agentrun-Authorization: ...

Credential

Credential=AccessKeyId

Credential=AccessKeyId/date/region/product/aliyun_v4_request

签名代码示例

以下为 Node.js 参考实现,可用于生成 Agentrun-Authorization 及配套请求头。

const crypto = require('crypto');

const SIGN_PREFIX = 'aliyun_v4';
const ALGORITHM = 'AGENTRUN4-HMAC-SHA256';

function percentEncode(value) {
  if (value == null) return '';
  return encodeURIComponent(value).replace(/%7E/g, '~');
}

function hmacSha256(key, data) {
  const hmac = crypto.createHmac('sha256', key);
  hmac.update(data, 'utf8');
  return hmac.digest();
}

function getSigningKey(secret, product, region, date) {
  const sc = SIGN_PREFIX + secret;
  const dateKey = hmacSha256(Buffer.from(sc, 'utf8'), date);
  const regionKey = hmacSha256(dateKey, region);
  const productKey = hmacSha256(regionKey, product);
  return hmacSha256(productKey, SIGN_PREFIX + '_request');
}

function getSignedHeaders(headers) {
  const headerSet = new Set();
  for (const [key, value] of Object.entries(headers)) {
    const lowerKey = key.toLowerCase();
    if (value != null && (lowerKey.startsWith('x-acs-') || lowerKey === 'host' || lowerKey === 'content-type')) {
      headerSet.add(lowerKey);
    }
  }
  return Array.from(headerSet).sort();
}

function buildCanonicalizedResource(query) {
  if (!query || Object.keys(query).length === 0) return '';
  const parts = [];
  for (const key of Object.keys(query).sort()) {
    const value = query[key];
    if (value != null && value !== '') {
      parts.push(`${percentEncode(key)}=${percentEncode(value)}`);
    } else {
      parts.push(`${percentEncode(key)}=`);
    }
  }
  return parts.join('&');
}

function buildCanonicalizedHeaders(headers) {
  const newHeaders = {};
  const processedKeys = new Set();

  for (const [key, value] of Object.entries(headers)) {
    const lowerKey = key.toLowerCase();
    if (value != null) {
      if (!processedKeys.has(lowerKey) || lowerKey === 'host') {
        processedKeys.add(lowerKey);
        newHeaders[lowerKey] = String(value).trim();
      } else {
        newHeaders[lowerKey] = newHeaders[lowerKey] + ',' + String(value).trim();
      }
    }
  }

  const signedHeaders = getSignedHeaders(headers);
  let result = '';
  for (const h of signedHeaders) {
    result += `${h}:${newHeaders[h]}\n`;
  }
  return result;
}

/**
 * 生成 AgentRun 签名头
 *
 * @param {Object} options
 * @param {string} options.url - 请求 URL
 * @param {string} [options.method='GET'] - HTTP 方法
 * @param {string} [options.accessKeyId] - AccessKey ID
 * @param {string} [options.accessKeySecret] - AccessKey Secret
 * @param {string} [options.securityToken] - Security Token (STS)
 * @param {string} [options.region='cn-hangzhou'] - Region
 * @param {string} [options.product='agentrun'] - Product
 * @param {Buffer|Uint8Array} [options.body] - 请求体
 * @param {Date} [options.signTime] - 签名时间,默认当前 UTC
 * @returns {Object} 包含 host, x-acs-date, x-acs-content-sha256, Agentrun-Authorization 等
 */
function getAgentrunSignedHeaders(options = {}) {
  const {
    url,
    method = 'GET',
    accessKeyId = process.env.ALIBABA_CLOUD_ACCESS_KEY_ID,
    accessKeySecret = process.env.ALIBABA_CLOUD_ACCESS_KEY_SECRET,
    securityToken = process.env.ALIBABA_CLOUD_SECURITY_TOKEN,
    region = 'cn-hangzhou',
    product = 'agentrun',
    body,
    signTime,
  } = options;

  if (!accessKeyId || !accessKeySecret) {
    throw new Error('Access Key ID and Secret are required');
  }

  const parsed = new URL(url);
  const host = parsed.host;
  const pathname = parsed.pathname || '/';

  const queryParams = {};
  if (parsed.search) {
    for (const [k, v] of parsed.searchParams) {
      queryParams[k] = v;
    }
  }

  const now = signTime ? new Date(signTime) : new Date();
  const timestamp = now.toISOString().replace(/\.\d{3}Z$/, 'Z');
  const date = now.toISOString().slice(0, 10).replace(/-/g, '');

  const headers = {
    host,
    'x-acs-date': timestamp,
    'x-acs-content-sha256': 'UNSIGNED-PAYLOAD',
  };
  if (securityToken) {
    headers['x-acs-security-token'] = securityToken;
  }

  const hashPayload = 'UNSIGNED-PAYLOAD';

  const signingKey = getSigningKey(accessKeySecret, product, region, date);
  const canonicalUri = pathname || '/';
  const canonicalizedResource = buildCanonicalizedResource(queryParams);
  const canonicalizedHeaders = buildCanonicalizedHeaders(headers);
  const signedHeaders = getSignedHeaders(headers);
  const signedHeadersStr = signedHeaders.join(';');

  const stringToSignFirst =
    method.toUpperCase() +
    '\n' +
    canonicalUri +
    '\n' +
    canonicalizedResource +
    '\n' +
    canonicalizedHeaders +
    '\n' +
    signedHeadersStr +
    '\n' +
    hashPayload;

  const hashHex = crypto.createHash('sha256').update(stringToSignFirst, 'utf8').digest('hex');
  const stringToSignFinal = ALGORITHM + '\n' + hashHex;

  const signature = hmacSha256(signingKey, stringToSignFinal).toString('hex');

  const authHeader =
    `${ALGORITHM} Credential=${accessKeyId}/${date}/${region}/${product}/` +
    `${SIGN_PREFIX}_request,SignedHeaders=${signedHeadersStr},Signature=${signature}`;

  headers['Agentrun-Authorization'] = authHeader;
  return headers;
}

module.exports = { getAgentrunSignedHeaders };

使用示例

以下示例展示如何使用上述签名函数发起带签名的请求:

const https = require('https');
const { getAgentrunSignedHeaders } = require('./agentrun-sign');

const url = 'https://12345678901234-ram.agentrun-data.cn-hangzhou.aliyuncs.com/agent-runtimes/my-agent/endpoints/Default/invocations/openai/v1/chat/completions';
const body = JSON.stringify({
  messages: [{ role: 'user', content: '你好' }],
  stream: false,
});

const signedHeaders = getAgentrunSignedHeaders({
  url,
  method: 'POST',
  accessKeyId: process.env.ALIBABA_CLOUD_ACCESS_KEY_ID,
  accessKeySecret: process.env.ALIBABA_CLOUD_ACCESS_KEY_SECRET,
  region: 'cn-hangzhou',
  product: 'agentrun',
});

const parsed = new URL(url);
const options = {
  hostname: parsed.hostname,
  path: parsed.pathname,
  method: 'POST',
  headers: {
    ...signedHeaders,
    'Content-Type': 'application/json',
    'Content-Length': Buffer.byteLength(body),
  },
};

const req = https.request(options, (res) => {
  let data = '';
  res.on('data', (chunk) => { data += chunk; });
  res.on('end', () => { console.log(JSON.parse(data)); });
});

req.write(body);
req.end();