API认证逻辑

本文为您介绍独立部署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):用于加密拼接生成的字符串,用户必须保密。

image
  • string_to_sign:待签名的字符串,按照指定规则拼接而成的字符串,经过SHA256算法生成签名。

  • signature:签名字符串。

  • 路径参数:携带在URI中的参数。例如:/openapi/v2/user/{userId}。具体的userId便是路径参数。

  • Query参数:携带在URL的参数中。例如:/openapi/v2/user?city=hangzhou。其中,city=hangzhou就是路径Query参数。

  • 表单参数:对于POSTPUT请求,如果content-type=application/x-wwww-url-encoded。则参数会以键值对的形式附属在HTTP请求的body entity中。&uuid=confsnc6rfn&release=true`

  • json串参数:对于POSTPUT请求,如果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,采用的签名认证的方式校验请求的合法性。签名的具体规则如下图所示。image.png

主体签名分成两个步骤:

  1. 构建待签名字符串string_to_sign

  2. 使用密钥(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中携带符号+,则需要先将+替换成空格。

对于请求:http://abc.test/openapi/v2/user?status=3。其中的Uri/openapi/v2/user

Request_QueryString

主要由请求中所有Query参数、所有表单参数拼接而成。

拼接规则:

  • 对请求参数名按照字典顺序从小到大排序,然后拼接。拼接时,每组键值对用"="链接,键值对之间,用"&"连接。

说明

  • json参数不参与。当POSTPUT请求且content-type=application/json时,参数会以JSON串格式附在请求体中,因此,不参数该字段的拼接。

  • 对于参数键值对,如果参数名或者参数值为空时,不参与拼接。

  • 对于表单参数,可能存在同一个参数有多个值的情况。此时先要将value进行字母排序,中间以英文逗号分割开,拼接成一个value。当做一组参数键值对。

例如:

对于请求:http://abc.test/openapi/v2/user?status=3&pageNo=1&pageSize=10&key=

其中,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。

拼接规则:

对请求参数名按照字典顺序从小到大排序,然后拼接。拼接时,每组键值对用:链接,键值对之间以换行符\n连接。请求头值为空时,不参与拼接。

-

表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

如果没有特殊说明,一律按照如下进行:

content-type:application/json;charset=utf-8

X-Gw-Debug

如果X-Gw-Debug:true,在请求签名不通过时,则可以通过返回的response的请求头中,获取后端的拼接的字符串以及签名字符串,用于比对调试。

R-Gw-String-To-Sign:后端拼接的待签名字符串。

R-Gw-Signatured:后端生成的签名字符串。

说明

其中X-Gw-AccessIdX-Gw-TimestampX-Gw-NonceX-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))

其中,需要说明的是:

  1. 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方法不同,若遇到无法解析的符号,请参考上述样例手动替换。

  1. 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));
    }