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=
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 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 %格式化输出