SigV4与HTTPS,两套完全不同维度的安全机制

做云原生开发或者对接S3兼容存储的时候,经常会碰到一个让人纠结的问题------已经用了HTTPS了,为什么请求头里还要塞一个又长又丑的Authorization字段,里面写满了SigV4之类的东西。是不是重复加密了,是不是多此一举。

答案恰恰相反。这两套机制根本不在同一个维度上工作,一个管的是通道安全,一个管的是身份和内容的真实性。搞清楚这个区别,才能理解为什么AWS的S3、以及像RustFS这类S3兼容存储系统,会同时要求HTTPS加身、又要求每个请求都带着SigV4签名。


HTTPS负责的是通道,不是内容本身

HTTPS本质上是HTTP套了一层TLS。它解决的问题很纯粹,就是让客户端和服务器之间的通信不被第三方偷看、不被中途篡改、并且能确认对方的身份没被冒充。

TLS握手的大致流程是这样的,客户端发起连接后先和服务器交换支持的加密套件,服务器把自己的数字证书发过去,客户端验证这张证书是不是权威CA签发的、域名对不对,验证通过后双方通过非对称加密协商出一个只有彼此知道的会话密钥,后续所有数据都用这个对称密钥加密传输。

这里的关键词是通道。TLS只关心从A到B这条管道有没有被人截获或者篡改,它完全不关心管道里跑的是谁的请求、这个请求有没有权限访问某个资源。换句话说,一个持有合法HTTPS证书的服务器,可以毫无阻碍地加密传输任何内容,哪怕这个内容是一个没有权限的人发起的恶意请求。

TLS握手结束后拿到的会话密钥,可以简单表示成下面这种非对称交换后的对称密钥关系。
Ksession =f(PublicKe yserver ,PrivateKe yclient ) K_{session} = f(PublicKey_{server}, PrivateKey_{client}) Ksession=f(PublicKeyserver,PrivateKeyclient)

这个密钥只在当前这一次连接里有效,连接断开就作废,跟具体是谁发的请求、请求内容是什么完全没有关联。


SigV4负责的是身份和完整性,不是加密

AWS Signature Version 4做的事情完全不同。它不加密任何东西,它做的是给每一个请求盖一个只有持有正确密钥的人才能盖出来的,用来证明这个请求确实是声称的那个人发出的,而且请求内容在发出之后没有被篡改过。

整个签名过程分成四步,AWS官方文档里叫得很规整,拆开来看其实逻辑很朴素。

第一步,构造canonical request。 把HTTP方法、URI、查询参数、请求头、请求体的哈希值,按照严格规定的格式排好顺序拼成一个字符串。这一步的意义是把整个请求"压缩"成一个确定性的指纹,只要请求里任何一个字节变了,这个指纹就会完全不同。

第二步,构造string to sign。 在canonical request的哈希值前面加上算法标识、时间戳、以及credential scope(由日期、区域、服务名、固定字符串aws4_request拼成)。

第三步,派生签名密钥。 这是SigV4设计里最巧妙的一环,它不是直接用你的secret key去签名,而是通过一条HMAC链式推导,一层层把密钥和日期、区域、服务名绑定进去。
kdate =HMAC("AWS4"+secretKey,date) k_{date} = HMAC(\text{"AWS4"} + secretKey, date) kdate=HMAC("AWS4"+secretKey,date)
kregion =HMAC( kdate ,region) k_{region} = HMAC(k_{date}, region) kregion=HMAC(kdate,region)
kservice =HMAC( kregion ,service) k_{service} = HMAC(k_{region}, service) kservice=HMAC(kregion,service)
ksigning =HMAC( kservice ,"aws4_request") k_{signing} = HMAC(k_{service}, \text{"aws4\_request"}) ksigning=HMAC(kservice,"aws4_request")

这样做的好处是即便某一天的签名密钥意外泄露,攻击者也只能在那个特定日期、特定区域、特定服务的范围内伪造请求,爆炸半径被死死限制住了。

第四步,计算最终签名。 用上面推导出的 ksigning k_{signing} ksigning对string to sign做一次HMAC-SHA256,得到的十六进制字符串就是最终的signature,塞进Authorization请求头一起发出去。

服务器收到请求后,会拿同样的方法在本地重新算一遍签名,两个签名字符串完全一致才算通过,任何一个字符的偏差都会导致校验失败。这也是为什么很多人对接S3 API时,时钟稍微差个几分钟就会报SignatureDoesNotMatch的原因,因为时间戳也是参与签名计算的一部分。


用一张图看清两者所在的层级

flowchart TD A[应用层业务逻辑] --> B[SigV4签名校验] B --> C[HTTP协议封装] C --> D[TLS加密通道] D --> E[TCP传输层] E --> F[网络层] style B fill:#ffd6a5 style D fill:#a5d8ff

从这张图能看得很清楚,TLS工作在传输层和应用层之间,负责整条管道的保密性和完整性。SigV4工作在应用层内部,是HTTP请求本身携带的一段元数据,负责证明这条请求的发起者身份和内容没被中途换掉。两者互不冲突,反而是互补关系,缺了HTTPS,SigV4签名本身以及请求里携带的access key ID都可能在传输中被窃听(虽然签名本身重放攻击有时效窗口限制,但敏感信息暴露的风险仍然存在)。缺了SigV4,任何拿到URL的人都能对着一个加密良好的通道发起未授权请求。


核心概念一览表

维度 HTTPS / TLS AWS SigV4
所在层级 传输层与应用层之间 应用层,HTTP请求头内
解决的问题 防窃听、防篡改、身份认证(服务器) 请求身份认证、内容完整性校验
是否加密内容 是,对称加密整条通道 否,仅做哈希与签名,不加密
密钥类型 会话密钥,连接结束即失效 长期secret key派生出的短期签名密钥
校验方 客户端校验服务器证书 服务器校验客户端签名
典型有效期 单次TLS会话周期 通常15分钟内有效(X-Amz-Expires控制)

SigV4签名生成的完整流程图

sequenceDiagram participant Client as 客户端 participant Canon as 构造Canonical Request participant Sign as 构造String to Sign participant Key as HMAC链式派生密钥 participant Server as 服务端校验 Client->>Canon: 方法、URI、参数、头部哈希 Canon->>Sign: 拼接算法、时间戳、Credential Scope Sign->>Key: 用secret key逐层HMAC推导 Key->>Client: 得到最终signature Client->>Server: 携带Authorization头发出请求 Server->>Server: 本地重新计算签名并比对

在FastAPI中怎么落地

FastAPI的场景通常分两种,一种是作为客户端 去调用S3或S3兼容存储,需要自己给请求签名;另一种是作为服务端去实现一个S3兼容接口,需要校验别人发来的签名是否合法。

客户端签名,调用S3类接口

最省心的做法是直接借用botocore里现成的SigV4Auth类,不用自己手写HMAC链。

python 复制代码
import requests
from botocore.auth import SigV4Auth
from botocore.awsrequest import AWSRequest
from botocore.credentials import Credentials

def sign_request(method, url, headers, body, region, service):
    credentials = Credentials(
        access_key="YOUR_ACCESS_KEY",
        secret_key="YOUR_SECRET_KEY"
    )
    request = AWSRequest(method=method, url=url, data=body, headers=headers)
    SigV4Auth(credentials, service, region).add_auth(request)
    return dict(request.headers)

signed_headers = sign_request(
    method="PUT",
    url="https://your-rustfs-endpoint/bucket/object.txt",
    headers={"Content-Type": "text/plain"},
    body=b"hello world",
    region="us-east-1",
    service="s3"
)

resp = requests.put(
    "https://your-rustfs-endpoint/bucket/object.txt",
    headers=signed_headers,
    data=b"hello world"
)

服务端校验,实现S3兼容鉴权中间件

如果FastAPI这边要扮演S3兼容服务器的角色,就得自己在依赖注入里重算一遍签名去比对。核心逻辑是把请求里的时间戳、区域、服务名从Authorization头解析出来,用同样的算法本地推导出签名,再跟客户端传过来的做字符串比较。

python 复制代码
from fastapi import FastAPI, Request, HTTPException, Depends
import hmac
import hashlib

app = FastAPI()

def get_secret_key(access_key: str) -> str:
    # 实际场景里应该去数据库或配置里查找
    return "YOUR_SECRET_KEY"

def hmac_sha256(key: bytes, msg: str) -> bytes:
    return hmac.new(key, msg.encode("utf-8"), hashlib.sha256).digest()

async def verify_sigv4(request: Request):
    auth_header = request.headers.get("Authorization", "")
    if "AWS4-HMAC-SHA256" not in auth_header:
        raise HTTPException(status_code=401, detail="缺少有效签名")

    # 解析Credential Scope、SignedHeaders、Signature三段
    parts = dict(
        item.strip().split("=", 1)
        for item in auth_header.replace("AWS4-HMAC-SHA256 ", "").split(",")
    )
    credential = parts["Credential"]
    access_key, date, region, service, _ = credential.split("/")
    client_signature = parts["Signature"]

    secret_key = get_secret_key(access_key)
    k_date = hmac_sha256(("AWS4" + secret_key).encode(), date)
    k_region = hmac_sha256(k_date, region)
    k_service = hmac_sha256(k_region, service)
    k_signing = hmac_sha256(k_service, "aws4_request")

    string_to_sign = build_string_to_sign(request)  # 按规范拼接
    server_signature = hmac.new(
        k_signing, string_to_sign.encode(), hashlib.sha256
    ).hexdigest()

    if not hmac.compare_digest(server_signature, client_signature):
        raise HTTPException(status_code=403, detail="签名校验失败")

@app.put("/bucket/{key}")
async def put_object(key: str, request: Request, _=Depends(verify_sigv4)):
    return {"status": "ok", "key": key}

这里用hmac.compare_digest做比对而不是直接用等号,是为了避免计时攻击,这个细节在写鉴权代码时容易被忽略,也算是个小提醒。


在RustFS中怎么操作

RustFS是用Rust写的S3兼容对象存储系统,走的路线跟MinIO类似,目标就是让aws-cli、boto3、各种S3 SDK不改一行代码就能直连。要做到这一点,SigV4校验就不是可选项,而是必须原生支持的核心能力。

从架构上看,RustFS的网关层通常会挂一个鉴权中间件,拦截每一个进来的请求,做的事情跟前面FastAPI示例里那段verify_sigv4几乎一模一样,只是换成了Rust的写法,并且因为是系统级实现,性能会打磨得更极致。

大致的处理链路是这样的。

flowchart LR A[客户端请求] --> B[TLS终止] B --> C[SigV4中间件] C --> D{签名是否合法} D -->|合法| E[对象存储核心引擎] D -->|不合法| F[返回403]

在Rust生态里,常见做法是使用aws-sigv4这个crate来复用官方的签名算法实现,避免自己手写HMAC链条踩坑。一个校验中间件的思路大致如下。

rust 复制代码
use aws_sigv4::http_request::{SignableRequest, SignableBody};
use hyper::{Request, Body, StatusCode};

async fn verify_sigv4_middleware(
    req: Request<Body>,
    secret_key_lookup: impl Fn(&str) -> Option<String>,
) -> Result<Request<Body>, StatusCode> {
    let auth_header = req.headers()
        .get("Authorization")
        .and_then(|v| v.to_str().ok())
        .ok_or(StatusCode::UNAUTHORIZED)?;

    let access_key = extract_access_key(auth_header)
        .ok_or(StatusCode::UNAUTHORIZED)?;

    let secret_key = secret_key_lookup(&access_key)
        .ok_or(StatusCode::FORBIDDEN)?;

    let expected_signature = compute_signature(&req, &secret_key);
    let client_signature = extract_signature(auth_header)
        .ok_or(StatusCode::UNAUTHORIZED)?;

    if expected_signature != client_signature {
        return Err(StatusCode::FORBIDDEN);
    }

    Ok(req)
}

对使用者来说,实际操作上更关心的是配置层面的东西,通常包括创建access key和secret key对(一般在RustFS的管理界面或者配置文件里生成)、设置bucket的访问策略、以及确保客户端SDK里配置的region和endpoint跟RustFS服务端约定的一致,因为region字段会参与到credential scope的计算里,一旦客户端和服务端对region的理解不一致,签名校验百分之百会失败。

日常调试中最容易踩的坑集中在几个地方,一个是系统时钟不同步导致时间戳超出容忍窗口,一个是请求体的hash值计算错误(比如流式上传时body hash用了UNSIGNED-PAYLOAD却在校验端按普通哈希处理),还有一个是自定义header没有按字典序排进SignedHeaders里,这些细节虽然琐碎,但恰恰是SigV4实现里最容易出bug的地方。


说到底

HTTPS管的是路上的安全,SigV4管的是每一份货是谁寄的、有没有被人动过手脚。两者叠在一起用,才能构成一个既保密又可信的完整链路,这也是为什么几乎所有正经的S3兼容存储系统,无论是AWS自己的S3,还是RustFS这类开源实现,都会把这两层同时做实,缺一不可。

对开发者来说,理解清楚这两套机制的边界之后,无论是在FastAPI里写签名逻辑,还是在RustFS这类Rust项目里排查鉴权bug,思路都会清晰很多,不至于把加密通道和身份签名这两个本该分开考虑的问题混在一起纠结。


参考资料

Create a signed AWS API request, AWS IAM User Guide, docs.aws.amazon.com

AWS Signature Version 4 for API requests, AWS IAM User Guide, docs.aws.amazon.com

What Happens in a TLS Handshake, Cloudflare Learning Center, cloudflare.com

How to verify a signed AWS request from custom RESTful service, Reddit r/aws讨论, reddit.com

相关推荐
青石路14 分钟前
好好的OceanBase官方驱动你不用,非要用第三方驱动,ArrayIndexOutOfBoundsException了吧
java·后端
qq_4260039615 分钟前
多语言新增语种全量测试策略的测试范围
前端·javascript·python·自动化
SamChan9017 分钟前
Python+ReportLab自动生成PDF翻译质量审计报告:从数据到可视化的完整方案
开发语言·python·ai·pdf·wpf
whcyhhh19 分钟前
头歌实践教学平台:数据科学与大数据技术导论(十八2)
大数据·开发语言·python
一晌小贪欢20 分钟前
Python办公18:PDF 转 Word——利用 OCR 技术批量提取不可编辑的文档内容
开发语言·python·pdf·word·excel·数据可视化·python办公
wtGEOyh28 分钟前
2026:劲豆如何用科技“种”出大豆芯?
python·科技
深念Y30 分钟前
微服务抽取路线图:从胖单体到 ARM 集群
前端·arm开发·数据库·后端·微服务·云原生·架构
苏灿烤鱼30 分钟前
一个用 Nim 写的隐私优先 Twitter 替代前端,源码级技术剖析
后端·github·twitter
SamChan9037 分钟前
用Prometheus+Grafana搭建PDF翻译服务监控看板:指标采集与告警实战
python·ai·pdf·grafana·prometheus·机器翻译