← 中文资源

简体中文版本

请求签名

请求签名给每个 HTTP 请求附加密码学签名,证明调用方身份,并把签名与方法、路径、请求头和正文绑定。

阅读英文原文

签名证明什么

请求签名把共享密钥或私钥绑定到请求的确切字节。接收方可以确认调用方持有密钥,且方法、路径、请求头和负载未被篡改。它提供完整性与身份认证,不是加密:除非 TLS 保护,内容仍然可见。

工作机制

RFC 9421 定义了由选定组件构成的规范化形式。发送方在 Signature-Input 中列出组件,例如 sig1=("@method" "@path" "content-digest");created=1788307200;keyid="k1",再发送 Signature: sig1=:base64value: 和正文的 Content-Digest。接收方重建相同字符串,用登记的密钥验签,检查创建时间是否在短窗口内,并可选记录 nonce 防重放。

常见替代方案是对排序后的参数串做 HMAC-SHA256,或使用非对称签名,让验证方不持有签名密钥。当多个接收方都要验证且都不应能伪造时,非对称更合适。

何时使用与取舍

跨信任边界的服务间调用、Webhook 投递和支付指令都应签名。请求经过会终止 TLS 的代理或网关时,签名仍然有效。浏览器客户端无法保存密钥,应改用短期令牌。

规范化是最脆弱的部分:头顺序、空白、重复头、URL 编码必须完全一致。签名大正文需要缓冲或流式摘要。时钟会漂移,窗口要容忍偏差又不能允许重放。密钥分发与轮换也增加运维工作。

示例与常见误解

某 Webhook 提供方对 POST /hooks 的请求体摘要和时间戳签名。接收方只校验正文,攻击者篡改 X-Tenant 头后重放仍会通过;校验完整组件列表和时间窗口才能拒绝。

常见误解是签名可以替代 TLS。两者互补:TLS 保护通道,签名保护请求免受中间人和重放影响。相关概念:Webhooks、幂等性、JSON Web Token。

代码示例

package main

import (
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"net/http/httptest"
	"strings"
)

type Middleware func(http.Handler) http.Handler

type Payload struct {
	Name string `json:"name"`
}

func LimitBody(maxBytes int64) Middleware {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			r.Body = http.MaxBytesReader(w, r.Body, maxBytes)
			next.ServeHTTP(w, r)
		})
	}
}

func decodePayload(w http.ResponseWriter, r *http.Request) (Payload, error) {
	var payload Payload
	decoder := json.NewDecoder(r.Body)
	decoder.DisallowUnknownFields()
	if err := decoder.Decode(&payload); err != nil {
		var tooLarge *http.MaxBytesError
		if errors.As(err, &tooLarge) {
			http.Error(w, fmt.Sprintf("body exceeds %d bytes", tooLarge.Limit), http.StatusRequestEntityTooLarge)
			return payload, err
		}
		http.Error(w, "invalid JSON body", http.StatusBadRequest)
		return payload, err
	}
	if strings.TrimSpace(payload.Name) == "" {
		http.Error(w, "name is required", http.StatusUnprocessableEntity)
		return payload, errors.New("name is required")
	}
	return payload, nil
}

func main() {
	handler := LimitBody(64)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		payload, err := decodePayload(w, r)
		if err != nil {
			return
		}
		w.WriteHeader(http.StatusCreated)
		fmt.Fprintf(w, "created %s\n", payload.Name)
	}))

	bodies := []string{
		`{"name":"ada"}`,
		`{"name":"` + strings.Repeat("x", 80) + `"}`,
	}
	for _, body := range bodies {
		request := httptest.NewRequest(http.MethodPost, "/users", strings.NewReader(body))
		recorder := httptest.NewRecorder()
		handler.ServeHTTP(recorder, request)
		fmt.Printf("status=%d body=%q\n", recorder.Code, strings.TrimSpace(recorder.Body.String()))
	}
}
import re
from pathlib import PurePosixPath
from urllib.parse import unquote

NAME_STAR = re.compile(r"filename\*\s*=\s*([^;]+)", re.IGNORECASE)
NAME = re.compile(r'filename\s*=\s*(?:"([^"]*)"|([^;]+))', re.IGNORECASE)


def safe_basename(value: str) -> str | None:
    candidate = PurePosixPath(value.replace("\\", "/")).name.strip()
    if candidate in {"", ".", ".."} or "\x00" in candidate:
        return None
    return candidate


def attachment_filename(header: str) -> str | None:
    match = NAME_STAR.search(header)
    if match:
        value = match.group(1).strip().strip('"')
        charset, _, remainder = value.partition("'")
        _, _, encoded = remainder.partition("'")
        try:
            decoded = unquote(
                encoded or remainder,
                encoding=charset or "utf-8",
                errors="strict",
            )
        except (LookupError, UnicodeDecodeError):
            decoded = unquote(encoded or remainder)
        return safe_basename(decoded)
    match = NAME.search(header)
    if not match:
        return None
    value = match.group(1) if match.group(1) is not None else match.group(2)
    return safe_basename(unquote(value.strip()))


def main() -> None:
    headers = [
        'attachment; filename="report final.pdf"',
        "attachment; filename=../../etc/passwd",
        "attachment; filename*=UTF-8''%E5%B9%B4%E6%8A%A5.pdf",
        'attachment; filename=".."',
    ]
    for header in headers:
        print(attachment_filename(header))


if __name__ == "__main__":
    main()