mirror of
https://github.com/alibaba/higress.git
synced 2026-02-06 23:21:08 +08:00
feat(wasm-plugin): add tests and docs for hmac-auth-apisix (#2842)
This commit is contained in:
209
plugins/wasm-go/extensions/hmac-auth-apisix/README.md
Normal file
209
plugins/wasm-go/extensions/hmac-auth-apisix/README.md
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
---
|
||||||
|
title: APISIX HMAC 认证
|
||||||
|
keywords: [higress,hmac auth,apisix]
|
||||||
|
description: APISIX HMAC 认证插件配置参考
|
||||||
|
---
|
||||||
|
|
||||||
|
## 功能说明
|
||||||
|
|
||||||
|
`hmac-auth-apisix` 插件兼容 Apache APISIX 的 HMAC 认证机制,通过 HMAC 算法为 HTTP 请求生成防篡改的数字签名,实现请求的身份认证和权限控制。该插件完全兼容 Apache APISIX HMAC 认证插件的配置和签名算法,签名生成方法可参考 [Apache APISIX HMAC 认证文档](https://apisix.apache.org/docs/apisix/plugins/hmac-auth/)
|
||||||
|
|
||||||
|
## 运行属性
|
||||||
|
|
||||||
|
插件执行阶段:`认证阶段`
|
||||||
|
插件执行优先级:`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:基础路由与域名鉴权配置
|
||||||
|
|
||||||
|
**实例级别插件配置**:
|
||||||
|
```yaml
|
||||||
|
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):
|
||||||
|
```yaml
|
||||||
|
allow:
|
||||||
|
- consumer1 # 仅允许consumer1访问
|
||||||
|
```
|
||||||
|
|
||||||
|
**域名级配置**(适用于 `*.example.com` 和 `test.com`):
|
||||||
|
```yaml
|
||||||
|
allow:
|
||||||
|
- consumer2 # 仅允许consumer2访问
|
||||||
|
```
|
||||||
|
|
||||||
|
**配置说明**:
|
||||||
|
- 路由名称(如 route-a、route-b)对应网关路由创建时定义的名称,匹配时仅允许consumer1访问
|
||||||
|
- 域名匹配(如 `*.example.com`、`test.com`)用于过滤请求域名,匹配时仅允许consumer2访问
|
||||||
|
- 未在allow列表中的调用者将被拒绝访问
|
||||||
|
|
||||||
|
**请求与响应示例**:
|
||||||
|
|
||||||
|
1. **验证通过场景**
|
||||||
|
```shell
|
||||||
|
curl -X POST 'http://localhost:8082/foo' \
|
||||||
|
-H 'Authorization:Signature keyId="consumer1-key",algorithm="hmac-sha256",headers="@request-target date",signature="G2+60rCCHQCQDZOailnKHLCEy++P1Pa5OEP1bG4QlRo="' \
|
||||||
|
-H 'Date:Sat, 30 Aug 2025 00:52:39 GMT' \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{}'
|
||||||
|
```
|
||||||
|
- 响应:返回后端服务正常响应
|
||||||
|
- 附加信息:认证通过后会自动添加请求头 `X-Mse-Consumer: consumer1` 传递给后端
|
||||||
|
|
||||||
|
2. **请求方法修改导致验签失败**
|
||||||
|
```shell
|
||||||
|
curl -X PUT 'http://localhost:8082/foo' \ # 此处将POST改为PUT
|
||||||
|
-H 'Authorization:Signature keyId="consumer1-key",algorithm="hmac-sha256",headers="@request-target date",signature="G2+60rCCHQCQDZOailnKHLCEy++P1Pa5OEP1bG4QlRo="' \
|
||||||
|
-H 'Date:Sat, 30 Aug 2025 00:52:39 GMT' \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{}'
|
||||||
|
```
|
||||||
|
- 响应:`401 Unauthorized`
|
||||||
|
- 错误信息:`{"message":"client request can't be validated: Invalid signature"}`
|
||||||
|
|
||||||
|
3. **不在允许列表中的调用者**
|
||||||
|
```shell
|
||||||
|
curl -X POST 'http://localhost:8082/foo' \
|
||||||
|
-H 'Authorization:Signature keyId="consumer2-key",algorithm="hmac-sha256",headers="@request-target date",signature="5sqSbDX9b91dQsfQra2hpluM7O6/yhS7oLcKPQylyCo="' \
|
||||||
|
-H 'Date:Sat, 30 Aug 2025 00:54:18 GMT' \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{}'
|
||||||
|
```
|
||||||
|
- 响应:`401 Unauthorized`
|
||||||
|
- 错误信息:`{"message":"client request can't be validated: consumer 'consumer2' is not allowed"}`
|
||||||
|
|
||||||
|
4. **时间戳过期**
|
||||||
|
```shell
|
||||||
|
curl -X POST 'http://localhost:8082/foo' \
|
||||||
|
-H 'Authorization: Signature keyId="consumer1-key",algorithm="hmac-sha256",headers="@request-target date",signature="gvIUwoYNiK57w6xX2g1Ntpk8lfgD7z+jgom434r5qwg="' \
|
||||||
|
-H 'Date: Sat, 30 Aug 2025 00:40:21 GMT' \ # 过期的时间戳
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{}'
|
||||||
|
```
|
||||||
|
- 响应:`401 Unauthorized`
|
||||||
|
- 错误信息:`{"message":"client request can't be validated: Clock skew exceeded"}`
|
||||||
|
|
||||||
|
#### 示例2:带自定义签名头与请求体验证的配置
|
||||||
|
|
||||||
|
**实例级别插件配置**:
|
||||||
|
```yaml
|
||||||
|
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. **验证通过场景**
|
||||||
|
```shell
|
||||||
|
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="+xCWYCmidq3Sisn08N54NWaau5vSY9qEanWoO9HD4mA="' \
|
||||||
|
-H 'Date:Sat, 30 Aug 2025 01:04:06 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 '{}'
|
||||||
|
```
|
||||||
|
- 响应:返回后端服务正常响应
|
||||||
|
|
||||||
|
2. **缺少签名头**
|
||||||
|
```shell
|
||||||
|
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="+xCWYCmidq3Sisn08N54NWaau5vSY9qEanWoO9HD4mA="' \
|
||||||
|
-H 'Date:Sat, 30 Aug 2025 01:04:06 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"}`
|
||||||
|
|
||||||
|
3. **请求体被篡改**
|
||||||
|
```shell
|
||||||
|
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="dSbv6pdQOcgkN89TmSxiT8F9nypbPUqAR2E7ELL8K2s="' \
|
||||||
|
-H 'Date:Sat, 30 Aug 2025 01:10:17 GMT' \
|
||||||
|
-H 'Digest:SHA-256=RBNvo1WzZ4oRRq0W9+hknpT7T8If536DEMBg9hyq/4o=' \ # 与实际body不匹配
|
||||||
|
-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 认证,**所有请求必须经过认证才能访问**:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
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` 时,所有访问网关的请求都需要携带有效的认证信息,未认证的请求将被直接拒绝
|
||||||
215
plugins/wasm-go/extensions/hmac-auth-apisix/README_EN.md
Normal file
215
plugins/wasm-go/extensions/hmac-auth-apisix/README_EN.md
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
# APISIX HMAC Authentication
|
||||||
|
keywords: [higress, hmac auth, apisix]
|
||||||
|
description: Configuration Reference for APISIX HMAC Authentication Plugin
|
||||||
|
---
|
||||||
|
|
||||||
|
## Feature Description
|
||||||
|
The `hmac-auth-apisix` plugin is compatible with Apache APISIX's HMAC authentication mechanism. It generates tamper-proof digital signatures for HTTP requests using the HMAC algorithm, enabling request identity authentication and permission control. This plugin is fully compatible with the configuration and signature algorithm of the Apache APISIX HMAC Authentication Plugin. For signature generation methods, please refer to the [Apache APISIX HMAC Authentication Documentation](https://apisix.apache.org/docs/apisix/plugins/hmac-auth/).
|
||||||
|
|
||||||
|
|
||||||
|
## Operational Attributes
|
||||||
|
- Plugin Execution Phase: `Authentication Phase`
|
||||||
|
- Plugin Execution Priority: `330`
|
||||||
|
|
||||||
|
|
||||||
|
## Configuration Fields
|
||||||
|
**Note:**
|
||||||
|
- In a single rule, authentication configuration and authorization configuration cannot coexist.
|
||||||
|
- For requests that pass authentication and authorization, a `X-Mse-Consumer` field will be added to the request header to identify the caller's name.
|
||||||
|
|
||||||
|
|
||||||
|
### Authentication Configuration
|
||||||
|
|
||||||
|
| Name | Data Type | Requirements | Default Value | Description |
|
||||||
|
| ----------------------- | ---------------- | ----------------------------------------- | --------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| `global_auth` | bool | Optional (**Instance-level configuration only**) | - | Can only be configured at the instance level. If set to `true`, the authentication mechanism takes effect globally; if set to `false`, authentication only applies to domains and routes with specific configurations. If not configured, it takes effect globally only when there are no domain or route configurations (to maintain compatibility with legacy user habits). |
|
||||||
|
| `consumers` | array of object | Required | - | Configures service callers for request authentication. |
|
||||||
|
| `allowed_algorithms` | array of string | Optional | ["hmac-sha1", "hmac-sha256", "hmac-sha512"] | List of allowed HMAC algorithms. Valid values are combinations of "hmac-sha1", "hmac-sha256", and "hmac-sha512". |
|
||||||
|
| `clock_skew` | number | Optional | 300 | Maximum allowed time difference (in seconds) between the timestamp of the client request and the current time of the Higress server. This helps resolve time synchronization differences between the client and server and prevents replay attacks. The timestamp is calculated based on the time in the `Date` header (must be in GMT format). If set to `0`, this check is skipped. |
|
||||||
|
| `signed_headers` | array of string | Optional | - | List of HTTP headers that should be included in the HMAC signature of the client request. |
|
||||||
|
| `validate_request_body` | boolean | Optional | false | If set to `true`, the integrity of the request body is verified to ensure no tampering during transmission. Specifically, the plugin creates a SHA-256 base64-encoded digest and compares it with the `Digest` header. Verification fails if the `Digest` header is missing or the digest does not match. |
|
||||||
|
| `hide_credentials` | boolean | Optional | false | If set to `true`, the authorization request header will not be passed to the upstream service. |
|
||||||
|
| `anonymous_consumer` | string | Optional | - | Name of the anonymous consumer. If configured, anonymous users are allowed to bypass identity authentication. |
|
||||||
|
|
||||||
|
|
||||||
|
### Configuration Fields for Each Item in `consumers`
|
||||||
|
|
||||||
|
| Name | Data Type | Requirements | Default Value | Description |
|
||||||
|
|--------------|-----------|--------------|---------------|-----------------------------------------------------------------------------|
|
||||||
|
| `access_key` | string | Required | - | A unique identifier for the consumer, used to reference configurations such as the secret key. |
|
||||||
|
| `secret_key` | string | Required | - | Secret key used to generate the HMAC signature. |
|
||||||
|
| `name` | string | Optional | `access_key` | Name of the consumer. |
|
||||||
|
|
||||||
|
|
||||||
|
### Authorization Configuration (Non-essential)
|
||||||
|
|
||||||
|
| Name | Data Type | Requirements | Default Value | Description |
|
||||||
|
|---------|------------------| ----------------------------------------- |---------------|---------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
|
| `allow` | array of string | Optional (**Non-instance-level configuration only**) | - | Can only be configured in fine-grained rules such as routes or domains. For requests that match the criteria, it configures the consumers allowed to access, enabling fine-grained permission control. |
|
||||||
|
|
||||||
|
|
||||||
|
## Configuration Examples
|
||||||
|
|
||||||
|
### Global Authentication Configuration and Route-level Authorization
|
||||||
|
The following configuration enables HMAC Auth authentication and authorization for specific routes or domains of the gateway. **Note: The `access_key` field must be unique.**
|
||||||
|
|
||||||
|
|
||||||
|
#### Example 1: Basic Route and Domain Authorization Configuration
|
||||||
|
**Instance-level Plugin Configuration**:
|
||||||
|
```yaml
|
||||||
|
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-level Configuration** (Applicable to `route-a` and `route-b`):
|
||||||
|
```yaml
|
||||||
|
allow:
|
||||||
|
- consumer1 # Only consumer1 is allowed to access
|
||||||
|
```
|
||||||
|
|
||||||
|
**Domain-level Configuration** (Applicable to `*.example.com` and `test.com`):
|
||||||
|
```yaml
|
||||||
|
allow:
|
||||||
|
- consumer2 # Only consumer2 is allowed to access
|
||||||
|
```
|
||||||
|
|
||||||
|
**Configuration Description**:
|
||||||
|
- Route names (e.g., `route-a`, `route-b`) correspond to the names defined when creating gateway routes. Only `consumer1` is allowed to access when the route matches.
|
||||||
|
- Domain matching (e.g., `*.example.com`, `test.com`) is used to filter request domains. Only `consumer2` is allowed to access when the domain matches.
|
||||||
|
- Callers not in the `allow` list will be denied access.
|
||||||
|
|
||||||
|
|
||||||
|
**Request and Response Examples**:
|
||||||
|
|
||||||
|
1. **Successful Verification Scenario**
|
||||||
|
```shell
|
||||||
|
curl -X POST 'http://localhost:8082/foo' \
|
||||||
|
-H 'Authorization:Signature keyId="consumer1-key",algorithm="hmac-sha256",headers="@request-target date",signature="G2+60rCCHQCQDZOailnKHLCEy++P1Pa5OEP1bG4QlRo="' \
|
||||||
|
-H 'Date:Sat, 30 Aug 2025 00:52:39 GMT' \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{}'
|
||||||
|
```
|
||||||
|
- Response: Returns a normal response from the backend service.
|
||||||
|
- Additional Info: After successful authentication, the request header `X-Mse-Consumer: consumer1` is automatically added and passed to the backend.
|
||||||
|
|
||||||
|
|
||||||
|
2. **Signature Verification Failure Due to Modified Request Method**
|
||||||
|
```shell
|
||||||
|
curl -X PUT 'http://localhost:8082/foo' \ # Changed from POST to PUT here
|
||||||
|
-H 'Authorization:Signature keyId="consumer1-key",algorithm="hmac-sha256",headers="@request-target date",signature="G2+60rCCHQCQDZOailnKHLCEy++P1Pa5OEP1bG4QlRo="' \
|
||||||
|
-H 'Date:Sat, 30 Aug 2025 00:52:39 GMT' \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{}'
|
||||||
|
```
|
||||||
|
- Response: `401 Unauthorized`
|
||||||
|
- Error Message: `{"message":"client request can't be validated: Invalid signature"}`
|
||||||
|
|
||||||
|
|
||||||
|
3. **Caller Not in the Allow List**
|
||||||
|
```shell
|
||||||
|
curl -X POST 'http://localhost:8082/foo' \
|
||||||
|
-H 'Authorization:Signature keyId="consumer2-key",algorithm="hmac-sha256",headers="@request-target date",signature="5sqSbDX9b91dQsfQra2hpluM7O6/yhS7oLcKPQylyCo="' \
|
||||||
|
-H 'Date:Sat, 30 Aug 2025 00:54:18 GMT' \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{}'
|
||||||
|
```
|
||||||
|
- Response: `401 Unauthorized`
|
||||||
|
- Error Message: `{"message":"client request can't be validated: consumer 'consumer2' is not allowed"}`
|
||||||
|
|
||||||
|
|
||||||
|
4. **Expired Timestamp**
|
||||||
|
```shell
|
||||||
|
curl -X POST 'http://localhost:8082/foo' \
|
||||||
|
-H 'Authorization: Signature keyId="consumer1-key",algorithm="hmac-sha256",headers="@request-target date",signature="gvIUwoYNiK57w6xX2g1Ntpk8lfgD7z+jgom434r5qwg="' \
|
||||||
|
-H 'Date: Sat, 30 Aug 2025 00:40:21 GMT' \ # Expired timestamp
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{}'
|
||||||
|
```
|
||||||
|
- Response: `401 Unauthorized`
|
||||||
|
- Error Message: `{"message":"client request can't be validated: Clock skew exceeded"}`
|
||||||
|
|
||||||
|
|
||||||
|
#### Example 2: Configuration with Custom Signature Headers and Request Body Verification
|
||||||
|
**Instance-level Plugin Configuration**:
|
||||||
|
```yaml
|
||||||
|
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: # Custom request headers to be included in the signature
|
||||||
|
- X-Custom-Header-A
|
||||||
|
- X-Custom-Header-B
|
||||||
|
validate_request_body: true # Enable request body signature verification
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
**Request and Response Examples**:
|
||||||
|
|
||||||
|
1. **Successful Verification Scenario**
|
||||||
|
```shell
|
||||||
|
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="+xCWYCmidq3Sisn08N54NWaau5vSY9qEanWoO9HD4mA="' \
|
||||||
|
-H 'Date:Sat, 30 Aug 2025 01:04:06 GMT' \
|
||||||
|
-H 'Digest:SHA-256=RBNvo1WzZ4oRRq0W9+hknpT7T8If536DEMBg9hyq/4o=' \ # Request body digest
|
||||||
|
-H 'X-Custom-Header-A:test1' \
|
||||||
|
-H 'X-Custom-Header-B:test2' \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{}'
|
||||||
|
```
|
||||||
|
- Response: Returns a normal response from the backend service.
|
||||||
|
|
||||||
|
|
||||||
|
2. **Missing Signature Header**
|
||||||
|
```shell
|
||||||
|
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="+xCWYCmidq3Sisn08N54NWaau5vSY9qEanWoO9HD4mA="' \
|
||||||
|
-H 'Date:Sat, 30 Aug 2025 01:04:06 GMT' \
|
||||||
|
-H 'Digest:SHA-256=RBNvo1WzZ4oRRq0W9+hknpT7T8If536DEMBg9hyq/4o=' \
|
||||||
|
-H 'X-Custom-Header-B:test2' \ # Missing X-Custom-Header-A
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{}'
|
||||||
|
```
|
||||||
|
- Response: `401 Unauthorized`
|
||||||
|
- Error Message: `{"message":"client request can't be validated: expected header \"X-Custom-Header-A\" missing in signing"}`
|
||||||
|
|
||||||
|
|
||||||
|
3. **Tampered Request Body**
|
||||||
|
```shell
|
||||||
|
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="dSbv6pdQOcgkN89TmSxiT8F9nypbPUqAR2E7ELL8K2s="' \
|
||||||
|
-H 'Date:Sat, 30 Aug 2025 01:10:17 GMT' \
|
||||||
|
-H 'Digest:SHA-256=RBNvo1WzZ4oRRq0W9+hknpT7T8If536DEMBg9hyq/4o=' \ # Mismatches the actual body
|
||||||
|
-H 'X-Custom-Header-A:test1' \
|
||||||
|
-H 'X-Custom-Header-B:test2' \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{"key":"value"}' # Tampered request body
|
||||||
|
```
|
||||||
|
- Response: `401 Unauthorized`
|
||||||
|
- Error Message: `{"message":"client request can't be validated: Invalid digest"}`
|
||||||
|
|
||||||
|
|
||||||
|
### Enabling Global Authentication at the Gateway Instance Level
|
||||||
|
The following configuration enables HMAC Auth authentication at the gateway instance level. **All requests must pass authentication to access**:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
global_auth: true # Enable global authentication
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
**Description**: When `global_auth: true`, all requests accessing the gateway must carry valid authentication information. Unauthenticated requests will be directly rejected.
|
||||||
@@ -8,19 +8,15 @@ import (
|
|||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
// validAlgorithms allowed_algorithms 配置中允许的算法
|
||||||
// RuleSet 插件是否至少在一个 domain 或 route 上生效
|
var validAlgorithms = map[string]bool{
|
||||||
RuleSet bool
|
"hmac-sha1": true,
|
||||||
// allowed_algorithms 配置中允许的算法
|
"hmac-sha256": true,
|
||||||
validAlgorithms = map[string]bool{
|
"hmac-sha512": true,
|
||||||
"hmac-sha1": true,
|
}
|
||||||
"hmac-sha256": true,
|
|
||||||
"hmac-sha512": true,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
type HmacAuthConfig struct {
|
type HmacAuthConfig struct {
|
||||||
Consumers []Consumer `json:"consumers,omitempty" yaml:"consumers,omitempty"`
|
Consumers []Consumer `json:"consumers" yaml:"consumers"`
|
||||||
GlobalAuth *bool `json:"global_auth,omitempty" yaml:"global_auth,omitempty"`
|
GlobalAuth *bool `json:"global_auth,omitempty" yaml:"global_auth,omitempty"`
|
||||||
AllowedAlgorithms []string `json:"allowed_algorithms,omitempty" yaml:"allowed_algorithms,omitempty"`
|
AllowedAlgorithms []string `json:"allowed_algorithms,omitempty" yaml:"allowed_algorithms,omitempty"`
|
||||||
ClockSkew int `json:"clock_skew,omitempty" yaml:"clock_skew,omitempty"`
|
ClockSkew int `json:"clock_skew,omitempty" yaml:"clock_skew,omitempty"`
|
||||||
@@ -28,7 +24,9 @@ type HmacAuthConfig struct {
|
|||||||
ValidateRequestBody bool `json:"validate_request_body,omitempty" yaml:"validate_request_body,omitempty"`
|
ValidateRequestBody bool `json:"validate_request_body,omitempty" yaml:"validate_request_body,omitempty"`
|
||||||
HideCredentials bool `json:"hide_credentials,omitempty" yaml:"hide_credentials,omitempty"`
|
HideCredentials bool `json:"hide_credentials,omitempty" yaml:"hide_credentials,omitempty"`
|
||||||
AnonymousConsumer string `json:"anonymous_consumer,omitempty" yaml:"anonymous_consumer,omitempty"`
|
AnonymousConsumer string `json:"anonymous_consumer,omitempty" yaml:"anonymous_consumer,omitempty"`
|
||||||
Allow []string `json:"allow" yaml:"allow"`
|
Allow []string `json:"allow,omitempty" yaml:"allow,omitempty"`
|
||||||
|
// RuleSet 插件是否至少在一个 domain 或 route 上生效
|
||||||
|
RuleSet bool `json:"-" yaml:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Consumer struct {
|
type Consumer struct {
|
||||||
@@ -39,7 +37,7 @@ type Consumer struct {
|
|||||||
|
|
||||||
func ParseGlobalConfig(jsonData gjson.Result, global *HmacAuthConfig) error {
|
func ParseGlobalConfig(jsonData gjson.Result, global *HmacAuthConfig) error {
|
||||||
log.Debug("global config")
|
log.Debug("global config")
|
||||||
RuleSet = false
|
global.RuleSet = false
|
||||||
|
|
||||||
// 处理 consumers 配置
|
// 处理 consumers 配置
|
||||||
consumers := jsonData.Get("consumers")
|
consumers := jsonData.Get("consumers")
|
||||||
@@ -170,7 +168,7 @@ func ParseOverrideRuleConfig(jsonData gjson.Result, global HmacAuthConfig, confi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
RuleSet = true
|
config.RuleSet = true
|
||||||
if configBytes, err := json.Marshal(config); err == nil {
|
if configBytes, err := json.Marshal(config); err == nil {
|
||||||
log.Debugf("config: %s", string(configBytes))
|
log.Debugf("config: %s", string(configBytes))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ go 1.24.1
|
|||||||
toolchain go1.24.4
|
toolchain go1.24.4
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80
|
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0
|
||||||
github.com/higress-group/wasm-go v1.0.1
|
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8
|
||||||
github.com/stretchr/testify v1.9.0
|
github.com/stretchr/testify v1.9.0
|
||||||
github.com/tidwall/gjson v1.18.0
|
github.com/tidwall/gjson v1.18.0
|
||||||
)
|
)
|
||||||
@@ -15,8 +15,10 @@ require (
|
|||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
github.com/tetratelabs/wazero v1.7.2 // indirect
|
||||||
github.com/tidwall/match v1.1.1 // indirect
|
github.com/tidwall/match v1.1.1 // indirect
|
||||||
github.com/tidwall/pretty v1.2.1 // indirect
|
github.com/tidwall/pretty v1.2.1 // indirect
|
||||||
github.com/tidwall/resp v0.1.1 // indirect
|
github.com/tidwall/resp v0.1.1 // indirect
|
||||||
|
github.com/tidwall/sjson v1.2.5 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,14 +2,17 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
|||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80 h1:xqmtTZI0JQ2O+Lg9/CE6c+Tw9KD6FnvWw8EpLVuuvfg=
|
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0 h1:YGdj8KBzVjabU3STUfwMZghB+VlX6YLfJtLbrsWaOD0=
|
||||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250611100342-5654e89a7a80/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
|
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
|
||||||
github.com/higress-group/wasm-go v1.0.1 h1:T1m++qTEANp8+jwE0sxltwtaTKmrHCkLOp1m9N+YeqY=
|
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8 h1:rs+AH1wfZy4swzuAyiRXT7xPUm8gycXt9Gwy0tqOq0o=
|
||||||
github.com/higress-group/wasm-go v1.0.1/go.mod h1:9k7L730huS/q4V5iH9WLDgf5ZUHEtfhM/uXcegKDG/M=
|
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8/go.mod h1:9k7L730huS/q4V5iH9WLDgf5ZUHEtfhM/uXcegKDG/M=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/tetratelabs/wazero v1.7.2 h1:1+z5nXJNwMLPAWaTePFi49SSTL0IMx/i3Fg8Yc25GDc=
|
||||||
|
github.com/tetratelabs/wazero v1.7.2/go.mod h1:ytl6Zuh20R/eROuyDaGPkp82O9C/DJfXAwJfQ3X6/7Y=
|
||||||
|
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||||
@@ -19,6 +22,8 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
|||||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||||
github.com/tidwall/resp v0.1.1 h1:Ly20wkhqKTmDUPlyM1S7pWo5kk0tDu8OoC/vFArXmwE=
|
github.com/tidwall/resp v0.1.1 h1:Ly20wkhqKTmDUPlyM1S7pWo5kk0tDu8OoC/vFArXmwE=
|
||||||
github.com/tidwall/resp v0.1.1/go.mod h1:3/FrruOBAxPTPtundW0VXgmsQ4ZBA0Aw714lVYgwFa0=
|
github.com/tidwall/resp v0.1.1/go.mod h1:3/FrruOBAxPTPtundW0VXgmsQ4ZBA0Aw714lVYgwFa0=
|
||||||
|
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||||
|
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
|||||||
@@ -1,3 +1,17 @@
|
|||||||
|
// Copyright (c) 2025 Alibaba Group Holding Ltd.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -55,7 +69,7 @@ func onHttpRequestHeaders(ctx wrapper.HttpContext, cfg config.HmacAuthConfig) ty
|
|||||||
globalAuthNoSet = cfg.GlobalAuth == nil
|
globalAuthNoSet = cfg.GlobalAuth == nil
|
||||||
globalAuthSetTrue = !globalAuthNoSet && *cfg.GlobalAuth
|
globalAuthSetTrue = !globalAuthNoSet && *cfg.GlobalAuth
|
||||||
globalAuthSetFalse = !globalAuthNoSet && !*cfg.GlobalAuth
|
globalAuthSetFalse = !globalAuthNoSet && !*cfg.GlobalAuth
|
||||||
ruleSet = config.RuleSet
|
ruleSet = cfg.RuleSet
|
||||||
)
|
)
|
||||||
|
|
||||||
// 不需要认证而直接放行的情况:
|
// 不需要认证而直接放行的情况:
|
||||||
@@ -155,9 +169,7 @@ func onHttpRequestBody(ctx wrapper.HttpContext, cfg config.HmacAuthConfig, body
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 计算请求体的 SHA-256 摘要
|
// 计算请求体的 SHA-256 摘要
|
||||||
hash := sha256.Sum256(body)
|
digestCreated := calculateBodyDigest(body)
|
||||||
encodedDigest := base64.StdEncoding.EncodeToString(hash[:])
|
|
||||||
digestCreated := "SHA-256=" + encodedDigest
|
|
||||||
|
|
||||||
// 比较请求头中的 Digest 和服务端计算的摘要
|
// 比较请求头中的 Digest 和服务端计算的摘要
|
||||||
if digestCreated != digestHeaderVal {
|
if digestCreated != digestHeaderVal {
|
||||||
@@ -169,7 +181,7 @@ func onHttpRequestBody(ctx wrapper.HttpContext, cfg config.HmacAuthConfig, body
|
|||||||
return types.ActionContinue
|
return types.ActionContinue
|
||||||
}
|
}
|
||||||
|
|
||||||
// HmacParams 存储从 Authorization 头解析出的 HMAC 参数
|
// HmacParams 存储从 Authorization 头中提取 HMAC 参数和消费者信息
|
||||||
type HmacParams struct {
|
type HmacParams struct {
|
||||||
KeyId string
|
KeyId string
|
||||||
Algorithm string
|
Algorithm string
|
||||||
@@ -180,7 +192,7 @@ type HmacParams struct {
|
|||||||
|
|
||||||
// retrieveHmacFieldsAndConsumer 从 Authorization 头中提取 HMAC 参数和消费者信息
|
// retrieveHmacFieldsAndConsumer 从 Authorization 头中提取 HMAC 参数和消费者信息
|
||||||
func retrieveHmacFieldsAndConsumer(cfg config.HmacAuthConfig) (*HmacParams, error) {
|
func retrieveHmacFieldsAndConsumer(cfg config.HmacAuthConfig) (*HmacParams, error) {
|
||||||
hmacParams := &HmacParams{}
|
params := &HmacParams{}
|
||||||
|
|
||||||
// 获取 Authorization 头
|
// 获取 Authorization 头
|
||||||
authString, err := proxywasm.GetHttpRequestHeader(authorizationHeader)
|
authString, err := proxywasm.GetHttpRequestHeader(authorizationHeader)
|
||||||
@@ -206,26 +218,26 @@ func retrieveHmacFieldsAndConsumer(cfg config.HmacAuthConfig) (*HmacParams, erro
|
|||||||
|
|
||||||
switch key {
|
switch key {
|
||||||
case "keyId":
|
case "keyId":
|
||||||
hmacParams.KeyId = value
|
params.KeyId = value
|
||||||
case "algorithm":
|
case "algorithm":
|
||||||
hmacParams.Algorithm = value
|
params.Algorithm = value
|
||||||
case "signature":
|
case "signature":
|
||||||
hmacParams.Signature = value
|
params.Signature = value
|
||||||
case "headers":
|
case "headers":
|
||||||
// 分割 headers 字段
|
// 分割 headers 字段
|
||||||
if value != "" {
|
if value != "" {
|
||||||
hmacParams.Headers = strings.Split(value, " ")
|
params.Headers = strings.Split(value, " ")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证必要字段
|
// 验证必要字段
|
||||||
if hmacParams.KeyId == "" || hmacParams.Signature == "" {
|
if params.KeyId == "" || params.Signature == "" {
|
||||||
return nil, fmt.Errorf("keyId or signature missing")
|
return nil, fmt.Errorf("keyId or signature missing")
|
||||||
}
|
}
|
||||||
|
|
||||||
if hmacParams.Algorithm == "" {
|
if params.Algorithm == "" {
|
||||||
return nil, fmt.Errorf("algorithm missing")
|
return nil, fmt.Errorf("algorithm missing")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,7 +245,7 @@ func retrieveHmacFieldsAndConsumer(cfg config.HmacAuthConfig) (*HmacParams, erro
|
|||||||
consumerName := ""
|
consumerName := ""
|
||||||
found := false
|
found := false
|
||||||
for _, consumer := range cfg.Consumers {
|
for _, consumer := range cfg.Consumers {
|
||||||
if consumer.AccessKey == hmacParams.KeyId {
|
if consumer.AccessKey == params.KeyId {
|
||||||
consumerName = consumer.Name
|
consumerName = consumer.Name
|
||||||
found = true
|
found = true
|
||||||
break
|
break
|
||||||
@@ -244,8 +256,8 @@ func retrieveHmacFieldsAndConsumer(cfg config.HmacAuthConfig) (*HmacParams, erro
|
|||||||
return nil, fmt.Errorf("Invalid keyId")
|
return nil, fmt.Errorf("Invalid keyId")
|
||||||
}
|
}
|
||||||
|
|
||||||
hmacParams.ConsumerName = consumerName
|
params.ConsumerName = consumerName
|
||||||
return hmacParams, nil
|
return params, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// validateClockSkew 检查时间偏差
|
// validateClockSkew 检查时间偏差
|
||||||
@@ -291,10 +303,7 @@ func validateSignature(hmacParams *HmacParams, cfg config.HmacAuthConfig) error
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 生成 HMAC 签名
|
// 生成 HMAC 签名
|
||||||
signingString, err := generateSigningString(hmacParams)
|
signingString := generateSigningString(hmacParams)
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Failed to generate signing string")
|
|
||||||
}
|
|
||||||
expectedSignature, err := generateHmacSignature(secretKey, hmacParams.Algorithm, signingString)
|
expectedSignature, err := generateHmacSignature(secretKey, hmacParams.Algorithm, signingString)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -311,7 +320,7 @@ func validateSignature(hmacParams *HmacParams, cfg config.HmacAuthConfig) error
|
|||||||
}
|
}
|
||||||
|
|
||||||
// generateSigningString 生成签名字符串
|
// generateSigningString 生成签名字符串
|
||||||
func generateSigningString(hmacParams *HmacParams) (string, error) {
|
func generateSigningString(hmacParams *HmacParams) string {
|
||||||
var signingStringItems []string
|
var signingStringItems []string
|
||||||
signingStringItems = append(signingStringItems, hmacParams.KeyId)
|
signingStringItems = append(signingStringItems, hmacParams.KeyId)
|
||||||
|
|
||||||
@@ -341,7 +350,7 @@ func generateSigningString(hmacParams *HmacParams) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
signingString := strings.Join(signingStringItems, "\n") + "\n"
|
signingString := strings.Join(signingStringItems, "\n") + "\n"
|
||||||
return signingString, nil
|
return signingString
|
||||||
}
|
}
|
||||||
|
|
||||||
// generateHmacSignature 生成 HMAC 签名
|
// generateHmacSignature 生成 HMAC 签名
|
||||||
@@ -364,6 +373,13 @@ func generateHmacSignature(secretKey, algorithm, message string) (string, error)
|
|||||||
return base64.StdEncoding.EncodeToString(signature), nil
|
return base64.StdEncoding.EncodeToString(signature), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// calculateBodyDigest 计算请求体的 SHA-256 摘要
|
||||||
|
func calculateBodyDigest(body []byte) string {
|
||||||
|
hash := sha256.Sum256(body)
|
||||||
|
encodedDigest := base64.StdEncoding.EncodeToString(hash[:])
|
||||||
|
return "SHA-256=" + encodedDigest
|
||||||
|
}
|
||||||
|
|
||||||
func sendUnauthorizedResponse(message string) types.Action {
|
func sendUnauthorizedResponse(message string) types.Action {
|
||||||
errorResponse := fmt.Sprintf(errorResponseTemplate, message)
|
errorResponse := fmt.Sprintf(errorResponseTemplate, message)
|
||||||
proxywasm.SendHttpResponse(401, nil, []byte(errorResponse), -1)
|
proxywasm.SendHttpResponse(401, nil, []byte(errorResponse), -1)
|
||||||
|
|||||||
1279
plugins/wasm-go/extensions/hmac-auth-apisix/main_test.go
Normal file
1279
plugins/wasm-go/extensions/hmac-auth-apisix/main_test.go
Normal file
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/higress-group/wasm-go/pkg/log"
|
|
||||||
"github.com/higress-group/wasm-go/pkg/wrapper"
|
"github.com/higress-group/wasm-go/pkg/wrapper"
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
)
|
)
|
||||||
@@ -26,7 +25,7 @@ type RedisConfig struct {
|
|||||||
KeyPrefix string
|
KeyPrefix string
|
||||||
}
|
}
|
||||||
|
|
||||||
func ParseConfig(json gjson.Result, config *ReplayProtectionConfig, log log.Log) error {
|
func ParseConfig(json gjson.Result, config *ReplayProtectionConfig) error {
|
||||||
// Parse Redis configuration
|
// Parse Redis configuration
|
||||||
redisConfig := json.Get("redis")
|
redisConfig := json.Get("redis")
|
||||||
if !redisConfig.Exists() {
|
if !redisConfig.Exists() {
|
||||||
|
|||||||
@@ -18,12 +18,12 @@ func main() {}
|
|||||||
func init() {
|
func init() {
|
||||||
wrapper.SetCtx(
|
wrapper.SetCtx(
|
||||||
"replay-protection",
|
"replay-protection",
|
||||||
wrapper.ParseConfigBy(config.ParseConfig),
|
wrapper.ParseConfig(config.ParseConfig),
|
||||||
wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
|
wrapper.ProcessRequestHeaders(onHttpRequestHeaders),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func onHttpRequestHeaders(ctx wrapper.HttpContext, cfg config.ReplayProtectionConfig, log log.Log) types.Action {
|
func onHttpRequestHeaders(ctx wrapper.HttpContext, cfg config.ReplayProtectionConfig) types.Action {
|
||||||
nonce, _ := proxywasm.GetHttpRequestHeader(cfg.NonceHeader)
|
nonce, _ := proxywasm.GetHttpRequestHeader(cfg.NonceHeader)
|
||||||
if cfg.ForceNonce && nonce == "" {
|
if cfg.ForceNonce && nonce == "" {
|
||||||
// In force mode, reject the request if a required header is missing.
|
// In force mode, reject the request if a required header is missing.
|
||||||
|
|||||||
Reference in New Issue
Block a user