openapi.md 5.6 KB

基于 HMAC 的 API Key + Secret 鉴权实现

1. 导入相关依赖

package main

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "fmt"
    "log"
    "net/http"
    "time"
)

const apiKeyHeader = "X-API-KEY"
const apiSignatureHeader = "X-API-SIGNATURE"
const apiTimestampHeader = "X-API-TIMESTAMP"

// 这些 API Key 和 Secret 应该存储在安全的存储中,例如数据库或配置文件
var validAPIKeys = map[string]string{
    "your-api-key": "your-api-secret",
}

// 验证签名是否有效
func isValidSignature(apiKey, signature, timestamp string) bool {
    // 使用提供的 API Key 查找对应的 API Secret
    secret, ok := validAPIKeys[apiKey]
    if !ok {
        return false
    }

    // 计算签名,签名内容是 "apiKey + timestamp"
    message := apiKey + timestamp
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write([]byte(message))
    expectedSignature := hex.EncodeToString(mac.Sum(nil))

    // 验证客户端提供的签名是否与预期签名匹配
    return hmac.Equal([]byte(signature), []byte(expectedSignature))
}

func apiKeyAuthMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        apiKey := r.Header.Get(apiKeyHeader)
        signature := r.Header.Get(apiSignatureHeader)
        timestamp := r.Header.Get(apiTimestampHeader)

        // 检查 API Key, 签名和时间戳是否存在
        if apiKey == "" || signature == "" || timestamp == "" {
            http.Error(w, "API Key, Signature, and Timestamp required", http.StatusUnauthorized)
            return
        }

        // 校验请求的签名是否有效
        if !isValidSignature(apiKey, signature, timestamp) {
            http.Error(w, "Invalid Signature", http.StatusForbidden)
            return
        }

        // 校验时间戳是否在合理范围内(防止重放攻击)
        reqTime, err := time.Parse(time.RFC3339, timestamp)
        if err != nil || time.Since(reqTime) > 5*time.Minute {
            http.Error(w, "Request too old or invalid timestamp", http.StatusForbidden)
            return
        }

        // 如果鉴权成功,继续处理请求
        next.ServeHTTP(w, r)
    })
}

func helloHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello, World!")
}

func main() {
    // 路由定义
    mux := http.NewServeMux()

    // 在 API 处理器中使用中间件进行鉴权
    mux.Handle("/", apiKeyAuthMiddleware(http.HandlerFunc(helloHandler)))

    // 启动服务器
    log.Println("Server starting on :8080")
    log.Fatal(http.ListenAndServe(":8080", mux))
}

2. 代码详解

1. HMAC 签名验证
  • 使用 HMAC-SHA256 算法来对请求进行签名。服务器和客户端都需要拥有一对 API KeyAPI Secret。客户端用它们生成签名,服务端用相同的算法生成签名来进行比对。
  • 具体流程如下:
    1. 服务器通过 apiKey 在内存或数据库中查找对应的 secret
    2. 服务器生成一个字符串,这个字符串由 apiKeytimestamp 组成。
    3. 使用 HMAC-SHA256 算法生成签名。
    4. 将生成的签名与客户端提供的签名进行对比,若匹配则鉴权通过。
2. 防止重放攻击
  • 在请求头中加入一个时间戳 X-API-TIMESTAMP,服务器验证时间戳是否在当前时间的合理范围内(例如5分钟内)。这有助于防止旧请求被恶意重放。
3. 主函数
  • 服务器的每个请求都会通过 apiKeyAuthMiddleware 中间件来进行 API Key、签名以及时间戳的校验。
  • 如果签名和时间戳都有效,允许请求继续处理,否则返回 401403 错误。

3. 客户端请求的生成方法

客户端在发出请求时,需要生成以下内容:

  1. API Key: 作为请求头的一部分发送,表示客户端的身份。
  2. 时间戳: 使用 RFC3339 格式化的当前时间(例如 "2024-09-20T12:00:00Z")。
  3. 签名: 使用 HMAC-SHA256 算法生成的签名,签名的内容是 apiKey + timestamp 的组合,密钥为该客户端的 API Secret。

以下是客户端伪代码示例(可以用任意语言实现):

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "net/http"
    "time"
)

func generateSignature(apiKey, apiSecret, timestamp string) string {
    message := apiKey + timestamp
    mac := hmac.New(sha256.New, []byte(apiSecret))
    mac.Write([]byte(message))
    return hex.EncodeToString(mac.Sum(nil))
}

func makeRequest() {
    apiKey := "your-api-key"
    apiSecret := "your-api-secret"
    timestamp := time.Now().UTC().Format(time.RFC3339)
    signature := generateSignature(apiKey, apiSecret, timestamp)

    req, _ := http.NewRequest("GET", "http://localhost:8080", nil)
    req.Header.Set("X-API-KEY", apiKey)
    req.Header.Set("X-API-TIMESTAMP", timestamp)
    req.Header.Set("X-API-SIGNATURE", signature)

    client := &http.Client{}
    res, _ := client.Do(req)
    defer res.Body.Close()
    // 处理响应
}

4. 测试流程

在服务启动后,你可以通过命令行或代码发出请求,携带 API Key、时间戳、签名等。

curl -H "X-API-KEY: your-api-key" \
     -H "X-API-TIMESTAMP: 2024-09-20T12:00:00Z" \
     -H "X-API-SIGNATURE: <计算出的签名>" \
     http://localhost:8080/

如果鉴权成功,返回 "Hello, World!";否则会返回 401 Unauthorized403 Forbidden 错误。

总结

通过使用 HMAC 签名,你可以大大提升 API 鉴权的安全性,并防止重放攻击。