做云原生开发或者对接S3兼容存储的时候,经常会碰到一个让人纠结的问题------已经用了HTTPS了,为什么请求头里还要塞一个又长又丑的Authorization字段,里面写满了SigV4之类的东西。是不是重复加密了,是不是多此一举。
答案恰恰相反。这两套机制根本不在同一个维度上工作,一个管的是通道安全,一个管的是身份和内容的真实性。搞清楚这个区别,才能理解为什么AWS的S3、以及像RustFS这类S3兼容存储系统,会同时要求HTTPS加身、又要求每个请求都带着SigV4签名。
HTTPS负责的是通道,不是内容本身
HTTPS本质上是HTTP套了一层TLS。它解决的问题很纯粹,就是让客户端和服务器之间的通信不被第三方偷看、不被中途篡改、并且能确认对方的身份没被冒充。
TLS握手的大致流程是这样的,客户端发起连接后先和服务器交换支持的加密套件,服务器把自己的数字证书发过去,客户端验证这张证书是不是权威CA签发的、域名对不对,验证通过后双方通过非对称加密协商出一个只有彼此知道的会话密钥,后续所有数据都用这个对称密钥加密传输。
这里的关键词是通道。TLS只关心从A到B这条管道有没有被人截获或者篡改,它完全不关心管道里跑的是谁的请求、这个请求有没有权限访问某个资源。换句话说,一个持有合法HTTPS证书的服务器,可以毫无阻碍地加密传输任何内容,哪怕这个内容是一个没有权限的人发起的恶意请求。
TLS握手结束后拿到的会话密钥,可以简单表示成下面这种非对称交换后的对称密钥关系。
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)
kregion=HMAC(kdate,region)
kservice=HMAC(kregion,service)
ksigning=HMAC(kservice,"aws4_request")
这样做的好处是即便某一天的签名密钥意外泄露,攻击者也只能在那个特定日期、特定区域、特定服务的范围内伪造请求,爆炸半径被死死限制住了。
第四步,计算最终签名。 用上面推导出的 ksigning对string to sign做一次HMAC-SHA256,得到的十六进制字符串就是最终的signature,塞进Authorization请求头一起发出去。
服务器收到请求后,会拿同样的方法在本地重新算一遍签名,两个签名字符串完全一致才算通过,任何一个字符的偏差都会导致校验失败。这也是为什么很多人对接S3 API时,时钟稍微差个几分钟就会报SignatureDoesNotMatch的原因,因为时间戳也是参与签名计算的一部分。
用一张图看清两者所在的层级
从这张图能看得很清楚,TLS工作在传输层和应用层之间,负责整条管道的保密性和完整性。SigV4工作在应用层内部,是HTTP请求本身携带的一段元数据,负责证明这条请求的发起者身份和内容没被中途换掉。两者互不冲突,反而是互补关系,缺了HTTPS,SigV4签名本身以及请求里携带的access key ID都可能在传输中被窃听(虽然签名本身重放攻击有时效窗口限制,但敏感信息暴露的风险仍然存在)。缺了SigV4,任何拿到URL的人都能对着一个加密良好的通道发起未授权请求。
核心概念一览表
| 维度 | HTTPS / TLS | AWS SigV4 |
|---|---|---|
| 所在层级 | 传输层与应用层之间 | 应用层,HTTP请求头内 |
| 解决的问题 | 防窃听、防篡改、身份认证(服务器) | 请求身份认证、内容完整性校验 |
| 是否加密内容 | 是,对称加密整条通道 | 否,仅做哈希与签名,不加密 |
| 密钥类型 | 会话密钥,连接结束即失效 | 长期secret key派生出的短期签名密钥 |
| 校验方 | 客户端校验服务器证书 | 服务器校验客户端签名 |
| 典型有效期 | 单次TLS会话周期 | 通常15分钟内有效(X-Amz-Expires控制) |
SigV4签名生成的完整流程图
在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的写法,并且因为是系统级实现,性能会打磨得更极致。
大致的处理链路是这样的。
在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