Files
higress/plugins/wasm-go/extensions/hmac-auth-apisix/README.md

14 KiB
Raw Blame History

title, keywords, description
title keywords description
APISIX HMAC 认证
higress
hmac auth
apisix
APISIX HMAC 认证插件配置参考

功能说明

hmac-auth-apisix 插件兼容 Apache APISIX 的 HMAC 认证机制,通过 HMAC 算法为 HTTP 请求生成防篡改的数字签名,实现请求的身份认证和权限控制。该插件完全兼容 Apache APISIX HMAC 认证插件的配置和签名算法,签名生成方法可参考 Apache APISIX HMAC 认证文档

运行属性

插件执行阶段:认证阶段 插件执行优先级:330

配置字段

注意:

  • 在一个规则里,鉴权配置和认证配置不可同时存在
  • 对于通过认证鉴权的请求,请求的 header 会被添加一个 X-Mse-Consumer 字段,用以标识调用者的名称

认证配置

名称 数据类型 填写要求 默认值 描述
global_auth bool 选填(仅实例级别配置 - 只能在实例级别配置,若配置为 true则全局生效认证机制若配置为 false则只对做了配置的域名和路由生效认证机制若不配置则仅当没有域名和路由配置时全局生效兼容老用户使用习惯
consumers array of object 必填 - 配置服务的调用者,用于对请求进行认证
allowed_algorithms array of string 选填 ["hmac-sha1", "hmac-sha256", "hmac-sha512"] 允许的 HMAC 算法列表。有效值为 "hmac-sha1"、"hmac-sha256" 和 "hmac-sha512" 的组合
clock_skew number 选填 300 客户端请求的时间戳与 Higress 服务器当前时间之间允许的最大时间差(以秒为单位)。这有助于解决客户端和服务器之间的时间同步差异,并防止重放攻击。时间戳将根据 Date 头中的时间(必须为 GMT 格式进行计算。如果配置为0会跳过该校验
signed_headers array of string 选填 - 客户端请求的 HMAC 签名中应包含的 HMAC 签名头列表
validate_request_body boolean 选填 false 如果为 true则验证请求正文的完整性以确保在传输过程中没有被篡改。具体来说插件会创建一个 SHA-256 的 base64 编码 digest并将其与 Digest 头进行比较。如果 Digest 头丢失或 digest 不匹配,验证将失败
hide_credentials boolean 选填 false 如果为 true则不会将授权请求头传递给上游服务
anonymous_consumer string 选填 - 匿名消费者名称。如果已配置,则允许匿名用户绕过身份验证

consumers中每一项的配置字段说明如下:

名称 数据类型 填写要求 默认值 描述
access_key string 必填 - 消费者的唯一标识符,用于标识相关配置,例如密钥
secret_key string 必填 - 用于生成 HMAC 的密钥
name string 选填 access_key 配置该 consumer 的名称

鉴权配置(非必需)

名称 数据类型 填写要求 默认值 描述
allow array of string 选填(非实例级别配置) - 只能在路由或域名等细粒度规则上配置,对于符合匹配条件的请求,配置允许访问的 consumer从而实现细粒度的权限控制

配置示例

全局配置认证和路由粒度鉴权

以下配置用于对网关特定路由或域名开启 Hmac Auth 认证和鉴权。注意access_key 字段不可重复

示例1基础路由与域名鉴权配置

实例级别插件配置

global_auth: false
consumers:
- name: consumer1
  access_key: consumer1-key
  secret_key: 2bda943c-ba2b-11ec-ba07-00163e1250b5
- name: consumer2
  access_key: consumer2-key
  secret_key: c8c8e9ca-558e-4a2d-bb62-e700dcc40e35

路由级配置(适用于 route-a 和 route-b

allow: 
- consumer1  # 仅允许consumer1访问

域名级配置(适用于 *.example.comtest.com

allow:
- consumer2  # 仅允许consumer2访问

配置说明

  • 路由名称(如 route-a、route-b对应网关路由创建时定义的名称匹配时仅允许consumer1访问
  • 域名匹配(如 *.example.comtest.com用于过滤请求域名匹配时仅允许consumer2访问
  • 未在allow列表中的调用者将被拒绝访问

生成签名,可以使用以下 Go 代码片段或其他技术栈

package main

import (
	"crypto/hmac"
	"crypto/sha1"
	"crypto/sha256"
	"crypto/sha512"
	"encoding/base64"
	"fmt"
	"hash"
	"strings"
	"time"
)

// SignedHeader 定义签名头的结构
type SignedHeader struct {
	Name  string
	Value string
}

func main() {
	// 配置参数
	keyID := "consumer1-key"                            // key id
	secretKey := "2bda943c-ba2b-11ec-ba07-00163e1250b5" // secret key
	requestMethod := "POST"                             // HTTP method
	requestPath := "/foo"                               // Route URI
	algorithm := "hmac-sha256"                          // algorithm
	validateRequestBody := false                        // 是否验证请求体设置为true时会添加Digest头部

	// 如果配置了 signed_headers则需要按照顺序添加
	signedHeaders := []SignedHeader{
		//{Name: "x-custom-header-a", Value: "test1"},
		//{Name: "x-custom-header-b", Value: "test2"},
	}

	body := []byte("{}") // request body

	// 获取当前 GMT 时间
	gmtTime := time.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05 GMT")

	// 动态构造签名字符串(有序)
	signingStringBuilder := strings.Builder{}
	signingStringBuilder.WriteString(fmt.Sprintf("%s\n%s %s\ndate: %s\n",
		keyID,
		requestMethod,
		requestPath,
		gmtTime))

	// 按照signedHeaders中的顺序添加header
	for _, header := range signedHeaders {
		signingStringBuilder.WriteString(fmt.Sprintf("%s: %s\n", header.Name, header.Value))
	}

	signingString := signingStringBuilder.String()

	// 创建签名
	signature, err := generateHmacSignature(secretKey, algorithm, signingString)
	if err != nil {
		fmt.Printf("Error generating signature: %v\n", err)
		return
	}

	// 动态构建headers字段内容
	headersField := "@request-target date"
	for _, header := range signedHeaders {
		headersField += " " + header.Name
	}

	// 构造请求头部
	headers := map[string]string{
		"Date": gmtTime,
		"Authorization": fmt.Sprintf(`Signature keyId="%s",algorithm="%s",headers="%s",signature="%s"`,
			keyID,
			algorithm,
			headersField,
			signature,
		),
	}

	// 如果需要验证请求体则添加Digest头部
	if validateRequestBody {
		headers["Digest"] = calculateBodyDigest(body)
	}

	// 添加签名的请求头
	for _, header := range signedHeaders {
		formattedHeaderName := formatHeaderName(header.Name)
		headers[formattedHeaderName] = header.Value
	}

	// 打印签名字符串
	fmt.Printf("signingString: %s\n", signingString)
	// 打印请求头
	fmt.Println("Headers:")
	for key, value := range headers {
		fmt.Printf("%s: %s\n", key, value)
	}
}

// generateHmacSignature 生成HMAC签名
func generateHmacSignature(secretKey, algorithm, message string) (string, error) {
	var mac hash.Hash

	switch algorithm {
	case "hmac-sha1":
		mac = hmac.New(sha1.New, []byte(secretKey))
	case "hmac-sha256":
		mac = hmac.New(sha256.New, []byte(secretKey))
	case "hmac-sha512":
		mac = hmac.New(sha512.New, []byte(secretKey))
	default:
		return "", fmt.Errorf("unsupported algorithm: %s", algorithm)
	}

	mac.Write([]byte(message))
	signature := mac.Sum(nil)
	return base64.StdEncoding.EncodeToString(signature), nil
}

// calculateBodyDigest 计算body的摘要
func calculateBodyDigest(body []byte) string {
	hash := sha256.Sum256(body)
	encodedDigest := base64.StdEncoding.EncodeToString(hash[:])
	return "SHA-256=" + encodedDigest
}

// formatHeaderName 将header name转换为标准HTTP头格式
func formatHeaderName(headerName string) string {
	parts := strings.Split(headerName, "-")
	for i, part := range parts {
		if len(part) > 0 {
			parts[i] = strings.ToUpper(part[:1]) + strings.ToLower(part[1:])
		}
	}
	return strings.Join(parts, "-")
}

请求与响应示例

  1. 验证通过场景
curl -X POST 'http://localhost:8082/foo' \
-H 'Authorization:Signature keyId="consumer1-key",algorithm="hmac-sha256",headers="@request-target date",signature="746z4VISwZehUwZdzTV486ZMMbBtakmMHKPfs/A4RdU="' \
-H 'Date:Fri, 12 Sep 2025 23:53:18 GMT' \
-H 'Content-Type: application/json' \
-d '{}'
  • 响应:返回后端服务正常响应
  • 附加信息:认证通过后会自动添加请求头 X-Mse-Consumer: consumer1 传递给后端
  1. 请求方法修改导致验签失败
curl -X PUT 'http://localhost:8082/foo' \  # 此处将POST改为PUT
-H 'Authorization:Signature keyId="consumer1-key",algorithm="hmac-sha256",headers="@request-target date",signature="746z4VISwZehUwZdzTV486ZMMbBtakmMHKPfs/A4RdU="' \
-H 'Date:Fri, 12 Sep 2025 23:53:18 GMT' \
-H 'Content-Type: application/json' \
-d '{}'
  • 响应:401 Unauthorized
  • 错误信息:{"message":"client request can't be validated: Invalid signature"}
  1. 不在允许列表中的调用者
curl -X POST 'http://localhost:8082/foo' \
-H 'Authorization:Signature keyId="consumer2-key",algorithm="hmac-sha256",headers="@request-target date",signature="dltotPwd4iWGGz//kuehPJlHXZemR5WKwCPAJD/KPhE="' \
-H 'Date:Fri, 12 Sep 2025 23:59:01 GMT' \
-H 'Content-Type: application/json' \
-d '{}'
  • 响应:401 Unauthorized
  • 错误信息:{"message":"client request can't be validated: consumer 'consumer2' is not allowed"}
  1. 时间戳过期
curl -X POST 'http://localhost:8082/foo' \
-H 'Authorization:Signature keyId="consumer1-key",algorithm="hmac-sha256",headers="@request-target date",signature="746z4VISwZehUwZdzTV486ZMMbBtakmMHKPfs/A4RdU="' \
-H 'Date:Fri, 12 Sep 2025 23:53:18 GMT' \  # 过期的时间戳
-H 'Content-Type: application/json' \
-d '{}'
  • 响应:401 Unauthorized
  • 错误信息:{"message":"client request can't be validated: Clock skew exceeded"}

示例2带自定义签名头与请求体验证的配置

实例级别插件配置

global_auth: false
consumers:
- name: consumer1
  access_key: consumer1-key
  secret_key: 2bda943c-ba2b-11ec-ba07-00163e1250b5
- name: consumer2
  access_key: consumer2-key
  secret_key: c8c8e9ca-558e-4a2d-bb62-e700dcc40e35
signed_headers:  # 需要纳入签名的自定义请求头
- X-Custom-Header-A
- X-Custom-Header-B
validate_request_body: true  # 启用请求体签名校验

请求与响应示例

  1. 验证通过场景
curl -X POST 'http://localhost:8082/foo' \
-H 'Authorization:Signature keyId="consumer1-key",algorithm="hmac-sha256",headers="@request-target date x-custom-header-a x-custom-header-b",signature="KoOlbkDIR/JzlKK47eURewnIpmhpkQU+KIyBUhqVfmo="' \
-H 'Date:Sat, 13 Sep 2025 00:04:34 GMT' \
-H 'Digest:SHA-256=RBNvo1WzZ4oRRq0W9+hknpT7T8If536DEMBg9hyq/4o=' \  # 请求体摘要
-H 'X-Custom-Header-A:test1' \
-H 'X-Custom-Header-B:test2' \
-H 'Content-Type: application/json' \
-d '{}'
  • 响应:返回后端服务正常响应
  1. 缺少签名头
curl -X POST 'http://localhost:8082/foo' \
-H 'Authorization:Signature keyId="consumer1-key",algorithm="hmac-sha256",headers="@request-target date x-custom-header-b",signature="KoOlbkDIR/JzlKK47eURewnIpmhpkQU+KIyBUhqVfmo="' \
-H 'Date:Sat, 13 Sep 2025 00:04:34 GMT' \
-H 'Digest:SHA-256=RBNvo1WzZ4oRRq0W9+hknpT7T8If536DEMBg9hyq/4o=' \
-H 'X-Custom-Header-B:test2' \  # 缺少X-Custom-Header-A
-H 'Content-Type: application/json' \
-d '{}'
  • 响应:401 Unauthorized
  • 错误信息:{"message":"client request can't be validated: expected header "X-Custom-Header-A" missing in signing"}
  1. 请求体被篡改
curl -X POST 'http://localhost:8082/foo' \
-H 'Authorization:Signature keyId="consumer1-key",algorithm="hmac-sha256",headers="@request-target date x-custom-header-a x-custom-header-b",signature="NcA+44FFtl2rjNvV28wSn8Rln02i4i2tFXKp3/ahyYA="' \
-H 'Date:Sat, 13 Sep 2025 00:09:40 GMT' \
-H 'Digest:SHA-256=RBNvo1WzZ4oRRq0W9+hknpT7T8If536DEMBg9hyq/4o=' \
-H 'X-Custom-Header-A:test1' \
-H 'X-Custom-Header-B:test2' \
-H 'Content-Type: application/json' \
-d '{"key":"value"}'  # 篡改后的请求体
  • 响应:401 Unauthorized
  • 错误信息:{"message":"client request can't be validated: Invalid digest"}

网关实例级别开启全局认证

以下配置将在网关实例级别开启 Hmac Auth 认证,所有请求必须经过认证才能访问

global_auth: true  # 开启全局认证
consumers:
- name: consumer1
  access_key: consumer1-key
  secret_key: 2bda943c-ba2b-11ec-ba07-00163e1250b5
- name: consumer2
  access_key: consumer2-key
  secret_key: c8c8e9ca-558e-4a2d-bb62-e700dcc40e35

说明:当 global_auth: true 时,所有访问网关的请求都需要携带有效的认证信息,未认证的请求将被直接拒绝