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

FastAPI+Python实现S3服务:PUT请求签名验证失败求助

问题

使用FastAPI和Python实现S3服务器时,GET、DELETE请求的签名验证可正常工作,但PUT请求的计算签名与提供的签名不匹配。

签名验证代码

async def verify_signature(request: Request):
  try:
    authorization_header = request.headers.get("Authorization")
    amz_date = request.headers.get("x-amz-date")
    if not authorization_header or not amz_date:
        logging.error("Missing authorization or x-amz-date header")
        return False
    logging.debug(f"\n\n=======================================================\n")
    logging.debug(f"Request URL:\n{request.url}")
    logging.debug(f"Authorization Header:\n{authorization_header}")
    logging.debug(f"x-amz-date:\n{amz_date}")

    auth_parts = authorization_header.split(", ")
    credential_part = auth_parts[0]
    signed_headers_part = auth_parts[1]
    signature_part = auth_parts[2]

    credential_scope = (
        credential_part.split(" ")[1].split("Credential=")[1].split("/")[1:]
    )
    credential_scope = "/".join(credential_scope)

    signed_headers = signed_headers_part.split("SignedHeaders=")[-1].split(";")
    provided_signature = signature_part.split("Signature=")[-1]

    logging.debug(f"Signed Headers:\n{signed_headers}")
    logging.debug(f"Credential Scope:\n{credential_scope}")

    headers_dict = {k.lower(): v for k, v in request.headers.items()}
    sorted_headers = {
        k: headers_dict[k] for k in sorted(headers_dict) if k in signed_headers
    }

    logging.debug(f"Headers Dict:\n{headers_dict}")
    logging.debug(f"Sorted Headers:\n{sorted_headers}")

    canonical_uri = request.url.path
    canonical_querystring = request.url.query
    if False and headers_dict.get("x-amz-content-sha256") == "UNSIGNED-PAYLOAD":
        payload_hash = (
            "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
        )
    else:
        payload_hash = hashlib.sha256(await request.body()).hexdigest()

    sorted_headers["x-amz-content-sha256"] = payload_hash
    canonical_headers = "".join([f"{k}:{v}\n" for k, v in sorted_headers.items()])

    canonical_request = "\n".join(
        [
            request.method,
            canonical_uri,
            canonical_querystring,
            canonical_headers,
            ";".join(signed_headers),
            payload_hash,
        ]
    )

    logging.debug(f"Canonical Request:\n{canonical_request}")

    string_to_sign = "\n".join(
        [
            ALGORITHM,
            amz_date,
            credential_scope,
            hashlib.sha256(canonical_request.encode("utf-8")).hexdigest(),
        ]
    )

    logging.debug(f"String to Sign:\n{string_to_sign}")

    date_stamp = credential_scope.split("/")[0]
    signing_key = get_signature_key(
        AWS_SECRET_ACCESS_KEY,
        date_stamp,
        AWS_REGION,
        SERVICE,
    )

    signature = hmac.new(
        signing_key, string_to_sign.encode("utf-8"), hashlib.sha256
    ).hexdigest()

    logging.debug(f"Calculated Signature: {signature}")
    logging.debug(f"Provided Signature: {provided_signature}")

    is_valid = provided_signature == signature
    if not is_valid:
        logging.error("Signatures do not match")
    return is_valid

  except Exception as e:
    logging.error(f"Verification failed: {e}")
    return False

测试代码

def test():
    from io import BytesIO

    import boto3

    session = boto3.Session(
        AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, region_name=AWS_REGION
    )
    client = session.client(
        "s3", endpoint_url=ENDPOINT
    )
    f = BytesIO(b"salam2")
    f.seek(0)
    put_res = client.put_object(Bucket="mybucket", Key="myfile2.txt", Body=f)
    print(put_res)
    get_res = client.get_object(Bucket="mybucket", Key="myfile2.txt")
    print(get_res["Body"].read())
    del_res = client.delete_object(Bucket="mybucket", Key="myfile2.txt")
    print(del_res)

已参考S3 REST API签名v4官方文档及第三方实现说明。


解决方案

PUT请求签名不匹配的核心问题在于Payload哈希处理逻辑错误,以及对已签名请求头的不当修改,修复步骤如下:

1. 修正Payload哈希获取逻辑

S3签名v4规范中,PUT请求的x-amz-content-sha256头由客户端(如boto3)预先计算并发送,签名验证时必须直接使用该头的原始值,而非自行计算。自行计算可能因请求体读取方式、编码差异导致哈希值与客户端不一致。

替换原代码中Payload哈希的计算逻辑:

# 替换原有的payload_hash计算代码
content_sha256 = headers_dict.get("x-amz-content-sha256")
if content_sha256 == "UNSIGNED-PAYLOAD":
    payload_hash = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
elif content_sha256:
    payload_hash = content_sha256
else:
    # 兼容未发送该头的边缘情况,自行计算哈希
    payload_hash = hashlib.sha256(await request.body()).hexdigest()

2. 禁止修改已签名的请求头值

原代码中sorted_headers["x-amz-content-sha256"] = payload_hash会覆盖请求头中的原始值,导致Canonical Headers与客户端签名时使用的头数据不一致,直接删除该行代码:

# 删除此行:sorted_headers["x-amz-content-sha256"] = payload_hash

sorted_headers已从原始请求头中获取了x-amz-content-sha256的值(若它在signed_headers列表中),无需手动修改。

3. 确保Canonical Headers与Signed Headers顺序一致

AWS要求Canonical Headers必须按字母顺序排序,同时signed_headers参数也必须是按字母顺序排列的头名称列表。原代码中sorted_headers已按key排序,这部分逻辑正确,只需确保从Authorization头解析出的signed_headers列表符合排序规则(boto3默认会按此规则发送)。

完整修复后的签名验证代码

async def verify_signature(request: Request):
  try:
    authorization_header = request.headers.get("Authorization")
    amz_date = request.headers.get("x-amz-date")
    if not authorization_header or not amz_date:
        logging.error("Missing authorization or x-amz-date header")
        return False
    logging.debug(f"\n\n=======================================================\n")
    logging.debug(f"Request URL:\n{request.url}")
    logging.debug(f"Authorization Header:\n{authorization_header}")
    logging.debug(f"x-amz-date:\n{amz_date}")

    auth_parts = authorization_header.split(", ")
    credential_part = auth_parts[0]
    signed_headers_part = auth_parts[1]
    signature_part = auth_parts[2]

    credential_scope = (
        credential_part.split(" ")[1].split("Credential=")[1].split("/")[1:]
    )
    credential_scope = "/".join(credential_scope)

    signed_headers = signed_headers_part.split("SignedHeaders=")[-1].split(";")
    provided_signature = signature_part.split("Signature=")[-1]

    logging.debug(f"Signed Headers:\n{signed_headers}")
    logging.debug(f"Credential Scope:\n{credential_scope}")

    headers_dict = {k.lower(): v for k, v in request.headers.items()}
    sorted_headers = {
        k: headers_dict[k] for k in sorted(headers_dict) if k in signed_headers
    }

    logging.debug(f"Headers Dict:\n{headers_dict}")
    logging.debug(f"Sorted Headers:\n{sorted_headers}")

    canonical_uri = request.url.path
    canonical_querystring = request.url.query
    
    # 修正Payload哈希获取逻辑
    content_sha256 = headers_dict.get("x-amz-content-sha256")
    if content_sha256 == "UNSIGNED-PAYLOAD":
        payload_hash = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
    elif content_sha256:
        payload_hash = content_sha256
    else:
        payload_hash = hashlib.sha256(await request.body()).hexdigest()

    # 移除对已签名头的修改
    canonical_headers = "".join([f"{k}:{v}\n" for k, v in sorted_headers.items()])

    canonical_request = "\n".join(
        [
            request.method,
            canonical_uri,
            canonical_querystring,
            canonical_headers,
            ";".join(signed_headers),
            payload_hash,
        ]
    )

    logging.debug(f"Canonical Request:\n{canonical_request}")

    string_to_sign = "\n".join(
        [
            ALGORITHM,
            amz_date,
            credential_scope,
            hashlib.sha256(canonical_request.encode("utf-8")).hexdigest(),
        ]
    )

    logging.debug(f"String to Sign:\n{string_to_sign}")

    date_stamp = credential_scope.split("/")[0]
    signing_key = get_signature_key(
        AWS_SECRET_ACCESS_KEY,
        date_stamp,
        AWS_REGION,
        SERVICE,
    )

    signature = hmac.new(
        signing_key, string_to_sign.encode("utf-8"), hashlib.sha256
    ).hexdigest()

    logging.debug(f"Calculated Signature: {signature}")
    logging.debug(f"Provided Signature: {provided_signature}")

    is_valid = provided_signature == signature
    if not is_valid:
        logging.error("Signatures do not match")
    return is_valid

  except Exception as e:
    logging.error(f"Verification failed: {e}")
    return False

内容的提问来源于stack exchange,提问作者Mahdi Kiani

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.20 16:24:53