apollo源码分析过程:构造apollo签名请求

apollo 1.6之后的秘钥访问原理

参考:
秘钥访问的通用原理:https://blog.csdn.net/qq_25595025/article/details/82627349
apollo秘钥访问的原理:https://blog.csdn.net/qq_38385659/article/details/105292458


2020年3月份,携程发布了apollo配置中心1.6.0版本,这个版本一大亮点就是增加了密钥的验证以及管理功能,也就是说客户端必须用密钥对http请求签名才可以访问配置信息,这样一来,不但提高了配置中心的安全性,也让配置中心部署到公共环境成为可能。

一、总体源码分析

apollo\apollo-configservice\src\main\java\com\ctrip\framework\apollo\configservice\filter\ClientAuthenticationFilter.java
55-74行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
if (!CollectionUtils.isEmpty(availableSecrets)) {
  String timestamp = request.getHeader(Signature.HTTP_HEADER_TIMESTAMP);
  String authorization = request.getHeader(HttpHeaders.AUTHORIZATION);
 
  // check timestamp, valid within 1 minute
  if (!checkTimestamp(timestamp)) {
    logger.warn("Invalid timestamp. appId={},timestamp={}", appId, timestamp);
    response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "RequestTimeTooSkewed");
    return;
  }
 
  // check signature
  String uri = request.getRequestURI();
  String query = request.getQueryString();
  if (!checkAuthorization(authorization, availableSecrets, timestamp, uri, query)) {
    logger.warn("Invalid authorization. appId={},authorization={}", appId, authorization);
    response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
    return;
  }
}

  

秘钥安全访问机制作了2个方面的检查:
1、时间戳检查。前后不超过一分钟。header头中的键名Timestamp。
2、签名检查。secret、时间戳、uri、query作为入参,计算出签名验证client端的签名。header头中的键名Authorization。

二、时间戳相关源码分析


apollo\apollo-configservice\src\main\java\com\ctrip\framework\apollo\configservice\filter\ClientAuthenticationFilter.java

56行:# 说明服务器端从请求体的header中获取时间戳。

1
String timestamp = request.getHeader(Signature.HTTP_HEADER_TIMESTAMP);  

  

87行:# 说明时间戳以毫秒为单位。

1
requestTimeMillis = Long.parseLong(timestamp);

  

apollo\apollo-core\src\main\java\com\ctrip\framework\apollo\core\signature\Signature.java

20行:# 说明header中时间戳的键名为Timestamp。

1
public static final String HTTP_HEADER_TIMESTAMP = "Timestamp";

  

构造请求时间戳:

在header中添加一个字段,值为毫秒时间戳,且不超过最近一分钟。

Timestamp: 1609913782428


三、签名相关源码分析

apollo\apollo-configservice\src\main\java\com\ctrip\framework\apollo\configservice\filter\ClientAuthenticationFilter.java

96-114行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private boolean checkAuthorization(String authorization, List<String> availableSecrets,
    String timestamp, String path, String query) {
 
  String signature = null;
  if (authorization != null) {
    String[] split = authorization.split(":");
    if (split.length > 1) {
      signature = split[1];
    }
  }
 
  for (String secret : availableSecrets) {
    String availableSignature = accessKeyUtil.buildSignature(path, query, timestamp, secret);
    if (Objects.equals(signature, availableSignature)) {
      return true;
    }
  }
  return false;
}

  

1、client端传来的签名值中有冒号,冒号后的一段为签名值Sign。
2、服务端为了校验签名的计算签名availableSignature值,是由path、query、timestamp、secret这四个入参计算而得。


apollo\apollo-configservice\src\main\java\com\ctrip\framework\apollo\configservice\util\AccessKeyUtil.java

51-58行:

1
2
3
4
5
6
7
8
public String buildSignature(String path, String query, String timestampString, String secret) {
  String pathWithQuery = path;
  if (!Strings.isNullOrEmpty(query)) {
    pathWithQuery += "?" + query;
  }
 
  return Signature.signature(timestampString, pathWithQuery, secret);
}

  

先将path和query中间用问号?拼接为pathWithQuery后,再计算timestamp、pathWithQuery、secret这三个入参的签名。


apollo\apollo-core\src\main\java\com\ctrip\framework\apollo\core\signature\Signature.java

17行:# header中签名的值,格式为"Apollo xx:yy"。

1
private static final String AUTHORIZATION_FORMAT = "Apollo %s:%s";

  

apollo\apollo-core\src\main\java\com\ctrip\framework\apollo\core\signature\Signature.java

22-25行:

1
2
3
4
public static String signature(String timestamp, String pathWithQuery, String secret) {
  String stringToSign = timestamp + DELIMITER + pathWithQuery;
  return HmacSha1Utils.signString(stringToSign, secret);
}

  

先将timestamp和pathWithQuery中间用换行符"\n"拼接成stringToSign后,再计算stringToSign、secret这两个入参的签名。

 

35行:# 签名字段Authorization的构造方法,值中含appId和signature

1
headers.put(HttpHeaders.AUTHORIZATION, String.format(AUTHORIZATION_FORMAT, appId, signature));

  

apollo\apollo-core\src\main\java\com\ctrip\framework\apollo\core\signature\HmacSha1Utils.java

13-31行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class HmacSha1Utils {
 
  private static final String ALGORITHM_NAME = "HmacSHA1";
  private static final String ENCODING = "UTF-8";
 
  public static String signString(String stringToSign, String accessKeySecret) {
    try {
      Mac mac = Mac.getInstance(ALGORITHM_NAME);
      mac.init(new SecretKeySpec(
          accessKeySecret.getBytes(ENCODING),
          ALGORITHM_NAME
      ));
      byte[] signData = mac.doFinal(stringToSign.getBytes(ENCODING));
      return BaseEncoding.base64().encode(signData);
    } catch (NoSuchAlgorithmException | UnsupportedEncodingException | InvalidKeyException e) {
      throw new IllegalArgumentException(e.toString());
    }
  }
}

  

signString方法实现了对stringToSign和secret的合并签名。依赖了maven中的一些google算法类。


构造请求时间戳:
在header中添加一个字段,值为带标识的sign,且值中含义冒号:。
Authorization: Apollo xx:yy。
xx为appId
yy为signature

 四、构造签名请求的关键参数:java实现

 =================优美的分割线1:java构造参数=================

apollo\apollo-core\src\test\java\com\ctrip\framework\apollo\core\signature\SignatureTest.java

public void testSignature() 方法和方法体,用如下代码替换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
    @Test
    public void testSignature() {
        // header头中两个关键字段的构造格式
        final String TIMESTAMP_FORMAT = "Timestamp: %s";
        final String AUTHORIZATION_FORMAT = "Authorization: Apollo %s:%s";
 
        //构造时间戳
        long requestTimeMillis = System.currentTimeMillis();
        String Timestamp_str = String.format(TIMESTAMP_FORMAT, requestTimeMillis);
        System.out.println(Timestamp_str);
 
        //构造含有签名的字符串
        String timestamp = Long.toString(requestTimeMillis);
        String pathWithQuery = "/configs/dev01/saas/application";
        String appid = "dev01";
        String secret = "9c394bd3beef482e933e27225c740902";
 
        String actualSignature = Signature.signature(timestamp, pathWithQuery, secret);
        String Authorization_str = String.format(AUTHORIZATION_FORMAT, appid, actualSignature);
        System.out.println(Authorization_str);
 
        String expectedSignature = "EoKyziXvKqzHgwx+ijDJwgVTDgE=";
//        assertEquals(expectedSignature, actualSignature);
        assertEquals("EoKyziXvKqzHgwx+ijDJwgVTDgE=", "EoKyziXvKqzHgwx+ijDJwgVTDgE=");
    }

  

运行该签名测试:动态输出

Timestamp: 1609924706320

Authorization: Apollo dev01:TIaxkWvnay6pxVw1p+vyi0f2FMs=

 

 五、构造签名请求的关键参数:python2实现

 =================优美的分割线2:python2构造参数=================

python2构造header中的两个关键参数

实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# -*- coding: utf-8 -*-
import time
import hmac
from hmac import new as hmac
from hashlib import sha1
 
 
# 哈希算法
def hash_hmac(secret, stringToSign):
    '''哈希算法
    :param secret: 秘钥。保密的值,不要在网络中传输。
    :param stringToSign: 待哈希的字串
    :return: 哈希值
    '''
    return str(hmac(secret, stringToSign, sha1).digest().encode('base64')[:-1])
 
 
# 构造时间戳
def get_timestamp_ms():
    # 1、构造毫秒级的时间戳
    ms = int(round(time.time() * 1000))  # 毫秒级时间戳
    # print(timestamp_millis)
    return ms
 
 
# 构造签名
def signature(timestamp, pathWithQuery, secret):
    '''构造签名
    :param timestamp: 毫秒级时间戳
    :param pathWithQuery: 待签名字串
    :param secret: 签名使用的秘钥
    :return: 签名
    '''
    # 2、构造签名
    # 2.1 构造待签名字串
    stringToSign = str(timestamp) + "\n" + pathWithQuery  # 待签名字串
 
    # 2.2 计算出一个新的签名
    secret = "9c394bd3beef482e933e27225c740902"  # 秘钥。保密的值,不要在网络中传输。
    sign = hash_hmac(secret, stringToSign)  # 通过哈希算法,输出一个新签名
    # print(sign)
 
    return sign
 
 
# 构造header中两个关键的键值对
def build_some_header(timestamp_str, appid, sign):
    '''构造header中两个关键的键值对
    :param timestamp_str:毫秒级时间戳
    :param appid:项目名称
    :param sign:签名
    :return:
    '''
    # 3、构造header中两个关键的键值对
    # 3.1 header头中两个关键字段的构造格式
    TIMESTAMP_FORMAT = "Timestamp: %s"
    AUTHORIZATION_FORMAT = "Authorization: Apollo %s:%s"
 
    # 3.2 输出header构建结果
    header_timestamp_str = TIMESTAMP_FORMAT % timestamp_str
    header_authorization_str = AUTHORIZATION_FORMAT % (appid, sign)
    print(header_timestamp_str)
    print(header_authorization_str)
 
 
if __name__ == '__main__':
    pathWithQuery = "/configs/dev01/saas/application"
    secret = "9c394bd3beef482e933e27225c740902"  # 秘钥。保密的值,不要在网络中传输。
    appid = "dev01"
 
    # 1、构造毫秒级的时间戳
    timestamp_millis = get_timestamp_ms()
 
    # 2、构造签名
    sign = signature(timestamp_millis, pathWithQuery, secret)
 
    # 3、构造header中两个关键的键值对
    # 3.1 header头中两个关键字段的构造格式
    TIMESTAMP_FORMAT = "Timestamp: %s"
    AUTHORIZATION_FORMAT = "Authorization: Apollo %s:%s"
    # 3.2 输出header构建结果
    header_timestamp_str = TIMESTAMP_FORMAT % timestamp_millis
    header_authorization_str = AUTHORIZATION_FORMAT % (appid, sign)
    print(header_timestamp_str)
    print(header_authorization_str)

  

输出(动态产生的,时间戳有效期1min)

Timestamp: 1609924288391
Authorization: Apollo dev01:Dj/ep/mZB/AMnL88Qp7mJx3cBcU=

通过渗透测试工具burp测试:

 

 获得正确响应,说明构造有效

六、构造签名请求的关键参数:python3实现

=================优美的分割线3:python3构造参数=================

python3构造header中的两个关键参数

和python2的区别是哈希算法函数hash_hmac()不一样

python3代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
# -*- coding: utf-8 -*-
import time
import base64
import hmac
from hashlib import sha1
 
 
# 哈希算法
def hash_hmac(secret, stringToSign):
    '''哈希算法
    :param secret: 秘钥。保密的值,不要在网络中传输。
    :param stringToSign: 待哈希的字串
    :return: 哈希值
    '''
    ENCODING = "utf-8"
    # 编码为字节流
    secret_bytes = secret.encode(ENCODING)
    stringToSign_bytes = stringToSign.encode(ENCODING)
    hmac_sha1 = hmac.new(
        secret_bytes,
        stringToSign_bytes,
        sha1,
    )
    # 以二进制的字节流返回。如:b'\xca\xa2\xd0\xe6\x1cg\xca?eOSm\xcb\x9b\x92\xde\xb0\xdah\r'
    hmac_sha1_bin_bytes = hmac_sha1.digest()
    # print(hmac_sha1_bin_bytes)
 
    # 将二进制的字节流按照base64规则编码,返回base64的字节流。如:b'yqLQ5hxnyj9lT1Nty5uS3rDaaA0='
    hmac_sha1_base64_bytes = base64.b64encode(hmac_sha1_bin_bytes)
    # print(hmac_sha1_base64_bytes)
 
    # 返回哈希值。base64版,如:yqLQ5hxnyj9lT1Nty5uS3rDaaA0=
    hmac_sha1_base64 = hmac_sha1_base64_bytes.decode(ENCODING)
    # print(hmac_sha1_base64)
 
    return hmac_sha1_base64
 
 
# 构造时间戳
def get_timestamp_ms():
    # 1、构造毫秒级的时间戳
    ms = int(round(time.time() * 1000))  # 毫秒级时间戳
    # print(timestamp_millis)
    return ms
 
 
# 构造签名
def signature(timestamp, pathWithQuery, secret):
    '''构造签名
    :param timestamp: 毫秒级时间戳
    :param pathWithQuery: 待签名字串
    :param secret: 签名使用的秘钥
    :return: 签名
    '''
    # 2、构造签名
    # 2.1 构造待签名字串
    stringToSign = str(timestamp) + "\n" + pathWithQuery  # 待签名字串
 
    # 2.2 计算出一个新的签名
    sign = hash_hmac(secret, stringToSign)  # 通过哈希算法,输出一个新签名
    # print(sign)
 
    return sign
 
 
# 构造header中两个关键的键值对
def build_some_header(timestamp_str, appid, sign):
    '''构造header中两个关键的键值对
    :param timestamp_str:毫秒级时间戳
    :param appid:项目名称
    :param sign:签名
    :return:
    '''
    # 3、构造header中两个关键的键值对
    # 3.1 header头中两个关键字段的构造格式
    TIMESTAMP_FORMAT = "Timestamp: %s"
    AUTHORIZATION_FORMAT = "Authorization: Apollo %s:%s"
 
    # 3.2 输出header构建结果
    header_timestamp_str = TIMESTAMP_FORMAT % timestamp_str
    header_authorization_str = AUTHORIZATION_FORMAT % (appid, sign)
    print(header_timestamp_str)
    print(header_authorization_str)
 
 
if __name__ == '__main__':
    pathWithQuery = "/configs/dev01/saas/application"
    secret = "9c394bd3beef482e933e27225c740902"  # 秘钥。保密的值,不要在网络中传输。
    appid = "dev01"
 
    # 1、构造毫秒级的时间戳
    timestamp_millis = get_timestamp_ms()
 
    # 2、构造签名
    sign = signature(timestamp_millis, pathWithQuery, secret)
 
    # 3、构造header中两个关键的键值对
    # 3.1 header头中两个关键字段的构造格式
    TIMESTAMP_FORMAT = "Timestamp: %s"
    AUTHORIZATION_FORMAT = "Authorization: Apollo %s:%s"
    # 3.2 输出header构建结果
    header_timestamp_str = TIMESTAMP_FORMAT % timestamp_millis
    header_authorization_str = AUTHORIZATION_FORMAT % (appid, sign)
    print(header_timestamp_str)
    print(header_authorization_str)

  

输出:

Timestamp: 1609930548812
Authorization: Apollo dev01:qRegdutePskKrlm6byTFaPeNebQ=

 

posted @   安迪9468  阅读(784)  评论(1编辑  收藏  举报
编辑推荐:
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
阅读排行:
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 提示词工程——AI应用必不可少的技术
· Open-Sora 2.0 重磅开源!
· 字符编码:从基础到乱码解决
历史上的今天:
2020-01-06 购房-小结
2019-01-06 if嵌套和elif的区别
2019-01-06 if判断代码 转变为 流程图
2019-01-06 python命名规范
2019-01-06 %格式化输出
点击右上角即可分享
微信分享提示