# 浅谈开放平台之接口鉴权设计

## 开放平台漫谈

当一个系统的外部接入方变得越来越多，业务越来越复杂，帮助接入方排查问题耗费的时间越来越多，就有必要构建一套自助接入的系统。再进一步，就会演变成公司战略意义上开放平台。其实通俗的说，常规企业的开放平台就是提供一些接口，使得合作伙伴或个人能通过这些接口获得企业的服务、能力、数据。

<!-- more -->

### 意义

- 减轻外部接入时的人员消耗
- 规范快捷的接入方式
- 减轻开发不必要的工作量
- 有利于企业构建相应的生态体系
- 方便对接口进行管控

### 基本结构

开放平台的目的就是把企业的能力提供出去，不管是数据还是服务，都是需要通过接口来进行交互，归根结底是围绕API来进行平台搭建。对于API我们需要进行定义、接入、鉴权、管理等相关的操作，可以大体上分为如下功能模块：

![开放平台结构](https://fliaping-blog.oss-rg-china-mainland.aliyuncs.com/storage/20181212/2018-12-12-20-57-54.png)

#### 接口网关

1. 平台服务接口：提供标准的接口定义
2. 接口管理：管理接口上线下线，版本管理，限流降级等
3. 外部消息通知：耗时比较久的接口或者其它外部关心的事件，需要进行异步通知

#### 鉴权服务

1. 应用鉴权：第三方应用是否有权限调用平台
2. 接口调用鉴权：第三方应用是否有权限以用户身份调用接口
3. 用户鉴权：开发者或者商户登录管理系统时的身份认证
4. 用户授权管理：用户授权给第三方应用，使其可以以用户身份调用接口

#### 平台门户

1. 用户入驻：企业/商户注册入驻，资质审核
2. 开发者入驻：开发者注册，身份认证
3. 管理运维：用户可以管理修改个人资源，开发者可以配置应用
4. 帮助中心：开发者需要的开发文档以及用户的帮助文档

#### 运营系统

1. 用户管理：运营人员对用户进行管理，例如拉黑、注销、审核等
2. 开发者管理：运营人员对开发者及其应用进行管理
3. 权限管理：运营人员对用户或者开发者的服务使用请求进行审核
4. 统计分析：对接口调用情况、第三方应用健康度等信息进行统计分析

## 鉴权

下面将对鉴权进行展开讨论。在开放平台的鉴权服务中主要涉及三个实体：开发者、用户和开放平台(企业)，他们之间的关系如下图所示：

![2018-12-13-14-03-38](https://fliaping-blog.oss-rg-china-mainland.aliyuncs.com/storage/20181213/2018-12-13-14-03-38.png)

- 企业的能力通过开放平台接口的形式对外提供
  - 例如淘宝提供的是类似于市集的能力，商家可以把东西放到市集上，顾客到市集上买东西，提供的是卖家与卖家建立联系的服务。
  - 例如物流公司提供的是货物运转的能力，寄件人寄出物品，收件人签收物品，提供的是物品流通的服务
  - 当商家或寄件人是普通的个体，企业可以直接提供直接面向人的UI服务，但当出现面向其他企业的服务时，一套统一的系统往往不能满足所有企业的需要，并且随着业务复杂度的增加或者需求的快速变更，这套统一的系统也会不堪重负。这时需要将属于本企业的部分研发工作转嫁出去，通过开放平台的接口，将服务模块化，第三方企业可以根据自己的需要对模块进行组合，实现自己的需求。
- 开发者对开放平台的接口进行封装组成功能，提供给用户使用
  - 第三方企业的开发者进行研发工作，将开放平台的接口封装成本公司需要的功能。
  - 但是不是所有的第三方企业都有研发能力，可以将研发工作交给其他软件公司来做。
- 用户寄存资源在开放平台
  - 用户（第三方企业），如果使用了本公司的某些服务，就会产生数据、充值等，这些都是用户的资源。
  - 用户（第三方企业）不必自己根据接口开发功能，可以授权其它软件公司开发的软件来获取服务

这三者的交互简单的来讲，用户要使用平台的服务，有开发人员开发了适配该平台的应用给用户用，但是平台需要认证用户的身份，用户把自己身份的凭证给开发者的应用①，然后愉快地用这个应用享受平台的服务②。

用户的身份凭证一般是用户名密码，但总不能每次操作都要输入用户名密码吧，所以一般情况下用户只用拿用户名密码做一个登录操作，服务器会给客户端一个sessionId，后续的操作都拿这个sessionId来标记用户身份。那么在这个场景中我们肯定不能把用户名密码给到开发者，同样，可以拿一个临时的sessionId给开发者，来作为身份凭证。这样的方法是能解决问题的，但是有局限性，当需要在凭证中加入其它属性，例如授权范围，过期时间，过期续约等，就需要一个更复杂的流程，于是就出现了Oauth2这样能满足上述诉求的授权协议。

### 基于Oauth2的改造

这里不对Oauth2进行详细解释，别人已经有很好的文章了：[理解OAuth 2.0 - 阮一峰](http://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html)

Oauth2有四种模式：

- 授权码模式（authorization code）
- 简化模式（implicit）
- 密码模式（resource owner password credentials）
- 客户端模式（client credentials）

出于安全的考虑，开放平台一般只使用授权码模式，我们接下来也是基于这种模式进行阐述。

> ![授权码模式](https://fliaping-blog.oss-rg-china-mainland.aliyuncs.com/storage/201911/2019-01-01-17-19-05.png)  
> （A）用户访问客户端，后者将前者导向认证服务器。  
> （B）用户选择是否给予客户端授权。  
> （C）假设用户给予授权，认证服务器将用户导向客户端事先指定的"重定向URI"（redirection URI），同时附上一个授权码。  
> （D）客户端收到授权码，附上早先的"重定向URI"，向认证服务器申请令牌。这一步是在客户端的后台的服务器上完成的，对用户不可见。  
> （E）认证服务器核对了授权码和重定向URI，确认无误后，向客户端发送访问令牌（access token）和更新令牌（refresh token）。

上面的引用，描述了标准的Oauth授权码模式, 下面的时序图对该过程进行略微的调整，以适应开放平台：

![基于OAuth2的接口鉴权](https://fliaping-blog.oss-rg-china-mainland.aliyuncs.com/storage/20181212/2018-12-12-21-02-12.png)

1. 不同于标准流程，初始动作不一定是用户发起，也可以是应用开发者发起，这样的目的是更加便捷，如果应用开发者和用户属于同一个组织，由开发者发起授权后以通知的形式告知用户做后续操作，能够减少用户操作的复杂性。
2. 通过不同的方式发起授权后，有一个必要的步骤就是需要用户选择授权范围，这样能够给用户更多的自由度，提高用户资源的安全性。（同标准流程步骤B）
3. 开发平台的认证服务在颁发token时，标准流程是通过转跳，携带code到第三方应用，然后由第三方应用去拉取token。不过开放平台的场景中，第三方应用往往已经配置了回调地址，所以可以将步骤简化，直接把token信息推送给第三方应用。
4. refreshToken的操作在图中没有展示，和标准流程基本一致，不过需要注意的一点是，token刷新之后原token如果没有过期，不应该立即失效，要有一个缓冲期。因为第三方应用可能有缓存机制，刷新的token还没有更新到缓存，依然在使用老token请求。正常来讲缓冲期2-3小时足够。

#### Token管理

开发平台还应该支持用户对token进行查看、撤销的操作，同时开发者也可以查看，包括授权范围，时效时间等信息，但是**不应该透传token key**到页面上，为的是避免中间人攻击，可以用id或其它和验证无关的标示代替。

### JWT的问题

上文中第三方应用拿token去访问资源时，资源服务器需要对token进行校验，通常这个任务是交给认证服务的，但是这里会多一次远程调用，并且无疑会增加认证服务负载，并且增大接口耗时。所以JWT提供了一种不需要远程调用的认证方式，其实本质是对token进行签名，所有的资源服务器同认证服务器使用同一种签名方法和同一个签名key，这样资源服务器可以直接校验token的合法性。但是不能提供撤销token的功能，JWT相当于是令牌，发放之后在有效期内都是有用的。

要在JWT上增加撤销token的功能也是能实现的，让认证服务存储撤销了的token，每次去校验下这个token是不是失效了。嗯，看起来很好，不过是不是哪个地方不对？这跟直接去认证服务器校验token有何区别？不过这在撤销token比例是少数的情况下可能一定程度减轻认证服务的负载。

### 如何实现

Java生态中比较常用的Oauth的框架有Spring Security和Shiro，前者基于Spring的生态，不能单独使用，并且很复杂，但是得益于Spring的整体生态的强大，可以很容易实现细粒度权限的管理，以及和其它spring工具的整合。后者能够非常清晰的处理认证、授权、管理会话以及密码加密。很容易入手，上手快控制粒度可糙可细。自由度高，既能配合Spring使用也可以单独使用。看上去Shiro更加灵活方便，不过为了方便使用spring的其它功能，少写点代码，最后选择了spring security。

#### 应用鉴权

对于开发者而言可以根据平台开放的接口组合出很多不同功能的应用，应用通过`appKey`来标示，通过`appSecret`作为密码验证，使用`accessToken`做为用户身份的令牌。`appKey+appSecret`来校验应用是合法的，用`accessToken`来校验应用获得了用户的授权。应用鉴权比较简单，通常是将`appKey`作为一个参数，用`appSecret`来对请求内容签名，为了防止重放攻击往往还需要携带上时间戳，开放平台收到请求后验证时间戳，然后找到该`appKey`对应的`appSecret`，按照和请求方同样的签名方式算出签名和请求的签名对比即可验证。

究竟具体的签名协议是什么样，就看各自的喜好了，各个大厂的签名规则也不尽相同，但主要有以下几点

- 参数：可分为两类，系统参数和业务参数。系统参数固定好一般就不会变化，主要包括`appKey`，`sign`,`timestamp`,`api`，对于业务参数，不同的api业务参数都是不一样的，这是可以灵活变化的部分。
- 签名规则: 参数中的`sign`字段是请求签名值，签名中有得玩的就是签名算法，要在安全性和性能之间找到平衡，通常安全性越高的算法需要消耗更多的cpu资源（例如SHA-256和SHA-512），因为为了防止hash碰撞的攻击需要加长签名值，从而需要更多步的运算。由于量子计算机的发展还在初期，仅仅能运算一些特定的量子算法，所以不用过于担心签名会被伪造，所以笔者认为如果不是金融级别的接口，MD5足矣。要想安全点SHA-256就够了，同时也不会消耗过多性能。
- http与参数：开放平台接口在restful盛行的今天，传输协议必然是http（为了不泄密，https是必须的），至于参数怎么传，组合方式很多，业务参数肯定是在body中，系统参数可以放query、header或body中都可以，看使用习惯和个人喜好了。

#### AccessToken的设计

AccessToken比较复杂一点，因为涉及到用户授权，授权范围、授权有效期等，不过Oauth2协议已经为我们铺了一段路。在标准流程中，第三方应用去认证服务拿token时返回的内容有以下字段：

- access_token：表示访问令牌，必选项。
- token_type：表示令牌类型，该值大小写不敏感，必选项，可以是bearer类型或mac类型。
- expires_in：表示过期时间，单位为秒。如果省略该参数，必须其他方式设置过期时间。
- refresh_token：表示更新令牌，用来获取下一次的访问令牌，可选项
- scope：表示权限范围，如果与客户端申请的范围一致，此项可省略。

上面的字段就已经可以满足需求，scope字段我们可以细化一下。因为我们是以接口为粒度进行权限的管理，scope中就应该表示的是针对本次授权应用可以请求的接口。该字段设计为字符数组，字符用来标示接口的pattern。

接口名的设计示例： `business.group.entity.action`

类似于URI，通过定位符的方式来对接口进行管理，很容易对接口进行分组管理。同样也可以使用URI常用的AntMatch来避免scope中有一大串的api，例如scope中可以用 `express.wireless.*` 来标示`快递业务`的`无线组`下的所有接口。

#### 接口API的管理

应用需要用到哪些接口，开发者是最清楚的，在一个应用上线之前需要在开放平台选好需要哪些接口的权限，用户确认授权可以根据应用的需要，选择性的给予授权，以保证用户资源的安全。不过由于接口的粒度过于细，普通用户不可能也不会了解每个接口的含义，用户要的是某个功能，那么在用户确认授权时就不要把接口直接展现给用户，可以预先把接口打包成一个个完整的功能让用户确认即可。

## 参考文章

- [搭建基于OAuth2和SSO的开放平台](https://blog.csdn.net/janwen2010/article/details/77892075)
- [理解OAuth 2.0](http://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html)
- [构建开放能力平台架构打造API生态圈](https://www-01.ibm.com/common/ssi/cgi-bin/ssialias?htmlfid=ESR12346CNZH)
- [如何设计一个开放平台openapi](https://www.jianshu.com/p/2177cabcaad6)