我花了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。
附:签名验证失败排查清单
- 时间偏差:服务器时间和本地时间差超过5分钟会直接失败,同步NTP
- 空格encode:URL参数里的空格必须encode为
%20,不能是+ - header小写:CanonicalHeaders里所有header name必须小写
- payload_hash:GET请求也必须包含
x-amz-content-sha256,值为空字符串的SHA256 - 用的Key类型:确认用的是访问密钥(Access Key),不是OpenAPI Key
本文代码在 Python 3.8+ 下测试通过,天翼云OBS地区 cn-east-1。