|
| 1 | +# API验签组件 |
| 2 | + |
| 3 | +> Since 2.0.0 |
| 4 | +
|
| 5 | +## 简介 |
| 6 | + |
| 7 | +为确保接口调用者的身份合法性并防止请求篡改,项目对外提供的接口通常需要添加验签处理。当前组件定义了一套签名规则,接口提供方可轻松接入以保护接口安全。 |
| 8 | + |
| 9 | +## 签名流程概述 |
| 10 | + |
| 11 | +应用接入方在使用接口前,需先向接口提供方申请一对 **AccessKey** 和 **SecretKey** 作为身份凭证。 |
| 12 | + |
| 13 | +在每次调用接口时,接入方需根据请求参数构建签名串,使用 MD5 算法生成签名值。签名值将与其他认证信息一同添加至 HTTP 请求头中。 |
| 14 | + |
| 15 | +### 签名串构建规则 |
| 16 | + |
| 17 | +签名串是生成签名值的核心数据,需按照以下顺序和规则拼接各参数: |
| 18 | + |
| 19 | +1. **HTTP Method** |
| 20 | + |
| 21 | + 请求的 HTTP 方法,大写字母形式(如:`POST`、`GET`)。 |
| 22 | + |
| 23 | +2. **HTTP Request** **URI** |
| 24 | + |
| 25 | + 请求的 URI,包括查询字符串(Query String)部分,但不包含域名。 |
| 26 | + |
| 27 | + 例如:对于 `http://www.xxx.com/order?name=zhangsan` ,Request URI 为 `/order?name=zhangsan`。 |
| 28 | + |
| 29 | +3. **Request Body** |
| 30 | + |
| 31 | + 请求体内容。若请求没有 Body,则该参数和分隔符 `#` 均不参与签名。 |
| 32 | + |
| 33 | +4. **Timestamp** |
| 34 | + |
| 35 | + 请求发起时的 UNIX 时间戳(从1970年1月1日 00:00:00 UTC 起的总毫秒数)。平台将拒绝处理超时请求,确保系统时间准确。 |
| 36 | + |
| 37 | +5. **Nonce** |
| 38 | + |
| 39 | + 随机生成的 32 位字符串,用于防止重放攻击。 |
| 40 | + |
| 41 | +6. **AccessKey** |
| 42 | + |
| 43 | + 接入方的访问标识,由平台提供。 |
| 44 | + |
| 45 | +7. **SecretKey** |
| 46 | + |
| 47 | + 访问密钥,用于签名生成,但 **不** 在请求中携带。 |
| 48 | + |
| 49 | +### 签名值计算 |
| 50 | + |
| 51 | +将按上述顺序拼接好的签名串,使用 MD5 算法进行摘要计算,得到小写签名值。 |
| 52 | + |
| 53 | +签名串的格式为: |
| 54 | + |
| 55 | +``` |
| 56 | +{HTTP_METHOD}#{HTTP_URI}#{RequestBody}#{Timestamp}#{Nonce}#{AccessKey}#{SecretKey} |
| 57 | +``` |
| 58 | + |
| 59 | +### 请求头设置 |
| 60 | + |
| 61 | +签名计算完成后,将以下参数添加至 HTTP 请求头: |
| 62 | + |
| 63 | +- **X-Access-Key**:申请到的 AccessKey |
| 64 | +- **X-Timestamp**:请求的时间戳 |
| 65 | +- **X-Nonce**:32 位随机字符串 |
| 66 | +- **X-Signature**:MD5 计算得到的签名值 |
| 67 | + |
| 68 | +## 具体示例 |
| 69 | + |
| 70 | +### 请求参数 |
| 71 | + |
| 72 | +假设以下请求参数: |
| 73 | + |
| 74 | +- **HTTP_METHOD**: `GET` |
| 75 | +- **HTTP_URI**: `/product/add` |
| 76 | +- **RequestBody**: `{"productId":1}` |
| 77 | +- **Timestamp**: `1710924789130` |
| 78 | +- **Nonce**: `Js3eTl1I7oP5g8YpDnYX2danVrqRrqZg` |
| 79 | +- **SecretKey**: `0cec22334545eea97776c7d5e39` |
| 80 | + |
| 81 | +### 签名计算 |
| 82 | + |
| 83 | +拼接后的签名串为: |
| 84 | + |
| 85 | +```Plain |
| 86 | +GET#/product/add#{"productId":1}#1710924789130#Js3eTl1I7oP5g8YpDnYX2danVrqRrqZg#0cecd9245cc1107d8eea97776c7d5e39#0cec22334545eea97776c7d5e39 |
| 87 | +``` |
| 88 | + |
| 89 | +MD5 计算后的签名值为: |
| 90 | + |
| 91 | +```Plain |
| 92 | +0cecd9245cc1107d8eea97776c7d5e39 |
| 93 | +``` |
| 94 | + |
| 95 | +### 最终请求头 |
| 96 | + |
| 97 | +设置 HTTP 请求头如下: |
| 98 | + |
| 99 | +```Plain |
| 100 | +X-Access-Key: 0d30cfd0929a46ffb1200955d35bf18f |
| 101 | +X-Timestamp: 1710924789130 |
| 102 | +X-Nonce: Js3eTl1I7oP5g8YpDnYX2danVrqRrqZg |
| 103 | +X-Signature: 0cecd9245cc1107d8eea97776c7d5e39 |
| 104 | +``` |
| 105 | + |
| 106 | +## 组件接入 |
| 107 | + |
| 108 | +### 依赖引入 |
| 109 | + |
| 110 | +组件已经推送到私服,可以直接按坐标引入,下面提供了 maven 的引入示例: |
| 111 | + |
| 112 | +spring boot 环境下,可以直接引入 ballcat-spring-boot-starter-apisignature 依赖包,该 starter 会在应用启动时进行自动配置。 |
| 113 | + |
| 114 | +```XML |
| 115 | + |
| 116 | +<dependency> |
| 117 | + <groupId>org.ballcat</groupId> |
| 118 | + <artifactId>ballcat-spring-boot-starter-apisignature</artifactId> |
| 119 | +</dependency> |
| 120 | +``` |
| 121 | + |
| 122 | +### 代码实现 |
| 123 | + |
| 124 | +组件中有两个重要的实体,需要开发者自行实现,并注册到 Spring 容器中: |
| 125 | + |
| 126 | +**ApiKeyManager** |
| 127 | + |
| 128 | +对于 **AccessKey** 、 **SecretKey 、Subject** 的一个管理类,`Subject` 是调用方主体的一个抽象表示。在验签过程中,需要根据 * |
| 129 | +*AccessKey** 查找到对应的 `Subject` ,再根据 `Subject` 获取到对应的 **SecretKey**,进行签名校验。 |
| 130 | + |
| 131 | +``` |
| 132 | + public interface ApiKeyManager { |
| 133 | + /** |
| 134 | + * 根据传入的 Access Key 获取用户主体信息 |
| 135 | + * @param accessKey Access Key |
| 136 | + * @return subject |
| 137 | + */ |
| 138 | + Object getSubject(String accessKey); |
| 139 | + |
| 140 | + /** |
| 141 | + * 根据用户主体获取到对应的 secretKey. |
| 142 | + * @param subject 用户主体 |
| 143 | + * @return 如果找到对应的 secretKey,则返回该 secretKey;否则返回 null |
| 144 | + */ |
| 145 | + String getSecretKey(Object subject); |
| 146 | + } |
| 147 | +``` |
| 148 | + |
| 149 | +**NonceStore** |
| 150 | +用于存储随机和校验字符串,防止请求重放。 |
| 151 | + |
| 152 | +```Java |
| 153 | + public interface NonceStore { |
| 154 | + /** |
| 155 | + * 存储随机字符串,如果其不存在的话。 |
| 156 | + * @param nonce 随机字符串 |
| 157 | + * @param timeout 存储的过期时长 |
| 158 | + * @param timeUnit 过期时长的单位 |
| 159 | + * @return 如果当前随机字符串已存在,则返回 false. |
| 160 | + */ |
| 161 | + boolean storeIfAbsent(String nonce, Long timeout, TimeUnit timeUnit); |
| 162 | + } |
| 163 | +``` |
| 164 | + |
| 165 | +`Subject` 的具体类型和实现由开发者自行定义,`Subject` 会在验签成功后存入线程上线文,开发者可以通过 `SubjectHolder` |
| 166 | +随时获取主体信息。所以建议在主体对象中存储一些常用的主体属性,如userId 等。 |
| 167 | + |
| 168 | +### 组件配置 |
| 169 | + |
| 170 | +| 配置项 | 数据类型 | 默认值 | 描述 | |
| 171 | +|---------------------------------------------|--------------|------------------|--------------------------------------------------| |
| 172 | +| `wd.api.signature.include-url-pattens` | List\<String\> | `["/**"]` | 需要进行签名校验的 URL 规则列表。 | |
| 173 | +| `wd.api.signature.exclude-url-pattens` | List\<String\> | `[]` | 不需要进行签名校验的 URL 规则列表,优先级高于 `include-url-pattens`。 | |
| 174 | +| `wd.api.signature.uri-prefix` | String | 无 | 请求 URI 的前缀字符串,当经过网关或 Nginx 时,恢复被重写的 URI。 | |
| 175 | +| `wd.api.signature.signature-header` | String | `X-Signature` | 存放签名信息的请求头名称。 | |
| 176 | +| `wd.api.signature.timestamp-header` | String | `X-Timestamp` | 存放请求时间戳的请求头名称。 | |
| 177 | +| `wd.api.signature.nonce-header` | String | `X-Nonce` | 存放32位随机字符串的请求头名称。 | |
| 178 | +| `wd.api.signature.access-key-header` | String | `X-Access-Key` | 存放请求方标识的请求头名称。 | |
| 179 | +| `wd.api.signature.timestamp-diff-threshold` | long | `300000` (5 分钟) | 请求时间戳和服务器时间戳允许的最大时间差(毫秒)。 | |
| 180 | +| `wd.api.signature.nonce-timeout` | long | `900000` (15 分钟) | `nonce` 随机字符串的存储过期时长(毫秒)。 | |
| 181 | +| `wd.api.signature.nonce-timeout-unit` | TimeUnit | `MILLISECONDS` | `nonce-timeout` 的时间单位,默认为毫秒。 | |
0 commit comments