# 功能说明
`hmac-auth`插件实现了基于 HMAC 算法为 HTTP 请求生成不可伪造的签名，并基于签名实现身份认证和鉴权

# 配置字段

| 名称          | 数据类型        | 填写要求 | 默认值 | 描述                                                                                                                |
| ------------- | --------------- | -------- | ------ | ------------------------------------------------------------------------------------------------------------------- |
| `consumers`   | array of object | 必填     | -      | 配置服务的调用者，用于对请求进行认证                                                                                |
| `date_offset` | number          | 选填     | -      | 配置允许的客户端最大时间偏移，单位为秒，根据请求头`Date`解析客户端 UTC 时间，可用于避免请求重放；未配置时，不做校验 |
| `_rules_`     | array of object | 选填     | -      | 配置特定路由或域名的访问权限列表，用于对请求进行鉴权                                                                |

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

| 名称     | 数据类型 | 填写要求 | 默认值 | 描述                                |
| -------- | -------- | -------- | ------ | ----------------------------------- |
| `key`    | string   | 必填     | -      | 配置从请求的`x-ca-key`头中提取的key |
| `secret` | string   | 必填     | -      | 配置用于生成签名的secret            |
| `name`   | string   | 必填     | -      | 配置该consumer的名称                |

`_rules_` 中每一项的配置字段说明如下：

| 名称             | 数据类型        | 填写要求                                          | 默认值 | 描述                                               |
| ---------------- | --------------- | ------------------------------------------------- | ------ | -------------------------------------------------- |
| `_match_route_`  | array of string | 选填，`_match_route_`，`_match_domain_`中选填一项 | -      | 配置要匹配的路由名称                               |
| `_match_domain_` | array of string | 选填，`_match_route_`，`_match_domain_`中选填一项 | -      | 配置要匹配的域名                                   |
| `allow`          | array of string | 必填                                              | -      | 对于符合匹配条件的请求，配置允许访问的consumer名称 |

**注意：**
- 若不配置`_rules_`字段，则默认对当前网关实例的所有路由开启认证；
- 对于通过认证鉴权的请求，请求的header会被添加一个`X-Mse-Consumer`字段，用以标识调用者的名称。

# 配置示例

以下配置将对网关特定路由或域名开启 Hmac Auth 认证和鉴权，注意`key`字段不能重复

## 对特定路由或域名开启
```yaml
consumers: 
- key: appKey-example-1
  secret: appSecret-example-1
  name: consumer-1
- key: appKey-example-2
  secret: appSecret-example-2
  name: consumer-2
# 使用 _rules_ 字段进行细粒度规则配置
_rules_:
# 规则一：按路由名称匹配生效
- _match_route_:
  - route-a
  - route-b
  allow:
  - consumer-1
# 规则二：按域名匹配生效
- _match_domain_:
  - "*.example.com"
  - test.com
  allow:
  - consumer-2
```
每条匹配规则下的`allow`字段用于指定该匹配条件下允许访问的调用者列表；

此例 `_match_route_` 中指定的 `route-a` 和 `route-b` 即在创建网关路由时填写的路由名称，当匹配到这两个路由时，将允许`name`为`consumer-1`的调用者访问，其他调用者不允许访问；

此例 `_match_domain_` 中指定的 `*.example.com` 和 `test.com` 用于匹配请求的域名，当发现域名匹配时，将允许`name`为`consumer-2`的调用者访问，其他调用者不允许访问；

认证成功后，请求的header中会被添加一个`X-Mse-Consumer`字段，其值为调用方的名称，例如`consumer-1`。

## 网关实例级别开启

以下配置将对网关实例级别开启 Hamc Auth 认证

```yaml
consumers: 
- key: appKey-example-1
  secret: appSecret-example-1
  name: consumer-1
- key: appKey-example-2
  secret: appSecret-example-2
  name: consumer-2
```


# 签名机制说明

## 配置准备

如上指引，在插件配置中配置生成和验证签名需要用的凭证配置

- key: 用于请求头 `x-ca-key` 中设置
- secret: 用于生成请求签名

## 客户端签名生成方式

### 流程简介

客户端生成签名一共分三步处理：

1. 从原始请求中提取关键数据，得到一个用来签名的字符串

2. 使用加密算法和配置的 `secret` 对关键数据签名串进行加密处理，得到签名

3. 将签名所相关的所有头加入到原始HTTP请求中，得到最终HTTP请求

如下图所示：
![](https://help-static-aliyun-doc.aliyuncs.com/assets/img/zh-CN/1745707061/p188113.png)

### 签名串提取流程

客户端需要从Http请求中提取出关键数据，组合成一个签名串，生成的签名串的格式如下：

```text
HTTPMethod
Accept
Content-MD5
Content-Type
Date
Headers
PathAndParameters
```

以上7个字段构成整个签名串，字段之间使用\n间隔，如果Headers为空，则不需要加\n，其他字段如果为空都需要保留\n。签名大小写敏感。下面介绍下每个字段的提取规则：

- HTTPMethod：HTTP的方法，全部大写，比如POST

- Accept：请求中的Accept头的值，可为空。建议显式设置 Accept Header。当 Accept 为空时，部分 Http 客户端会给 Accept 设置默认值为 `*/*`，导致签名校验失败。

- Content-MD5：请求中的Content-MD5头的值，可为空只有在请求存在Body且Body为非Form形式时才计算Content-MD5头，下面是Java的Content-MD5值的参考计算方式：

```java
String content-MD5 = Base64.encodeBase64(MD5(bodyStream.getbytes("UTF-8")));
```

- Content-Type：请求中的Content-Type头的值，可为空

- Date：请求中的Date头的值，当未开启`date_offset`配置时，可为空，否则将用于时间偏移校验

- Headers：用户可以选取指定的header参与签名，关于header的签名串拼接方式有以下规则：
    - 参与签名计算的Header的Key按照字典排序后使用如下方式拼接
    ```text
    HeaderKey1 + ":" + HeaderValue1 + "\n"\+
    HeaderKey2 + ":" + HeaderValue2 + "\n"\+
    ...
    HeaderKeyN + ":" + HeaderValueN + "\n"
    ```
    - 某个Header的Value为空，则使用HeaderKey+":"+"\n"参与签名，需要保留Key和英文冒号
    - 所有参与签名的Header的Key的集合使用英文逗号分割放到Key为X-Ca-Signature-Headers的Header中
    - 以下Header不参与Header签名计算：X-Ca-Signature、X-Ca-Signature-Headers、Accept、Content-MD5、Content-Type、Date
    
- PathAndParameters: 这个字段包含Path，Query和Form中的所有参数，具体组织形式如下
```text
Path + "?" + Key1 + "=" + Value1 + "&" + Key2 + "=" + Value2 + ... "&" + KeyN + "=" + ValueN
```

注意：
1. Query和Form参数对的Key按照字典排序后使用上面的方式拼接
    
2. Query和Form参数为空时，则直接使用Path，不需要添加?
    
3. 参数的Value为空时只保留Key参与签名，等号不需要再加入签名
   
4. Query和Form存在数组参数时（key相同，value不同的参数） ，取第一个Value参与签名计算
    
### 签名串提取示例

初始的HTTP请求：
```text
POST /http2test/test?param1=test HTTP/1.1
host:api.aliyun.com
accept:application/json; charset=utf-8
ca_version:1
content-type:application/x-www-form-urlencoded; charset=utf-8
x-ca-timestamp:1525872629832
date:Wed, 09 May 2018 13:30:29 GMT+00:00
user-agent:ALIYUN-ANDROID-DEMO
x-ca-nonce:c9f15cbf-f4ac-4a6c-b54d-f51abf4b5b44
content-length:33
username=xiaoming&password=123456789
```

生成的正确签名串为：
```text
POST
application/json; charset=utf-8
application/x-www-form-urlencoded; charset=utf-8
Wed, 09 May 2018 13:30:29 GMT+00:00
x-ca-key:203753385
x-ca-nonce:c9f15cbf-f4ac-4a6c-b54d-f51abf4b5b44
x-ca-signature-method:HmacSHA256
x-ca-timestamp:1525872629832
/http2test/test?param1=test&password=123456789&username=xiaoming
```

### 签名计算流程

客户端从HTTP请求中提取出关键数据组装成签名串后，需要对签名串进行加密及编码处理，形成最终的签名

具体的加密形式如下，其中 `stringToSign` 是提取出来的签名串，`secret` 就是插件配置中填写的，`sign` 是最终生成的签名：

```java
Mac hmacSha256 = Mac.getInstance("HmacSHA256");
byte[] secretBytes = secret.getBytes("UTF-8");
hmacSha256.init(new SecretKeySpec(secretBytes, 0, secretBytes.length, "HmacSHA256"));
byte[] result = hmacSha256.doFinal(stringToSign.getBytes("UTF-8"));
String sign = Base64.encodeBase64String(result);
```

总结一下，就是将 `stringToSign` 使用UTF-8解码后得到Byte数组，然后使用加密算法对Byte数组进行加密，然后使用Base64算法进行编码，形成最终的签名。

### 添加签名流程

客户端需要将以下四个Header放在HTTP请求中传输给API网关，进行签名校验：

- x-ca-key：取值APP Key，必选

- x-ca-signature-method：签名算法，取值HmacSHA256或者HmacSHA1，可选，默认值为HmacSHA256

- x-ca-signature-headers：所有签名头的Key的集合，使用英文逗号分隔，可选

- x-ca-signature：签名，必选

下面是携带签名的整个HTTP请求的示例：

```text
POST /http2test/test?param1=test HTTP/1.1
host:api.aliyun.com
accept:application/json; charset=utf-8
ca_version:1
content-type:application/x-www-form-urlencoded; charset=utf-8
x-ca-timestamp:1525872629832
date:Wed, 09 May 2018 13:30:29 GMT+00:00
user-agent:ALIYUN-ANDROID-DEMO
x-ca-nonce:c9f15cbf-f4ac-4a6c-b54d-f51abf4b5b44
x-ca-key:203753385
x-ca-signature-method:HmacSHA256
x-ca-signature-headers:x-ca-timestamp,x-ca-key,x-ca-nonce,x-ca-signature-method
x-ca-signature:xfX+bZxY2yl7EB/qdoDy9v/uscw3Nnj1pgoU+Bm6xdM=
content-length:33
username=xiaoming&password=123456789
```

## 服务端签名验证方式

### 流程简介

服务器验证客户端签名一共分四步处理：

1. 从接收到的请求中提取关键数据，得到一个用来签名的字符串

2. 从接收到的请求中读取 `key` ，通过 `key` 查询到对应的 `secret`

3. 使用加密算法和 `secret` 对关键数据签名串进行加密处理，得到签名

4. 从接收到的请求中读取客户端签名，对比服务器端签名和客户端签名的一致性

如下图所示：
![](https://help-static-aliyun-doc.aliyuncs.com/assets/img/zh-CN/1745707061/p188116.png)


## 签名排错方法

网关签名校验失败时，会将服务端的签名串（StringToSign）放到HTTP Response的Header中返回到客户端，Key为：X-Ca-Error-Message，用户只需要将本地计算的签名串（StringToSign）与服务端返回的签名串进行对比即可找到问题；

如果服务端与客户端的StringToSign一致请检查用于签名计算的APP Secret是否正确；

因为HTTP Header中无法表示换行，因此StringToSign中的换行符都被替换成`#`，如下所示：

```text
X-Ca-Error-Message:  Server StringToSign:`GET#application/json##application/json##X-Ca-Key:200000#X-Ca-Timestamp:1589458000000#/app/v1/config/keys?keys=TEST`

```

# 相关错误码

| HTTP 状态码 | 出错信息               | 原因说明                                                                         |
| ----------- | ---------------------- | -------------------------------------------------------------------------------- |
| 401         | Invalid Key            | 请求头未提供 x-ca-key，或者 x-ca-key 无效                                        |
| 401         | Empty Signature        | 请求头未提供 x-ca-signature 签名串                                               |
| 400         | Invalid Signature      | 请求头 x-ca-signature 签名串，与服务端计算得到签名不一致                         |
| 400         | Invalid Content-MD5    | 请求头 content-md5 不正确                                                        |
| 400         | Invalid Date           | 根据请求头 date 计算时间偏移超过配置的 date_offset                               |
| 413         | Request Body Too Large | 请求 Body 超过限制大小：32 MB                                                    |
| 413         | Payload Too Large      | 请求 Body 超过全局配置 DownstreamConnectionBufferLimits                          |
| 403         | Unauthorized Consumer  | 请求的调用方无访问权限                                                           |


