HMAC
HMAC是一种利用密码学中的散列函数来进行消息认证的一种机制,所能提供的消息认证包括两方面内容:
①消息完整性认证:能够证明消息内容在传送过程没有被修改。
②信源身份认证:因为通信双方共享了认证的密钥,接收方能够认证发送该数据的信源与所宣称的一致,即能够可靠地确认接收的消息与发送的一致。
HMAC是当前许多安全协议所选用的提供认证服务的方式,应用十分广泛,并且经受住了多种形式攻击的考验。
HMAC与一般的加密重要的区别在于它具有“瞬时"性,即认证只在当时有效,而加密算法被破解后,以前的加密结果就可能被解密。
Kong配置
请求参数
参数说明
需要增加2个request header:
- Date:当前时间(为了方便对接,开发阶段此值设定为距当前时间1小时内),GMT格式,与北京时间相差8小时。例:
Sat, 10 Oct 2020 08:10:21 GMT
。(该参数除了用于参与签名计算,更重要的作用是防止重复攻击。假设抓包到了请求数据,包含当前日期以及认证信息,如果该日期没有时间限制,就可以一直重复使用) - Authorization:
hmac username="app", algorithm="hmac-sha256", headers="date request-line", signature="Vr7f7g6dzwxW6bP7dSiJutyOm8aiJl+IG5XJqyOHERU="
,其中username的值由网关给出,signature为Base64编码后的签名,其余固定。
签名算法
使用HMAC-SHA256加密算法,参数为message和secret。secret由网关给出。伪代码描述:
message="date: Thu, 22 Jun 2017 17:15:21 GMT\nGET /requests HTTP/1.1"
secret="secret"
digest=HMAC-SHA256(message, secret)
base64_digest=base64(digest)
message格式:
- 如果request header的name不是
request-line
,追加小写的header name,追加冒号和空格,追加值。比如date: Thu, 22 Jun 2017 17:15:21 GMT
。 - 如果是
request-line
,追加HTTP request line
。 - 多个request header中间使用
\n
隔开。
message参数:
- Date:客户端增加的参数,见参数说明,符合message格式的第一条,注意冒号和值之间有一个空格。
- Request-Line:Request-Line=Method空格Request-URI空格HTTP-Version。例如:
GET /request?a=11&b=22 HTTP/1.1
- Request-URI:上面Request-Line的一部分,the path with the querystring。例如:url为
https://example.com:1234/v1/movies?movie=foo
,则Request-URI=/v1/movies?movie=foo
完整的message示例:date: Thu, 22 Jun 2017 17:15:21 GMT\nGET /request?a=11&b=22 HTTP/1.1
举个例子
假设有GET请求接口地址为:http://kong.alicontainer.com/app/hot
Date为:Sat, 15 Oct 2020 07:40:21 GMT
则message为:date: Sat, 15 Oct 2020 07:40:21 GMT\nGET /app/hot HTTP/1.1
signature为:cCUTMy/11ZkyAtp+iSh1/BftKJODibZQgf4Oi8q/d9o=
异常说明
http status:401
-
header中没有date参数,或者date时间不合法(距当前时间5分钟内,gmt格式,与北京时间差8小时)
{ "message": "HMAC signature cannot be verified, a valid date or x-date header is required for HMAC Authentication"}
-
签名错误
{ "message": "HMAC signature does not match"}
客户端实现
客户端当根据自己的环境使用拦截器思想,在请求即将发出前统一拦截,在header中增加认证参数,以下仅给出获取计算signature的代码示例。
Java版本
/**
* 获取当前GMT format 时间
*
* @return
*/
public static String getGMTTime() {
DateFormat dateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss 'GMT'", Locale.US);
dateFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
return dateFormat.format(Calendar.getInstance().getTime());
}
public static String sign(String secret, String message) {
try {
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKey = new SecretKeySpec(secret.getBytes(), "HmacSHA256");
mac.init(secretKey);
return Base64.encodeBase64String(mac.doFinal(message.getBytes()));
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new IllegalArgumentException("签名错误:" + e.getMessage());
}
}
public static void main(String[] args) {
String secret = "dapeng";
String message = "date: Sat, 10 Oct 2020 08:10:21 GMT\nGET /mock?a=1 HTTP/1.1";
String sign = sign(secret, message);
System.out.println(sign);//Vr7f7g6dzwxW6bP7dSiJutyOm8aiJl+IG5XJqyOHERU=
}
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.15</version>
</dependency>
body验证
body数据签名验证需要增加另一个header:Digest
,value为:Digest: SHA-256=base64(sha256(<body>)),例如:SHA-256=RgecWaKvMqh5T79Ps93OuyZ5CZuPnZ6dTgsh5i9xJUs=
将body字符串使用sha256加密,再使用base64编码,这里不涉及secret。
java版本:
public static String String2SHA256(String str) {
try {
MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
byte[] hash = messageDigest.digest(str.getBytes());
return Base64.encodeBase64String(hash);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("签名错误:" + e.getMessage());
}
}
public static void main(String[] args) {
Map<String, String> map = new HashMap<>(1);
map.put("name", "123");
String body = JSON.toJSONString(map);
System.out.println(String2SHA256(body));
}