API 开放鉴权

流量入口管理

创建流量入口

如需开放 API 鉴权,创建流量入口时需选择 面向合作伙伴开放 API 场景。

API 开放鉴权 - 图1

调用方认证方式

  • Key 认证:通过请求参数中的 appKey 字段,或请求头中的 X-App-Key 识别调用方。
  • HMAC 签名认证:使用 HMAC 对请求行、请求头、请求 Body 进行加密,具备较高的安全性,因根据 HTTP 签名算法标准草案设计,同时具备一定的通用性,具体请参见 HMAC 签名认证
  • 参数签名认证:通过请求参数中的 appKey 字段识别调用方,同时通过对参数进行签名的 sign 字段完成校验,具体请参见 参数签名认证
  • OAuth2 认证:基于 OAuth2 Client Credentials 模式,通过动态 Token 识别调用方,调用方可借助类似 Spring Cloud Security 的库实现。

调用方访问条件

  • 认证通过:仅需携带的调用凭证正确,即可通过识别进行访问。
  • 认证通过 + 授权许可:需额外针对调用方授权后,对应调用方才可访问。

调用者授权

若调用方访问条件选择 认证通过 + 授权许可,则需进行调用者授权。

您可以在创建流量入口时完成调用者授权,也可以在后续通过流量入口编辑操作完成。

API 开放鉴权 - 图2

调用量控制

您可以在创建流量入口时完成调用量控制,也可以在后续通过流量入口编辑操作完成。

API 开放鉴权 - 图3

调用方凭证管理

选择对应调用方后,点击 凭证

API 开放鉴权 - 图4

调用方需根据流量入口的认证方式,选择正确的凭证。

API 开放鉴权 - 图5

签名认证算法

在平台提供的认证鉴权方式中,HMAC 签名认证和参数签名认证均可在识别出调用方的同时,对请求参数、Body 进行签名检查,从而进一步确保请求未被篡改或伪造。

HMAC 签名认证(推荐)

该算法主要依据 HTTP 签名草案 建立,使用 Kong 原生的 HMAC-Auth 插件

Authorization 请求头构成

标准的 Authorization 请求头示例如下:

  1. Authorization: hmac appkey="wsK8t77fvAAs3i7878NSkC0j95ib3oVu", algorithm="hmac-sha256", headers="date request-line", signature="gaweQbATuaGmLrUr3HE0DzU1keWGCt3H96M28sSHTG8="
  • HMAC

    表明使用 HMAC 签名,此为静态字段,在所有请求中均为一致,无需变化。

  • appkey=”wsK8t77fvAAs3i7878NSkC0j95ib3oVu”

    即凭证中的 App Key 字段(如下图所示),需与 HMAC 以 ASCII 空格 分隔。

    API 开放鉴权 - 图6

  • algorithm=”hmac-sha256”

    表示使用的签名算法,无需变化,需与 appkey 以 ASCII 字符 , 和 ASCII 空格 分隔。

  • headers=”date request-line”

    参与签名的请求头,均为小写,其内容是有序的,表明签名过程中字段拼接的顺序(具体请参见 签名算法),需与 algorithm 以 ASCII 字符 , 和 ASCII 空格 分隔。

    ::: tip 提示 字段 request-line 相对特殊,表示请求行,例如 GET /api?name=bob HTTP/1.1,此处虽写在 headers 中,实质上并不是 header。 :::

  • signature=”gaweQbATuaGmLrUr3HE0DzU1keWGCt3H96M28sSHTG8=”

    基于签名算法生成的签名值,需与 headers 以 ASCII 字符 , 和 ASCII 空格 分隔。

签名算法

  1. 不存在请求 Body
  • 必须请求头

    • Date
    • Authorization
  • Date 请求头

    Date 请求头需遵循 RFC1123 HTTP 规范,例如 Thu, 10 Dec 2020 08:47:43 GMT

    • Unix 命令生成:

      1. env LANG=eng TZ=GMT date '+%a, %d %b %Y %T %Z'
    • Java 代码生成:

      1. System.out.println(DateTimeFormatter.RFC_1123_DATE_TIME.format(ZonedDateTime.now(ZoneOffset.UTC)));

    ::: tip 提示 若 Date 请求头时间与服务器时间的绝对差值大于 5 分钟,将被认为请求重放而拒绝请求。 :::

  • Authorization 请求头

    请求头的构造,请参见 Authorization 请求头构成。此处将介绍如何生成签名。

    待签名字符串的生成规则如下:

    1. 若非 request-line,拼接小写的请求头 key,并跟上 ASCII 字符 : 和 ASCII 空格
    2. 若非 request-line,拼接请求头 value,若是 request-line,拼接 HTTP request line。
    3. 若非 request-line,最后拼接 ASCII 换行符 \n

    例如,对于以下请求:

    1. curl -i -X GET http://localhost/requests?name=bob \
    2. -H 'Host: hmac.com' \
    3. -H 'Date: Thu, 22 Jun 2017 21:12:36 GMT' \
    4. -H 'Authorization: hmac appkey="wsK8t77fvAAs3i7878NSkC0j95ib3oVu", algorithm="hmac-sha256", headers="date host request-line", signature="FiPTWoayUGvlaAk6HbnxEzlXo0JO2HhiDGEwsR4yKPo="'

    Authorization 头中指定了用于签名的请求头,datehost 以及特殊的请求行 request-line,按序拼接字符串,获得待签名字符串:

    1. date: Thu, 22 Jun 2017 21:12:36 GMT
    2. host: hmac.com
    3. GET /requests?name=bob HTTP/1.1

    对待签名字符串进行签名,规则如下:

    1. signed_string=HMAC-SHA256(<signing_string>, "secret")
    2. signature=base64(<signed_string>)

    若 App Secret 为 qdWre3pJxitNm9NOBRH3EpWeVYepnt3f,可得到签名值为 FiPTWoayUGvlaAk6HbnxEzlXo0JO2HhiDGEwsR4yKPo=

    使用 Unix 命令生成签名:

    1. echo -ne "date: Thu, 22 Jun 2017 21:12:36 GMT\nhost: hmac.com\nGET /requests?name=bob HTTP/1.1" | \
    2. openssl dgst -sha256 -hmac "qdWre3pJxitNm9NOBRH3EpWeVYepnt3f" -binary | base64

    使用 Java 代码生成签名:

    1. import org.apache.commons.codec.binary.Base64;
    2. import org.apache.commons.codec.digest.HmacAlgorithms;
    3. import org.apache.commons.codec.digest.HmacUtils;
    4. // ...
    5. String digest =
    6. new String(
    7. Base64.encodeBase64String(
    8. new HmacUtils(HmacAlgorithms.HMAC_SHA_256, "qdWre3pJxitNm9NOBRH3EpWeVYepnt3f")
    9. .hmac("date: Thu, 22 Jun 2017 21:12:36 GMT\nhost: hmac.com\nGET /requests?name=bob HTTP/1.1")));
  1. 存在请求 Body
  • 必须请求头

    • Date
    • Digest
    • Authorization
  • Date 请求头

    与不存在 Body 时一致。

  • Digest 请求头

    需使用 SHA-256 对请求 Body 进行签名,例如 Body 为 {"name": "bob"},则对应的 Digest 请求头为 Digest: SHA-256=956ba28434677d7d825157df180ef8123067cd58277c73f2c0f5e461a2830b52,其中 Digest 请求头的 value 需以 SHA-256= 开头。

    使用 Unix 命令生成:

    1. echo -n '{"name": "bob"}' | openssl dgst -sha256

    请求限制:请求 Body 大小不超过 10 m。

  • Authorization 请求头

    区别于不存在 Body 的情况,Headers 部分必须带上 Digest,示例如下:

    1. curl -i -X POST http://localhost/requests \
    2. -H 'Host: hmac.com' \
    3. -H 'Date: Thu, 22 Jun 2017 21:12:36 GMT' \
    4. -H 'Digest: SHA-256=956ba28434677d7d825157df180ef8123067cd58277c73f2c0f5e461a2830b52' \
    5. -H 'Authorization: hmac appkey="wsK8t77fvAAs3i7878NSkC0j95ib3oVu", algorithm="hmac-sha256", headers="date host request-line digest", signature="CZSUv+kxWHN/vPEbwARg4r+NN3Vnb9+Aaq5XOQiENJA="'
    6. -d '{"name": "bob"}'

    Authorization 头中指定了用于签名的请求头,datehost 、特殊的请求行 request-line 以及请求 Body 的签名值 digest,按序拼接字符串,获得待签名字符串:

    1. date: Thu, 22 Jun 2017 21:12:36 GMT
    2. host: hmac.com
    3. GET /requests?name=bob HTTP/1.1
    4. digest: SHA-256=956ba28434677d7d825157df180ef8123067cd58277c73f2c0f5e461a2830b52

    生成签名的方式请参见 不存在请求 Body

参数签名认证

所有参数(包括 appKey,但不包括签名参数 sign 自身)均按照字符序增序排列,随后在排列的参数串末尾加上 appSecret,对完整字符串进行 SHA512 签名,生成签名参数 sign

基于 URL 参数的签名

例如,调用参数为:

  1. /api?appKey=foobar&name=dadu&abc=123

参数名按照字母序升序排列,得到:

  1. abc=123&appKey=foobar&name=dadu

假设调用凭证中的 App Secret 为 my.secret,并将其附加到参数末尾,得到:

  1. abc=123&appKey=foobar&name=dadumy.secret

计算该字符串的 SHA512,得到签名值为:

  1. f97efc239eef4eafe69bfe41438740199d939e2e123c4c5a6b5d0b5e58d295a2818d6444c5c7b9e5985e751ad93f9c854e1966e59a63a1eeceb31e46641e291a

最终得到请求为:

  1. /api?appKey=foobar&name=dadu&abc=123&sign=f97efc239eef4eafe69bfe41438740199d939e2e123c4c5a6b5d0b5e58d295a2818d6444c5c7b9e5985e751ad93f9c854e1966e59a63a1eeceb31e46641e291a

基于 Body 的签名

对于 Post 等带有 Body 的请求,将对 Body 进行签名,此时分为两种情况:

  1. Content-Type 为 application/x-www-form-urlencoded

    此时与 URL 参数签名的方式一致,只是将参数放至 Body 里面。

    请求限制:

    • Body 大小不超过 10 m。
    • 参数个数不大于 100 个。
  2. Content-Type 为 application/json

    例如,原始请求为:

    1. POST --header 'Content-Type: application/json'
    2. -d '
    3. {"userName":"abc","gender":"male"}
    4. '

    将 Body 整体作为名为 Data 的参数,按照字母升序排列,得到:

    1. appKey=foobar&data={"userName":"abc","gender":"male"}

    假设调用凭证中的 App Secret 为 my.secret,并将其附加到参数末尾,得到:

    1. appKey=foobar&data={"userName":"abc","gender":"male"}my.secret

    计算该字符串的 SHA512,得到签名值为:

    1. ec23eeda5f88abe26311ed020439172eea409e3475875c87e9abfa8a6856138e767608e8497435f573ccb417a90448c78abdca4a0de12c4da4583aa3add7bf52

    最终调用方需发起的请求为:

    1. POST --header 'Content-Type: application/json'
    2. -d '
    3. {
    4. "data": "{\"userName\":\"abc\",\"gender\":\"male\"}",
    5. "appKey": "foobar",
    6. "sign": "ec23eeda5f88abe26311ed020439172eea409e3475875c87e9abfa8a6856138e767608e8497435f573ccb417a90448c78abdca4a0de12c4da4583aa3add7bf52"
    7. }'

    网关收到请求后,发至后端服务的真正请求和原始请求一致:

    1. POST --header 'Content-Type: application/json'
    2. -d '
    3. {"userName":"abc","gender":"male"}
    4. '

    请求限制:Body 大小不超过 2 m。

加上时间戳的签名(可选)

增加名为 apiTimestamp 的时间戳参数,同其他参数一起字符升序排列之后,进行签名生成 sign

  • 时间戳取值

    Unix 时间戳标准:从 1970 年 1 月 1 日(UTC/GMT 的午夜 )开始所经过的秒数

    不同语言的获取方式:

    | Java | System.currentTimeMillis() / 1000 | | :————- | :—————————————————— | | JavaScript | Math.round(new Date().getTime()/1000) |

  • 时间校验

    添加 apiTimestamp 时间戳参数后,网关将判断是否和服务端时间接近,允许正负误差在 5 分钟内,否则鉴权失败。

  • Example

    以 URL 参数的签名为例:

    1. /api?appKey=foobar&name=dadu&abc=123

    加上参数 apiTimestamp=1581565619,进行字符升序排列,并加上 App Secret(假设为 my.secret):

    1. abc=123&apiTimestamp=1581565619&appKey=foobar&name=dadumy.secret

    计算该字符串的 SHA512,得到签名值为:

    1. 61cabbc719e5edff3021ab5047bd3c5981e6348066d0416254dd529241a7135d57498dac56d2400139bc1040c5759d1c0798f1673913c537d10769c149879edd

    最终得到请求为:

    1. /api?appKey=foobar&name=dadu&abc=123&apiTimestamp=1581565619&sign=61cabbc719e5edff3021ab5047bd3c5981e6348066d0416254dd529241a7135d57498dac56d2400139bc1040c5759d1c0798f1673913c537d10769c149879edd

    基于 Body 签名的方式类似,apiTimestamp 需带在 Body 的 Json 结构体中,例如:

    1. POST --header 'Content-Type: application/json'
    2. -d '
    3. {
    4. "data": "{\"userName\":\"abc\",\"gender\":\"male\"}",
    5. "appKey": "foobar",
    6. "apiTimestamp": 1581565619,
    7. "sign": "xxxx",
    8. }'

代码示例

签名实现:

  1. // SignAuthHelper.Java
  2. import org.apache.commons.codec.binary.Hex;
  3. import org.apache.commons.lang.StringUtils;
  4. import java.io.UnsupportedEncodingException;
  5. import java.security.NoSuchAlgorithmException;
  6. import java.util.ArrayList;
  7. import java.util.List;
  8. import java.util.Map;
  9. import java.security.MessageDigest;
  10. import java.util.stream.Collectors;
  11. /**
  12. * SignAuthHelper.java (JAVA 8+)
  13. */
  14. public class SignAuthHelper {
  15. public static Map<String, String> sign(Map<String,String> args, String appSecret) {
  16. args.put("appKey", args.get("appKey"));
  17. List<String> keyList = args.entrySet().stream().map(Map.Entry::getKey).sorted().collect(Collectors.toList());
  18. List<String> kvList = new ArrayList<>();
  19. for (String key: keyList) {
  20. kvList.add(key + "=" + args.get(key));
  21. }
  22. String argsStr = StringUtils.join(kvList, "&") + appSecret;
  23. String sign = string2SHA512(argsStr);
  24. args.put("sign", sign);
  25. return args;
  26. }
  27. private static String string2SHA512(String str) {
  28. MessageDigest messageDigest;
  29. String encdeStr = "";
  30. try {
  31. messageDigest = MessageDigest.getInstance("SHA-512");
  32. byte[] hash = messageDigest.digest(str.getBytes("UTF-8"));
  33. encdeStr = Hex.encodeHexString(hash);
  34. } catch (NoSuchAlgorithmException | UnsupportedEncodingException e) {
  35. e.printStackTrace();
  36. }
  37. return encdeStr;
  38. }
  39. }

测试程序:

  1. import java.util.HashMap;
  2. import java.util.Map;
  3. import static org.junit.Assert.assertEquals;
  4. /**
  5. * Main.java (JAVA 8+)
  6. */
  7. public class Main {
  8. public static void main(String[] args) {
  9. // Sign Request URL Parameter
  10. // GET https://domain.com?param1=123&param2=Abc&appKey=foobar&pampasCall=query.coupon
  11. Map<String, String> params = new HashMap<>(2);
  12. params.put("param1", "123");
  13. params.put("param2", "Abc");
  14. params.put("appKey", "foobar");
  15. params.put("pampasCall", "query.coupon");
  16. params = SignAuthHelper.sign(params, "my.secret");
  17. String expect = "d6fee3145be668425f70878084f9d"
  18. + "39fce3f7c5fca283ffc4c5d5a5568077334e9a505"
  19. + "26e7e806758a66b7647ae9951f9324a0f921e28417e07d69beed79f7ef";
  20. assertEquals(expect, params.get("sign"));
  21. System.out.println("Verify Success");
  22. // Sign Request Body
  23. // POST --header 'Content-Type: application/json'
  24. // --header 'Accept: application/json'
  25. // -d '{"userName":"abc","gender":"male"}'
  26. // 'https://domain.com'
  27. params = new HashMap<>(4);
  28. // request body use data as param name
  29. params.put("data", "{\"userName\":\"abc\",\"gender\":\"male\"}");
  30. params.put("appKey", "foobar");
  31. expect = "ec23eeda5f88abe26311ed020439172eea409e34"
  32. + "75875c87e9abfa8a6856138e767608e8497435f573c"
  33. + "cb417a90448c78abdca4a0de12c4da4583aa3add7bf52";
  34. params = SignAuthHelper.sign(params, "test-secret");
  35. assertEquals(expect, params.get("sign"));
  36. System.out.println("Verify Request Body Success");
  37. }
  38. }