天翼云OBS没有官方Python SDK,API文档有3个真实坑点。本文还原踩坑过程,给出完整签名实现,并开源了ctyun-obs-python库,pip in

0 阅读4分钟

我花了3天跑通天翼云OBS API,顺便写了个开源Python SDK

最近在做一个项目,需要把文件存到天翼云OBS。选天翼云的理由很朴实:便宜,电信骨干网,延迟低,国内备案也方便。

然后我就掉坑里了。


从V2EX开始的噩梦

去V2EX搜"天翼云 OBS",看到的不是教程,是一片哀嚎:

"签名验证怎么都跑不通,文档里的示例运行直接400" "API Key和OpenAPI Key,到底用哪个?" "GET请求文档说不需要body,但签名的时候body算不算进去?"

这不是一两个人,相关帖子回复加起来超过500条,时间跨度从2022年到2024年,问题基本没变。


三个真实问题

问题一:签名算法文档和实际行为不一致

天翼云OBS的签名算法基于AWS S3 Signature V4,但有几处天翼云自己的改动,文档没有说清楚。

最坑的一处:CanonicalQueryString的排序规则。正确实现:

import urllib.parse

def canonical_query_string(params: dict) -> str:
    sorted_params = sorted(params.items(), key=lambda x: x[0])
    return "&".join(
        f"{urllib.parse.quote(k, safe='')}={urllib.parse.quote(str(v), safe='')}"
        for k, v in sorted_params
    )

关键点:safe="" 这个参数,/ 也要被encode,很多示例代码漏掉了这个。

问题二:API Key vs OpenAPI Key,两套体系并存

天翼云控制台里能看到两个地方可以获取Key。OBS用的是API Key(账号管理→访问密钥里的Access Key ID + Secret Access Key那套)。

如果你拿OpenAPI Key去调OBS接口,会直接报403,错误信息不会告诉你原因。

问题三:GET请求的body处理

所有请求都必须包含 x-amz-content-sha256 header,GET请求body为空时,值应该是:

e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

这是空字符串的SHA256。这个header 必须存在,不能省略,很多人在这里翻车。


正确的签名流程(完整Python实现)

import hashlib, hmac, datetime, urllib.parse, requests

def sign(key, msg):
    return hmac.new(key, msg.encode("utf-8"), hashlib.sha256).digest()

def get_signature_key(secret_key, date_stamp, region, service):
    k_date = sign(("AWS4" + secret_key).encode("utf-8"), date_stamp)
    k_region = sign(k_date, region)
    k_service = sign(k_region, service)
    return sign(k_service, "aws4_request")

def build_auth_header(method, bucket, key_name, region, access_key, secret_key, body=b""):
    service = "s3"
    host = f"{bucket}.obs.{region}.ctyunapi.cn"
    endpoint = f"https://{host}/{key_name}"
    
    t = datetime.datetime.utcnow()
    amzdate = t.strftime("%Y%m%dT%H%M%SZ")
    datestamp = t.strftime("%Y%m%d")
    payload_hash = hashlib.sha256(body).hexdigest()
    
    headers = {"host": host, "x-amz-date": amzdate, "x-amz-content-sha256": payload_hash}
    signed_headers_list = sorted(headers.keys())
    canonical_headers = "".join(f"{k}:{headers[k]}\n" for k in signed_headers_list)
    signed_headers = ";".join(signed_headers_list)
    
    canonical_request = "\n".join([method, f"/{key_name}", "", canonical_headers, signed_headers, payload_hash])
    credential_scope = f"{datestamp}/{region}/{service}/aws4_request"
    string_to_sign = "\n".join(["AWS4-HMAC-SHA256", amzdate, credential_scope,
        hashlib.sha256(canonical_request.encode("utf-8")).hexdigest()])
    
    signing_key = get_signature_key(secret_key, datestamp, region, service)
    signature = hmac.new(signing_key, string_to_sign.encode("utf-8"), hashlib.sha256).hexdigest()
    auth_header = (f"AWS4-HMAC-SHA256 Credential={access_key}/{credential_scope}, "
        f"SignedHeaders={signed_headers}, Signature={signature}")
    
    return endpoint, {**headers, "Authorization": auth_header}

上传文件示例:

body = b"Hello, World!"
endpoint, headers = build_auth_header("PUT", "my-bucket", "test/hello.txt",
    "cn-east-1", "YOUR_ACCESS_KEY", "YOUR_SECRET_KEY", body)
headers["Content-Type"] = "text/plain"
response = requests.put(endpoint, headers=headers, data=body)
print(response.status_code)  # 200 表示成功

关于天翼云本身

说句公道话:天翼云的基础设施是有优势的。电信骨干网,延迟在国内主要地区普遍低于10ms,价格比阿里云、腾讯云便宜不少,对于大流量存储场景很划算。

问题出在文档和SDK的建设上——这块确实跟不上。这不是本质问题,是可以改善的。


开源Python SDK:ctyun-obs

在搞清楚签名算法之后,我把这套逻辑封装成了一个Python库:

pip install ctyun-obs-python

Quick Start(真的就3行):

from ctyun_obs import OBSClient

client = OBSClient(
    access_key="YOUR_ACCESS_KEY",
    secret_key="YOUR_SECRET_KEY",
    region="cn-east-1"
)

client.upload("my-bucket", "hello.txt", b"Hello World")
data = client.download("my-bucket", "hello.txt")
url = client.get_object_url("my-bucket", "hello.txt", expires=604800)
files = client.list_objects("my-bucket", prefix="images/")

GitHub地址:github.com/metafo333-h…

如果这个库帮你节省了时间,点个Star。发现了bug或者有改进建议,欢迎提issue或PR。


附:签名验证失败排查清单

  1. 时间偏差:服务器时间和本地时间差超过5分钟会直接失败,同步NTP
  2. 空格encode:URL参数里的空格必须encode为 %20,不能是 +
  3. header小写:CanonicalHeaders里所有header name必须小写
  4. payload_hash:GET请求也必须包含 x-amz-content-sha256,值为空字符串的SHA256
  5. 用的Key类型:确认用的是访问密钥(Access Key),不是OpenAPI Key

本文代码在 Python 3.8+ 下测试通过,天翼云OBS地区 cn-east-1。