本文为您介绍独立部署API认证的简介、相关概念、优势、开发约定和鉴权方式。
认证简介
Quick BI开放API,采用的签名认证的方式校验请求的合法性。用户在调用API的时候,通过使用AccessKey ID(ak)/AccessKey Secret(sk)来加密按照特定规则拼接生成的字符串,生成签名(signature)字符串并附属在请求中。其中ak会携带在请求中,用于标识用户。sk用于加密拼接生成的字符串,所以,用户必须保密。
Quick BI在收到请求之后,系统将会根据ak找到对应的sk,并按照同样的规则拼接生成字符串,使用sk进行签名计算(signature)。如果收到的签名与系统端生成的签名一致,则校验通过;否则,校验不通过,接口认证失败。
以下签名规则的详细讲解,适合纯手工调取API的用户,对于使用SDK的用户,无需关心,因为签名规则已经封装在SDK中。
名词解释
ak(AccessKey ID):用于标识用户,可附在HTTP请求中。通过Quick BI的开放平台 > 组织识别码获取。
sk(AccessKey Secret):用于加密拼接生成的字符串,用户必须保密。
string_to_sign:待签名的字符串,按照指定规则拼接而成的字符串,经过SHA256算法生成签名。
signature:签名字符串。
路径参数:携带在URI中的参数。例如:
/openapi/v2/user/{userId}
。具体的userId便是路径参数。Query参数:携带在URL的参数中。例如:
/openapi/v2/user?city=hangzhou
。其中,city=hangzhou
就是路径Query参数。表单参数:对于POST和PUT请求,如果
content-type=application/x-wwww-url-encoded
。则参数会以键值对的形式附属在HTTP请求的body entity中。&uuid=confsnc6rfn&release=true`
json串参数:对于POST和PUT请求,如果
content-type=application/json
,则参数会以JSON串的形式附属在body entity中。{"area_id":123,"uuid":"cofsnc6rfn","release":true}`
认证优势
过期校验:默认下,每个请求会携带请求发生的时间戳,后端会对请求的时效性进行判断。默认有效期时差3分钟,超过3分钟,则请求验证失效。
重放攻击校验:每个请求会附带一个随机数,并且,该随机数加入到签名字符串的拼接当中。保证一次请求,只能使用一次。
参数防篡改:路径参数、Query参数、表单参数加入到签名生成的算法中,防止参数篡改。
开发约定
Content-Type:application/json;charset-utf8
。如果没有特殊说明,Quick BI开放API调用,http请求头中的Content_Type
,一律指定为application/json;charset-utf-8
。涉及到字符串拼接、加密签名生成,字符的编码方式,没有特殊说明,统一采用UTF-8编码。
鉴权方式
Quick BI开放API,采用的签名认证的方式校验请求的合法性。签名的具体规则如下图所示。
主体签名分成两个步骤:
构建待签名字符串
string_to_sign
。使用密钥(sk)对拼接的
string_to_sign
进行签名操作,加密算法使用HMAC-SHA256。
待签名字符串(string_to_sign)
拼接待签名字符串string_to_sign的拼接规则如下:
string_to_sign =
Request_Method\n
Request_Uri\n
Request_QueryString\n
Request_Headers
上述各部分,以换行符\n
分割连接(非字符串"\n"
),各部分说明如下:
表1-1 待签名字符串拼接说明
拼接部分 | 说明 | 示例 |
Request_Method | http方法名:GET | POST | PUT | DELETE 。 格式为大写 | GET |
Request_Uri | 原始请求的相对路径,不包含host和URL请求参数。 说明 如果Uri中携带符号 | 对于请求: |
Request_QueryString | 主要由请求中所有Query参数、所有表单参数拼接而成。 拼接规则:
说明
| 例如: 对于请求: 其中,Query参数键值对: status=3 pageNo=1 pageSize=10 key= 按照参数名排序,顺序为: key > pageNo > pageSize > status 由于key的参数为null,因此不加入拼接,所以,拼接的Request_QueryString如下: pageNo=1&pageSize=10&status=3 |
Request_Headers | 参与签名的请求头。具体参与拼接的请求头,参考表1-2。 拼接规则: 对请求参数名按照字典顺序从小到大排序,然后拼接。拼接时,每组键值对用 | - |
表1-2、签名公共请求头(Header)
请求头 | 是否必选 | 是否加入签名拼接 | 说明 |
X-Gw-AccessId | 是 | 是 | Quick BI颁发的ak,用户标记用户身份。 |
X-Gw-Timestamp | 是 | 是 | 请求的发生时间,Unix时间戳,精确到毫秒。通过该字段,进行过期校验;如果与后台服务的系统时间相差3分钟,则表示请求过期,校验不通过。 |
X-Gw-Nonce | 是 | 是 | 唯一随机数。在不同请求间使用不同的随机数值,防止网络重放攻击。随机数生成以下参考UUID生成。 |
X-Gw-ExtHeaders | 否 | 否 (如果有定义,则定义指定的请求头加入签名生成) | 其他自定义需要加入签名的请求头。如果此处有定义,会将定义的请求头的键值对加入到签名计算中。主要用于扩展。 |
X-Gw-Signature | 是 | 否 | 签名字符串。用于检验请求的合法性。 |
Content-Type | 是 | 否 | 如果没有特殊说明,一律按照如下进行:
|
X-Gw-Debug | 否 | 否 | 如果X-Gw-Debug:true,在请求签名不通过时,则可以通过返回的response的请求头中,获取后端的拼接的字符串以及签名字符串,用于比对调试。
|
其中X-Gw-AccessId
、X-Gw-Timestamp
、X-Gw-Nonce
、X-Gw-Signature
四个鉴权参数需要置于Header中。
构建待签名字符串,可以参考以下代码:
/**
* 构建待签名的字符串
*
* @param uri 请求的URI
* @param method 方法,GET、POST
* @param headers 参与签名的请求头
* @param parameters 参与签名的请求参数。
* @return
*/
public static String buildStringToSign(String uri, String method,
Map<String, String> parameters,
Map<String, String> headers) {
if (StringUtils.isEmpty(uri) || StringUtils.isEmpty(method)) {
throw new IllegalArgumentException("input parameter error, uri or method can not be null");
}
// URL中。按照原始符号 --> 浏览器URL编码 --> spring web解析接收,对于加号(+)和空格, 有如下问题:
// + -> %2B --> 空格;
// 空格 -> %20 --> 空格;因此,对于spring web接收到的请求,并不清楚空格的原始对应,是 + 还是空格
// 因此,此处对于源头所有的+,按照空格处理
uri = uri.replace("+", " ");
// method
StringBuilder sb = new StringBuilder();
sb.append(method.toUpperCase());
sb.append(CommonConstants.LF);
// uri
sb.append(uri);
sb.append(CommonConstants.LF);
// paramters
if (null != parameters && parameters.size() > 0) {
String queryString = buildSortedString(parameters, "=", "&");
if (StringUtils.isNotEmpty(queryString)) {
sb.append(queryString);
sb.append(CommonConstants.LF);
}
}
// headers
if (null != headers && headers.size() > 0) {
String headerString = buildSortedString(headers, ":", "\n");
if (StringUtils.isNotEmpty(headerString)) {
sb.append(headerString);
}
}
return sb.toString();
}
/**
* 将map中的元素,按照key的字母顺序,进行排序
*
* @param maps
* @return
*/
private static String buildSortedString(Map<String, String> maps, String symbol1, String symbol2) {
StringBuilder sb = new StringBuilder();
List<String> keys = new LinkedList<String>();
for (String key : maps.keySet()) {
keys.add(key);
}
Collections.sort(keys);
for (int i = 0; i < keys.size(); i++) {
String key = keys.get(i);
String value = maps.get(key);
// key或者value为空或null,不加入字符串的拼接
if (null == key || key.trim().length() == 0
|| null == value || value.trim().length() == 0) {
continue;
}
sb.append(key);
sb.append(symbol1);
sb.append(value);
if (i != keys.size() - 1) {
sb.append(symbol2);
}
}
return sb.toString();
}
生成签名
生成了待签名字符串之后,使用sk对待签名字符串进行加密,生成最终的签名(signature)。具体的签名规则如下:
signature = HMAC-SHA256-BASE64(sk, percentURLEncode(string_to_sign))
其中,需要说明的是:
percentURLEncode:表示待签名字符串的编码以及特殊字符的处理。编码规则如下:
对于字符 A~Z、a~z、0~9 以及字符“-”、“_”、“.”、“~”不编码。
对于其它字符编码成 %XY 的格式,其中 XY 是字符对应 ASCII 码的 16 进制表示。比如英文的双引号(”)对应的编码为 %22。
对于扩展的 UTF-8 字符,编码成 %XY%ZA… 的格式。
英文空格( )要编码成 %20,而不是加号(+)。
该编码方式和一般采用的application/x-www-form-urlencoded MIME 格式编码算法(比如 Java 标准库中的 java.net.URLEncoder 的实现)相似,但又有所不同。实现时,可以先用标准库的方式进行编码,然后把编码后的字符串中加号+
替换成 %20
、星号*
替换成%2A
、%7E
替换回波浪号(~),即可得到上述规则描述的编码字符串。这个算法可以用下面的 percentEncode 方法来实现。
private static String percentEncode(String value) throws UnsupportedEncodingException {
return value != null ? URLEncoder.encode(value, "UTF-8").replace("+", "%20").replace("*", "%2A").replace("%7E", "~") : null;
}
例如待签名字符串为:
GET
/openapi/v2/works/95296e95-ca89-4c7d-8af9-dedf0ad06adf
worksType=DATAPRODUCT
X-Gw-AccessId:2fe4fbd8-1234-1234-1234-e92c7af083ea
X-Gw-Nonce:8dcdc141-5736-4c0b-bcf9-061a9970b6e3
X-Gw-Timestamp:1653288028340
encode后可得:
GET%0A%2Fopenapi%2Fv2%2Fworks%2F95296e95-ca89-4c7d-8af9-dedf0ad06adf%0AworksType%3DDATAPRODUCT%0AX-Gw-AccessId%3A2fe4fbd8-1234-1234-1234-e92c7af083ea%0AX-Gw-Nonce%3A7d71ed2d-d3d4-42ff-a418-7edaad39f773%0AX-Gw-Timestamp%3A1653288135869
不同语言的encode方法不同,若遇到无法解析的符号,请参考上述样例手动替换。
HMAC-SHA256-BASE64:表示先进行SHA256编码,而后采用BASE64对生成的结果进行加密。该算法可以参考如下代码:
public static String sign(String stringToSign, String secretKey) {
if (null == stringToSign || null == secretKey) {
throw new IllegalArgumentException("input parameter error");
}
String encodeString = percentEncode(stringToSign);
return sha256(encodeString, secretKey);
}
public static String sha256(String content, String secret) throws NoSuchAlgorithmException,
UnsupportedEncodingException, InvalidKeyException {
Mac hamcSha256 = Mac.getInstance("HmacSHA256");
byte[] keyBytes = secret.getBytes("UTF-8");
SecretKeySpec secretKey = new SecretKeySpec(keyBytes, 0, keyBytes.length, "HmacSHA256");
hamcSha256.init(secretKey);
byte[] result = hamcSha256.doFinal(content.getBytes("UTF-8"));
return new String(Base64.encodeBase64(result));
}