HTTP API认证授权方案

一.需求背景

在一些商业合作的场景下,合作方有自己的软件系统并且具备开发能力,需要访问我们的数据资源(比如:账号、产品、统计等),一般的技术方案是提供HTTP API给合作方调用。此时为了保证数据的安全性以及对数据访问范围的控制,就必须验证API调用方的身份,然后结合调用方的权限返回对应的资源,对于无法识别身份的调用方,服务端会进行拦截。

二.常用的API认证技术

2.1 App Secret Key + HMAC

这是一种用于给消息签名的技术,我们怕消息在传递的过程中被人修改,所以,我们需要用对消息进行一个MAC算法,得到一个摘要字串,然后,接收方得到消息后,进行同样的计算,然后比较这个MAC字符串,如果一致,则表明没有被修改过(整个过程参看下图)。而HMAC – Hash-based Authenticsation Code,指的是利用Hash技术完成这一工作,比如:SHA-256算法。

以SHA-256算法示例,签名流程:

  1. 发送方以 Key 作为算法的签名,对消息 Message 进行一个MAC算法,得到一个摘要字串 MAC
  2. 接收方 接收消息 Message 后进行同样的计算得到一个摘要字串 MAC
  3. 接收方 然后比较这个 MAC 字符串是否一致,如果一致,则表明没有被修改过。

2.2 OAuth 2.0

OAuth 是一个关于授权(authorization)的开放网络标准,在全世界得到广泛应用,目前的版本是2.0版。OAuth 2.0依赖于TLS/SSL的链路加密技术(HTTPS),完全放弃了签名的方式,认证服务器再也不返回什么token secret的密钥了。

2.2.1 Authorization Code Flow

Authorization Code 是最常使用的OAuth 2.0的授权许可类型,它适用于用户给第三方应用授权访问自己信息的场景。其流程图如下:

授权流程:

  1. 当用户 Resource Owner 访问第三方应用 Client 的时候,第三方应用会把用户带到认证服务器 Authorization Server 上去。
  2. Authorization Server 收到这个URL请求后,其会通过 client_id 来检查 redirect_uri 和 scope 是否合法,如果合法,则弹出一个页面,让用户授权。(如果用户没有登录,则先让用户登录,登录完成后,出现授权访问页面)
  3. 当用户授权同意访问以后,Authorization Server 会跳转回 Client ,并以返回一个 Authorization Code。
  4. 接下来,Client 就可以使用 Authorization Code 获得 Access Token。
  5. 最后就是用 Access Token 请求 Resource Server 用户的资源。

2.2.2 Client Credential Flow

客户端以自己的名义,而不是以用户的名义,向"服务提供商"进行认证。在这种模式中,用户直接向客户端注册,客户端以自己的名义要求"服务提供商"提供服务,其实不存在授权问题。

授权流程:

  1. Client 用自己的 client_idclient_secretAuthorization Server 请求 Access Token。
  2. 然后 Client 使用Access Token访问 Resource Server 相关的资源。

三.业内产品调研

3.1 微信支付

  1. 微信支付采用 App Secret Key + HMAC 签名,首先介绍一下微信支付的大致原理:

    • 微信是支付系统的开发方,掌管整个支付系统,负责记账。

    • 商家想要接入微信支付收银,需要向微信支付部门申请商户号。

    • 普通用户通过微信点击商家的付款链接,进行付款。

    • 微信后台记录一笔用户和商家之间的交易流水,然后通知商家系统支付成功。

    好了,现在可以知道,交易过程其实就是商家系统和微信后台的接口互相调用,而且只需要单向的关注商家调用微信后台。

  2. JSAPI支付-开发文档,签名算法:

    假设传递的参数如下:

    appid: wxd930ea5d5a258f4f
    mch_id: 10000100
    device_info: 1000
    body: test
    nonce_str: ibuaiVcKdpRxkhJA
    

    第一步,设所有发送或者接收到的数据为集合M,将集合M内非空参数值的参数按照参数名ASCII码从小到大排序(字典序),使用URL键值对的格式(即key1=value1&key2=value2…)拼接成字符串stringA。

    stringA="appid=wxd930ea5d5a258f4f&body=test&device_info=1000&mch_id=10000100&nonce_str=ibuaiVcKdpRxkhJA";
    

    第二步,在stringA最后拼接上key得到stringSignTemp字符串,并对stringSignTemp进行MD5运算,再将得到的字符串所有字符转换为大写,得到sign值signValue。

    stringSignTemp=stringA+"&key=192006250b4c09247ec02edce69f6a2d" //注:key为商户平台设置的密钥key
    sign=MD5(stringSignTemp).toUpperCase()="9A0A8659F005D6984697E2CA0A9CF3B7" //注:MD5签名方式
    sign=hash_hmac("sha256",stringSignTemp,key).toUpperCase()="6A9AE1657590FD6257D693A078E1C3E4BB6BA4DC30B23E0EE2496E54170DACD6" //注:HMAC-SHA256签名方式,部分语言的hmac方法生成结果二进制结果,需要调对应函数转化为十六进制字符串。
    

    最终发送的数据:

    <xml>
      <appid>wxd930ea5d5a258f4f</appid>
      <mch_id>10000100</mch_id>
      <device_info>1000</device_info>
      <body>test</body>
      <nonce_str>ibuaiVcKdpRxkhJA</nonce_str>
      <sign>9A0A8659F005D6984697E2CA0A9CF3B7</sign>
    </xml>
    

3.2 微信公众号

  1. 微信公众号-获取AccessToken 开发文档

    access_token是公众号的全局唯一接口调用凭据,公众号调用各接口时都需使用access_token。开发者需要进行妥善保存。access_token的存储至少要保留512个字符空间。access_token的有效期目前为2个小时,需定时刷新,重复获取将导致上次获取的access_token失效。

  2. 接口调用请求说明

    GET https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET
    

    参数说明

    • grant_type:获取access_token填写client_credential
    • appid:第三方用户唯一凭证
    • secret:第三方用户唯一凭证密钥,即appsecret

    返回情况

    正常情况下,微信会返回下述JSON数据包给公众号:

    {"access_token":"ACCESS_TOKEN","expires_in":7200}
    

    参数说明

    • access_token:获取到的凭证
    • expires_in:凭证有效时间,单位:秒

3.3 微信网页授权

  1. 微信网页授权-开放文档

    如果用户在微信客户端中访问第三方网页,公众号可以通过微信网页授权机制,来获取用户基本信息,进而实现业务逻辑。

  2. 网页授权AccessToken的流程

    • 第一步:引导用户进入授权页面同意授权,获取code
    • 第二步:通过code换取网页授权access_token
    • 第三步:如果需要,开发者可以刷新网页授权access_token,避免过期。
    • 第四步:通过网页授权access_token和openid获取用户基本信息

四、如何选择HTTP API鉴权方案

4.1 HTTP API鉴权方式的对比

前面介绍几种常用的API鉴权技术,在产品调研环节分别可以找到其落地场景。

首先拿 微信支付 来看,一笔交易的下单在一个接口中完成,请求的参数包含金额、商户号等,都是非常关键的参数,必须要求严格校验,防止被攻击篡改,同时参数还有时效限制(含时间戳)。此时自然不适合使用OAuth 2.0的鉴权方式,AccessToken的请求不对参数进行校验。

然后看下 微信公众号 AccessToken的场景,可以看到使用AccessToken调用接口(管理公众号菜单、管理账号)都属于一个企业范围内的数据,可以这么理解,这部分信息属于微信授权给企业的一份独立资产,公众号对应的企业有权限管理这份资产。此时使用AccessToken可以很好的控制访问范围。这里不是不能用 App Secret Key + HMAC 的鉴权方式,而是觉得这部分信息安全要求没有支付高。另一方面,不对参数加密,通信也会更加高效(加密有耗时,比如文件上传也不太适合进行加密)。

最后看下 微信网页授权,同理类推,用户的信息属于每个独立的用户,获取的AccessToken的访问范围也只能是当前用户的信息。

4.2 HTTP API鉴权经验分享

上面提到的两种鉴权方式,无论是作为服务方还是调用方,我都在工作中都有使用到。个人觉得 App Secret Key + HMAC 实践起来相对容易,客户端对服务端的调用比较直接,鉴权不通过时可以通过接口的响应及时获得反馈。

另一种,OAuth 2.0的AccessToken的方式,服务端需要维护AccessToken,并且还要控制AccessToken的失效,拿微信公众号来看,新的AccessToken生成后,旧的AccessToken在5分钟之内有效;客户端需要维护一份AccessToken并及时刷新保持有效。再看下业务的交互上,比起 App Secret Key + HMAC 明显多一些环节,环节多了就容易犯错。

4.3 结论

最后,具体选择使用哪一种鉴权方式,我想还是需要结合对应的业务场景来看。比如业务发展的初期,需要快速开发推向市场,这时就没必要纠结,直接选择一种相对而言简单且不容易犯错的 App Secret Key + HMAC 签名鉴权。等到后续用户量大了,业务成熟了,可以参考 微信公众号、AWS s4签名,精细划分每一个AccessToken的访问范围。

五.实践-方案实现

实践案例使用 App Secret Key + HMAC 的鉴权方式,下面会详细介绍 客户端签名服务端验签 的过程。

5.1 分配AppId和AppSecret

在签名之前首先需要分配 AppId 和 AppSecret,落实到业务场景中,这个就是我们作为资源方分配给合作方的租户配置。关于 AppId 和 AppSecret 的生成没有标准规范,每家的生成算法都不一样,也都不会公布出来。本次案例,我们使用32位的uuid作为AppId,以64位的hash串作为AppSecret:

// 生成AppId
private static String generateAppId() {
    UUID uuid = UUID.randomUUID();
    return uuid.toString().replaceAll("-", "");
}
// 生成AppSecret
private static String generateAppSecret() {
    UUID uuid = UUID.randomUUID();
    return DigestUtils.sha256Hex(uuid.toString());
}

计算得出:

APPID = "ivv49q404zfp8075ivbcwye4ardqafha"

APP_SECRET = "ut338c829x2yzfnklvy8lezyu3ndsss68dyzo9opt3icbin7lv7p2j4b0i2cvjz8"

5.2 客户端签名

  1. 假设传递的参数如下:

    private static final String APPID = "ivv49q404zfp8075ivbcwye4ardqafha";
    
    /**
     * 下单请求对象
     */
    class PlaceOrderForm {
        String appid;
        Integer totalAmount;
        String body;
        String detail;
        String nonceStr;
    }
    
    /**
     * 模拟请求对象
     */
    private static PlaceOrderForm mockWebForm () {
        PlaceOrderForm form = new PlaceOrderForm();
        form.appid = APPID;
        form.body = "test";
        form.detail = "test";
        form.nonceStr = "123456";
        form.totalAmount = 88;
        return form;
    }
    
  2. 第一步,设所有发送或者接收到的数据为集合M,将集合M内非空参数值的参数按照参数名ASCII码从小到大排序(字典序),使用URL键值对的格式(即key1=value1&key2=value2…)拼接成字符串stringA。

    /**
     * TreeMap会根据Key排序
     */
    private static Map<String, String> confirmToMap(PlaceOrderForm form) throws Exception {
        Map<String, String> map = new TreeMap<>();
        Field[] fields = PlaceOrderForm.class.getDeclaredFields();
        for (Field field : fields) {
          field.setAccessible(true);
          Object value = field.get(form);
          if (value != null && !field.getName().equals("sign")) {
            if (value instanceof String) {
              map.put(field.getName(), (String) value);
            } else if (value instanceof Integer) {
              map.put(field.getName(), String.valueOf(value));
            }
          }
        }
        return map;
    }
    
  3. 第二步,在stringA最后拼接上appsecret得到stringSignTemp字符串,并对stringSignTemp进行MD5运算,再将得到的字符串所有字符转换为大写,得到sign值signValue。

    private static final String APP_SECRET = "ut338c829x2yzfnklvy8lezyu3ndsss68dyzo9opt3icbin7lv7p2j4b0i2cvjz8";
    
    /**
     * 生成签名
     */
    private static String sign(Map<String, String> params) {
        StringBuilder sb = new StringBuilder();
        for (Map.Entry<String, String> entry : params.entrySet()) {
          sb.append(entry.getKey());
          sb.append("=");
          sb.append(entry.getValue());
          sb.append("&");
        }
        sb.append("appsecret=");
        sb.append(APP_SECRET);
        return DigestUtils.md5Hex(sb.toString()).toUpperCase();
    }
    
  4. 最后计算得到摘要

    public static void main(String[] args) {
      PlaceOrderForm form = mockWebForm();
      try {
        Map<String, String> stringStringMap = confirmToMap(form);
        String sign = sign(stringStringMap);
        System.out.println(sign);
      } catch (Exception e) {
        e.printStackTrace();
      }
    }
    

5.3 服务端验签

服务端接收请求的参数,使用同样的签名算法计算出摘要 sign 进行比较,如果一致,则说明请求没有被修改。

六.参考资料

  1. HTTP API 认证授权术 || 酷壳 - CoolShell

  2. Signature Version 4 规范请求 - AWS General Reference

  3. go语言并发编程与Context

posted @ 2020-11-21 23:41  MarsZuo  阅读(2457)  评论(0编辑  收藏  举报