跳到主要内容

认证测试

所有对 QI DTVM API 的请求都携带两个 header:

  • API-CLIENT-KEY:您集成的 API key。
  • AUTHORIZATION:使用您的私钥签名的 JWT。QI 使用您在密钥交换中注册的公钥验证签名。

本页面用 Python 构造这两个 header 并调用 /authentication_test;只有当主机、API key 和签名都正确时,该端点才返回 200。请在进行任何其他调用之前先完成此测试。

1. 选择您所属角色的 Base URL​

这是首次调用中最常见的错误:复制了使用其他角色主机的示例。

每种角色都有自己的主机。API key 只在其签发角色对应的主机上有效:管理人的 API key 发送到转让方的主机会被拒绝并返回 Invalid API Key,即使签名正确也是如此。

您的角色Sandbox生产环境
管理人(基金管理机构)https://manager-api.sandbox.qidtvm.com.brhttps://manager-api.qidtvm.com.br
顾问(顾问机构)https://consultant-api.sandbox.qidtvm.com.brhttps://consultant-api.qidtvm.com.br
转让方https://assignor-api.sandbox.qidtvm.com.brhttps://assignor-api.qidtvm.com.br
发起人https://originator-api.sandbox.qidtvm.com.br由集成团队提供
投资者https://investor-api.sandbox.qidtvm.com.brhttps://investor-api.qidtvm.com.br
分销商https://distributor-api.sandbox.qidtvm.com.brhttps://distributor-api.qidtvm.com.br

请使用创建集成时 QI 告知的角色;如果您是通过门户创建的集成,则使用注册该集成的门户所对应的角色。Sandbox 的 API key 在生产环境中无效,反之亦然。

管理人和顾问

请直接从门户复制 Base URL:它显示在集成的 Credenciais 区块中、API Key 旁边,已对应正确的角色和环境。参见第 3 步中的图片。

2. 安装并导入依赖库​

pip install "python-jose[cryptography]" requests
from datetime import datetime, timezone
import json
from hashlib import md5

from jose import jwt
import requests

3. 填写 API key、私钥和算法​

加密数据
api_key = "YOUR_API_KEY"

client_private_key = """-----BEGIN PRIVATE KEY-----
YOUR_PRIVATE_KEY
-----END PRIVATE KEY-----"""

algorithm = "RS256" # 您密钥的算法;见下表

各个值的获取位置取决于您的集成是如何创建的:

值管理人和顾问(门户)转让方、发起人、投资者和分销商
api_key在门户的 Gestão de Acesso > Integração API 中打开集成,复制 Credenciais 区块中的 API Key 字段。完成密钥交换后由 QI 集成团队发送。
client_private_key在门户中生成密钥对时下载的 qi-integration-...-private-key.pem 文件,或您自行生成的密钥对中的私钥。您在密钥交换中生成的密钥对中的私钥。
algorithm门户中所选算法下方提示的值:RSA 使用 RS256(门户默认),EC P-256、P-384 或 P-521 分别使用 ES256、ES384 或 ES512。ES512,对应密钥交换中生成的 EC P-521 密钥。

门户集成页面中的 Credenciais 区块,突出显示 API Key 和 API Base URL

粘贴私钥文件的全部内容,包括 BEGIN 和 END 行——根据生成密钥对的工具不同,头部可能是 PRIVATE KEY 或 EC PRIVATE KEY。API key 标识集成,而私钥签名证明请求来自您:请将两者都视为凭证。私钥绝不会发送给 QI。在生产环境中,请从密钥保管库或环境变量中加载私钥——切勿将其写在源代码中。

门户中从创建集成到注册公钥的完整步骤,参见通过门户集成。

4. 定义请求数据​

请求数据
# 替换为您所属角色及环境的 URL(见第 1 步的表格)。
base_url = "YOUR_PROFILE_BASE_URL" # 例如: "https://manager-api.sandbox.qidtvm.com.br"

method = "POST"
endpoint = "/authentication_test"
body = {"name": "QI Tech"}
注意

签名中的 uri 字段必须与请求中发送的路径完全一致。如果端点在 query string 中接收参数,这些参数也属于签名的 URI——参见带 query string 的请求。

5. 构造签名内容​

基础字典
timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S")

dict_to_sign = {"timestamp": timestamp, "method": method, "uri": endpoint}

timestamp 采用 AAAA-MM-DDTHH:MM:SS 格式,不带时区和毫秒,始终为 UTC,与 QI 服务器时间的偏差在 10 分钟以内才会被接受。每次请求都需生成新的 timestamp,并保持服务器时钟同步。

5.1. 如有 body,加入 MD5​

带 body 的请求需包含将要发送的确切字节的 MD5:

基础字典
body_bytes = json.dumps(body).encode()

dict_to_sign["payload_md5"] = md5(body_bytes).hexdigest()

在第 7 步中必须原样发送这些 body_bytes。如果 JSON 被重新序列化——字段顺序、空格或重音字符编码发生变化——MD5 就会不一致。

6. 签名 JWT​

encoded_header_token = jwt.encode(
claims=dict_to_sign,
key=client_private_key,
algorithm=algorithm,
headers={"alg": algorithm, "typ": "JWT"},
)

algorithm 必须与密钥类型匹配:RSA 使用 RS*,EC 使用 ES*。否则 jwt.encode 会在请求发出前失败,并抛出描述不清晰的 JWSError——请先检查 algorithm。

7. 发送请求​

headers = {
"API-CLIENT-KEY": api_key,
"AUTHORIZATION": encoded_header_token,
"Content-Type": "application/json",
}

resp = requests.post(url=f"{base_url}{endpoint}", headers=headers, data=body_bytes)
print(resp.status_code, resp.json())

预期响应为 200,内容为 {"name": "QI Tech", "success": "Congrats!"}。GET 和 POST /authentication_test 的接口契约参见测试端点。

认证错误​

所有主机上认证错误的 code 结尾数字都相同。前缀表示响应的主机:MIT 表示 manager-api,CIT 表示 consultant-api,AIT 表示 assignor-api。如果前缀不属于您的角色,说明 base_url 有误。

code 结尾状态码错误检查项
000007401缺少 header是否同时发送了 API-CLIENT-KEY 和 AUTHORIZATION 两个 header。
000008401API key 无效base_url 是否为您所属角色及正确环境的 URL,API key 复制时是否带有空格。
000009400集成未启用集成是否处于启用状态。请联系 integracao.dtvm@qitech.com.br。
000010401公钥待注册集成是否已注册公钥。
000011401认证无效JWT 是否使用与已注册公钥对应的私钥签名。
000012401签名不完整JWT 是否包含 timestamp、method 和 uri 字段。
000013401方法无效签名中的 method 是否与请求方法一致,且为大写。
000014401签名已过期timestamp 是否为 UTC、是否在请求时生成,以及服务器时钟是否同步。
000015401端点无效签名中的 uri 是否与发送的路径完全一致,包含 query string,且不含 base_url。
000016401MD5 无效payload_md5 是否基于 body 中发送的相同字节计算。
000020401timestamp 格式错误timestamp 是否为 AAAA-MM-DDTHH:MM:SS 格式,不带时区和毫秒。只有 manager-api 和 consultant-api 返回此代码:在 assignor-api 上,格式错误的 timestamp 会返回 500 错误。

权限、路由和各服务的错误参见 API 错误。

带 query string 的请求​

分页或带筛选条件的端点通过 query string 接收参数——例如 ?page=0&limit=20。这种情况下,query string 属于签名的 URI。

uri 字段必须与请求中发送的路径一致:参数相同、顺序相同、编码相同。如果签名的 URI 与发送的 URI 不一致,请求会被拒绝并返回状态码 401:

响应
{
"title": "Invalid endpoint",
"description": "Invalid endpoint.",
"translation": "O endpoint é invalido",
"code": "MIT000015"
}

示例​

分页 GET
# base_url 沿用第 4 步。此端点属于管理人(manager-api)。
path = "/quota/fund_class/{fund_class_key}/investor_positions"
query_string = "page=0&limit=20"

# query string 属于签名的 uri
uri = f"{path}?{query_string}"

method = "GET"
timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S")

dict_to_sign = {"timestamp": timestamp, "method": method, "uri": uri}

encoded_header_token = jwt.encode(
claims=dict_to_sign,
key=client_private_key,
algorithm=algorithm,
headers={"alg": algorithm, "typ": "JWT"},
)

headers = {"API-CLIENT-KEY": api_key, "AUTHORIZATION": encoded_header_token}

resp = requests.get(url=f"{base_url}{uri}", headers=headers)
print(resp.json())

建议​

  • 先构造好 query string,再发送完整 URL。避免在已签名的 URI 上同时使用 requests 的 params 参数:HTTP 客户端可能会重新排序或重新编码参数,导致签名失效。
  • GET 请求通常没有 body。这种情况下,请勿发送 body,也不要包含 payload_md5 字段。
  • 包含特殊字符的值(证件号中的标点、日期、空格)必须以最终发送到 URL 中的格式进行签名。