接口签名方案(简化版)
2020-06-17 17:39:26
## 签名作用
1. **请求者身份验证**。通过访问者唯一 ID 和密钥确定请求者身份。
2. **防止传输数据篡改**。对数据签名并检验,保障传输内容完整性。
3. **防止签名被盗用**。对签名设置时效,避免签名盗用并重复使用。
## 准备工作
1. SecretId 和 SecretKey。
2. 确定开发语言:
支持但不限于 Java、PHP、C#、C++、Node.js、Python,根据不同的开发语言,确定对应的 HMAC-SHA1、SHA1 和 UrlEncode 函数。
其中,HMAC-SHA1 和 SHA1 函数以 UTF-8 编码字符串为输入,以16进制小写字符串为输出。UrlEncode 基于 UTF-8 编码,此外对于 ASCII 范围内的可打印字符,下列特殊符号也应被编码:
| 字符 | 十进制 | 十六进制 | 字符 | 十进制 | 十六进制 |
| ------ | ------ | -------- | ---- | ------ | -------- |
| (空格) | 32 | 20 | ; | 59 | 3B |
| ! | 33 | 21 | < | 60 | 3C |
| " | 34 | 22 | = | 61 | 3D |
| # | 35 | 23 | > | 62 | 3E |
| $ | 36 | 24 | ? | 63 | 3F |
| % | 37 | 25 | @ | 64 | 40 |
| & | 38 | 26 | [ | 91 | 5B |
| ' | 39 | 27 | \ | 92 | 5C |
| ( | 40 | 28 | ] | 93 | 5D |
| ) | 41 | 29 | ^ | 94 | 5E |
| * | 42 | 2A | \` | 96 | 60 |
| + | 43 | 2B | { | 123 | 7B |
| , | 44 | 2C | \| | 124 | 7C |
| / | 47 | 2F | } | 125 | 7D |
| : | 58 | 3A | 无 | 无 | 无 |
## 签名步骤
### 步骤1:生成 KeyTime
1. 获取当前时间对应的 Unix 时间戳 StartTimestamp,Unix 时间戳是从 UTC(协调世界时,或 GMT 格林威治时间)1970年1月1日0时0分0秒(北京时间 1970年1月1日8时0分0秒)起至现在的总毫秒数。
2. 根据上述时间戳和期望的签名有效时长算出签名过期时间对应的 Unix 时间戳 EndTimestamp。
3. 拼接签名有效时间,格式为`StartTimestamp;EndTimestamp`,即为 KeyTime。
**示例:**`1592363963919;1593367993919`
### 步骤2:生成 SignKey
使用 `HMAC-SHA1` 以`SecretKey`为密钥,以 `KeyTime` 为消息,计算消息摘要(哈希值),即为 `SignKey`。
**示例:**`f48a7caaec408923b8ee49d802ab26d83591cfef`
### 步骤3:生成 UrlParamList 和 HttpParameters
1. 遍历 HTTP 请求参数,生成 key 到 value 的映射 Map 及 key 的列表 KeyList:
- key 使用 `UrlEncode`编码。
- value 使用 `UrlEncode` 编码。若无 value 的参数,则认为 value 为空字符串。例如请求路径为`/?acl`,则认为是`/?acl=`。
>? HTTP 请求参数,即请求路径中`?`以后的部分,例如请求路径为`/?versions&prefix=example-folder%2F&delimiter=%2F&max-keys=10`,则请求参数为`versions&prefix=example-folder%2F&delimiter=%2F&max-keys=10`。
2. 将 KeyList 按照字典序排序。
3. 按照 KeyList 的顺序拼接 Map 中的每一个键值对,格式为`key1=value1&key2=value2&key3=value3`,即为 HttpParameters。
4. 按照 KeyList 的顺序拼接 KeyList 中的每一项,格式为`key1;key2;key3`,即为 UrlParamList。
#### 示例:
- 示例一:
请求路径:`/?prefix=example-folder%2F&delimiter=%2F&max-keys=10`
UrlParamList:`delimiter;max-keys;prefix`
HttpParameters:`delimiter=%2F&max-keys=10&prefix=example-folder%2F`
>!请求路径中的请求参数在实际发送请求时也会进行 UrlEncode,因此要注意不要重复执行 UrlEncode。
- 示例二:
请求路径:`/exampleobject?acl`
UrlParamList:`acl`
HttpParameters:`acl=`
### 步骤4:生成 StringToSign
根据 `KeyTime` 和 `HttpParameters` 生成 `StringToSign`,格式为`sha1\nKeyTime\nSHA1(HttpParameters)\n`。
其中:
- sha1 为固定字符串。
- `\n`为换行符。
- SHA1(HttpParameters) 为使用 `SHA1` 对 `HttpParameters` 计算的消息摘要。
### 步骤5:生成 Signature
使用`HMAC-SHA1`以 `SignKey`为密钥,以`StringToSign`为消息,计算消息摘要,即为 `Signature`。
### 步骤6:生成签名
根据`SecretId`、`KeyTime`、`UrlParamList` 和 `Signature` 生成签名,格式为:
```
q-sign-time=KeyTime
&q-url-param-list=UrlParamList
&q-signature=Signature
&q-ak=SecretId
```
>!上述格式中的换行仅用于更好的阅读,实际格式并不包含换行。
## 签名使用
可以通过以下几种方式传递签名:
1. 通过标准的 HTTP Authorization 头,例如`Authorization: q-sign-time=1592...&q-url-param-list=a;b;c&q-signature=a40a5e...`
2. 作为 HTTP 请求参数,请注意 UrlEncode,例如`/exampleobject?q-sign-time=1592...&q-url-param-list=a;b;c&q-signature=a40a5e...`
3. 如需进一步防篡改,可将 Http 请求的更多元素进行签名,如请求方法(HttpMethod)、请求路径(UriPathname)、请求头(HttpHeaders),可参考 AWS S3 协议的完整签名方案。
>?上述示例中使用`...`省略了部分具体签名内容。
## 代码示例
### 伪代码
```plaintext
KeyTime = [Now];[Expires]
SignKey = HMAC-SHA1([SecretKey], KeyTime)
StringToSign = sha1\nKeyTime\nSHA1(HttpParameters)\n
Signature = HMAC-SHA1(SignKey, StringToSign)
```
### 消息摘要算法示例
不同语言如何调用 HMAC-SHA1 可以参考下面的示例:
#### PHP
```php
$sha1HttpString = sha1('ExampleHttpString');
$signKey = hash_hmac('sha1', 'ExampleKeyTime', 'YourSecretKey');
```
#### Java
```java
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.codec.digest.HmacUtils;
String sha1HttpString = DigestUtils.sha1Hex("ExampleHttpString");
String signKey = HmacUtils.hmacSha1Hex("YourSecretKey", "ExampleKeyTime");
```
#### Python
```python
import hmac
import hashlib
sha1_http_string = hashlib.sha1('ExampleHttpString'.encode('utf-8')).hexdigest()
sign_key = hmac.new('YourSecretKey'.encode('utf-8'), 'ExampleKeyTime'.encode('utf-8'), hashlib.sha1).hexdigest()
```
#### Node.js
```node
var crypto = require('crypto');
var sha1HttpString = crypto.createHash('sha1').update('ExampleHttpString').digest('hex');
var signKey = crypto.createHmac('sha1', 'YourSecretKey').update('ExampleKeyTime').digest('hex');
```
#### Go
```go
import (
"crypto/hmac"
"crypto/sha1"
)
h := sha1.New()
h.Write([]byte("ExampleHttpString"))
sha1HttpString := h.Sum(nil)
var hashFunc = sha1.New
h = hmac.New(hashFunc, []byte("YourSecretKey"))
h.Write([]byte("ExampleKeyTime"))
signKey := h.Sum(nil)
```
## 实际案例
### 请求示例
#### 原始请求
- **SecretId** = `12345`
- **SecretKey** = `BQYIM75p8x0iWVFSIgqEKwFprpRSVHlz`
```
GET /demo?a=1&b=2&c=3 HTTP/1.1
Date: Thu, 16 May 2019 06:45:51 GMT
Content-Type: text/plain
```
#### 中间变量
- **KeyTime** = `1592363963919;1593367993919`
- **SignKey** = `f48a7caaec408923b8ee49d802ab26d83591cfef`
- **UrlParamList** = `a;b;c`
- **HttpParameters** = `a=1&b=2&c=3`
- **StringToSign** = `sha1\n1592363963919;1593367993919\n147cb5937edc2fa8cb06a802bf0d64e0419a0fb1\n`
- **Signature** = `a4086a5ef76ccea81b0e65642446441f74326e0f`
其中,`\n`代表换行符。
#### 签名后的请求
```
GET /demo?a=1&b=2&c=3 HTTP/1.1
Date: Thu, 16 May 2019 06:45:51 GMT
Content-Type: text/plain
Authorization: q-sign-time=1592363963919;1593367993919&q-url-param-list=a;b;c&q-signature=a4086a5ef76ccea81b0e65642446441f74326e0f&q-ak=12345
```
> 本文档摘取自 https://cloud.tencent.com/document/product/436/7778
## 代码示例
```
package com.fiebug.test;
import org.junit.Test;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.*;
/**
* 接口签名测试类
*/
public class SignTest {
// 使用 HMAC-SHA1 以 SecretKey 为密钥,以 KeyTime 为消息,计算消息摘要(哈希值),即为 SignKey。
// 请求头 Authorization
// Ref. https://cloud.tencent.com/document/product/436/7778
/*
KeyTime = [Now];[Expires]
SignKey = HMAC-SHA1([SecretKey], KeyTime)
Signature = HMAC-SHA1(SignKey, UrlEncode([HttpParameters]))
q-sign-time=KeyTime
&q-url-param-list=UrlEncode(UrlParamList)
&q-signature=Signature
&q-ak=SecretId
*/
@Test
public void testSign() {
String secretId = "12345678";
String secretKey = "BQYIM75p8x0iWVFSIgqEKwFprpRSVHlz";
// String keyTime = "1593394537348;1593494537348";
String keyTime = System.currentTimeMillis() + ";" + (System.currentTimeMillis() + 5 * 60 * 1000);
// 构造参数
Map parameters = new LinkedHashMap<>();
parameters.put("a", "1");
parameters.put("b", "2");
parameters.put("c", "3");
parameters.put("特;殊", "4-特殊");
parameters.put("a&b", "5-a&b");
parameters.put("a=b", "6-a=b");
parameters.put("888", "88888");
parameters.put("null", null);
parameters.put("empty", "");
// 签名测试
String authorization = signature(secretId, secretKey, keyTime, parameters);
System.out.println(authorization);
System.out.println(getSecretId(authorization));
System.out.println(checkSignature(secretKey, authorization, parameters));
System.out.println("done");
}
/**
* 生成签名
*
* @param secretId 密钥 ID
* @param secretKey 密钥
* @param keyTime 密钥时间
* @param parameters 参数
* @return 签名串
*/
private String signature(String secretId, String secretKey, String keyTime, Map parameters) {
String SignKey = hmacSha1Hex(secretKey, keyTime);
StringBuilder urlParamListBuilder = new StringBuilder();
StringBuilder httpParametersBuilder = new StringBuilder();
if (parameters != null) {
List keyListOriginal = new ArrayList<>(parameters.keySet());
// 默认按字典排序
Collections.sort(keyListOriginal);
for (String key : keyListOriginal) {
urlParamListBuilder.append(urlEncode(key)).append(";");
httpParametersBuilder.append(urlEncode(key)).append("=").append(urlEncode(parameters.get(key))).append("&");
}
if (httpParametersBuilder.length() > 0) {
urlParamListBuilder.deleteCharAt(urlParamListBuilder.length() - 1);
httpParametersBuilder.deleteCharAt(httpParametersBuilder.length() - 1);
}
}
String HttpString = httpParametersBuilder.toString();
String StringToSign = "sha1\n" + keyTime + "\n" + sha1Hex(HttpString) + "\n";
String signature = hmacSha1Hex(SignKey, StringToSign);
// 组装签名串
return "q-sign-time=" + keyTime +
"&q-url-param-list=" + urlParamListBuilder +
"&q-signature=" + signature +
"&q-ak=" + secretId;
}
/**
* 校验签名
*
* @param secretKey 密钥
* @param authorization 签名串
* @param parameters 参数
* @return 校验结果
*/
private boolean checkSignature(String secretKey, String authorization, Map parameters) {
Map authorizationMap = new HashMap<>();
String[] qArr = authorization.split("&");
for (String qStr : qArr) {
String[] split = qStr.split("=");
if (split.length > 1) {
authorizationMap.put(split[0], split[1]);
} else {
authorizationMap.put(split[0], null);
}
}
String keyTime = authorizationMap.get("q-sign-time");
String urlParamStr = authorizationMap.get("q-url-param-list");
String[] urlParamArr = urlParamStr != null ? urlParamStr.split(";") : new String[0];
String signature = authorizationMap.get("q-signature");
// 校验时效
String[] keyTimeArr = keyTime.split(";");
if (System.currentTimeMillis() > Long.parseLong(keyTimeArr[1])) {
return false;
}
StringBuilder httpParametersBuilder = new StringBuilder();
if (parameters != null) {
for (String urlParam : urlParamArr) {
String key = urlDecoder(urlParam);
String value = parameters.get(key);
httpParametersBuilder.append(urlEncode(key)).append("=").append(urlEncode(value)).append("&");
}
if (httpParametersBuilder.length() > 0) {
httpParametersBuilder.deleteCharAt(httpParametersBuilder.length() - 1);
}
}
String SignKey = hmacSha1Hex(secretKey, keyTime);
String HttpString = httpParametersBuilder.toString();
String StringToSign = "sha1\n" + keyTime + "\n" + sha1Hex(HttpString) + "\n";
String signatureCheck = hmacSha1Hex(SignKey, StringToSign);
return signatureCheck.equals(signature);
}
private String getSecretId(String authorization) {
String[] qArr = authorization.split("&");
for (String qStr : qArr) {
String[] split = qStr.split("=");
if ("q-ak".equals(split[0])) {
if (split.length > 1) {
return split[1];
} else {
return null;
}
}
}
return null;
}
private String urlEncode(String str) {
try {
if (str == null) {
return URLEncoder.encode("", "UTF-8");
} else {
return URLEncoder.encode(str, "UTF-8");
}
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
private String urlDecoder(String str) {
try {
if (str == null) {
return URLDecoder.decode("", "UTF-8");
} else {
return URLDecoder.decode(str, "UTF-8");
}
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
private String sha1Hex(String data) {
if (data == null) {
throw new IllegalArgumentException("Input data cannot be null");
}
try {
MessageDigest md = MessageDigest.getInstance("SHA1");
byte[] dataBytes = data.getBytes(StandardCharsets.UTF_8);
md.update(dataBytes);
byte[] digestBytes = md.digest();
return bytesToHex(digestBytes);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("SHA1 algorithm not found", e);
}
}
private String hmacSha1Hex(String key, String valueToDigest) {
// 检查参数是否为 null
if (key == null) {
throw new IllegalArgumentException("Key cannot be null");
}
if (valueToDigest == null) {
throw new IllegalArgumentException("Value to digest cannot be null");
}
try {
// 创建HMAC-SHA1 Mac实例
Mac mac = Mac.getInstance("HmacSHA1");
// 创建密钥规格
SecretKeySpec secretKey = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "HmacSHA1");
// 初始化Mac
mac.init(secretKey);
// 计算HMAC
byte[] hmacBytes = mac.doFinal(valueToDigest.getBytes(StandardCharsets.UTF_8));
// 将字节数组转换为十六进制字符串
return bytesToHex(hmacBytes);
} catch (Exception e) {
throw new RuntimeException("Failed to compute HMAC-SHA1", e);
}
}
private static String bytesToHex(byte[] bytes) {
if (bytes == null) {
throw new IllegalArgumentException("Bytes array cannot be null");
}
StringBuilder hexString = new StringBuilder();
for (byte b : bytes) {
String hex = Integer.toHexString(0xFF & b);
if (hex.length() == 1) {
hexString.append('0');
}
hexString.append(hex);
}
return hexString.toString();
}
}
```
分类:编程 关键词:接口,签名
评论(0) 浏览(2906)