概述
前面我们讲了授权服务的流程,这里的关键就是授权服务将 OAuth 2.0 的复杂性都揽在了自己身上,这也是授权服务为什么是 OAuth 2.0 体系的核心的原因之一。
虽然授权服务做了大部分工作,但是在 OAuth 2.0 的体系里面,除了资源拥有者是作为用户参与,还有另外两个系统角色,也就是第三方软件和受保护资源服务。那么今天我们就站在这两个角色的角度,看看它们应该做哪些工作,才能接入到 OAuth 2.0 的体系里面。
第三方软件应用
构建第三方软件应用我们先来思考一下:如果要基于淘宝开放平台构建一个小石榴打单软件的应用,小石榴软件的研发人员应该做哪些工作?
是不是要到淘宝开放平台申请注册为开发者,在成为开发者以后再创建一个应用,之后就可以开始开发了,对吧?没错,一定是这样的流程。那么,开发第三方软件应用的过程中,我们需要重点关注哪些内容呢?
先来和你总结下,这些内容包括 4 部分,分别是:注册信息、引导授权、使用访问令牌、使用刷新令牌。
第一点,注册信息。
首先,小石榴软件只有先有了身份,才可以参与到 OAuth 2.0 的流程中去。也就是说,小石榴软件需要先拥有自己的 app_id 和 app_serect 等信息,同时还要填写自己的回调地址 redirect_uri、申请权限等信息。
这种方式的注册我们称它为静态注册,也就是小石榴软件的研发人员提前登录到淘宝开放平台进行手动注册,以便后续使用这些注册的相关信息来请求访问令牌。
第二点,引导授权。
当用户需要使用第三方软件,来操作其在受保护资源上的数据,就需要第三方软件来引导授权。比如,你要使用小石榴打单软件来对店铺里面的订单发货打印,那你首先访问的一定是小石榴软件,不会是授权服务,更不会是受保护资源服务。
但是小石榴软件需要你的授权,只有授权服务才能允许你这样做。所以小石榴软件需要配合你做的第一件事儿,就是将你引导至授权服务,如下面代码所示。其实就是让用户为第三方软件授权,得到了授权之后,第三方软件才可以代表用户去访问数据。也就是说,小石榴打单软件获得授权之后,才能够代表你处理其在淘宝店铺上的订单数据。
oauthUrl := "http://localhost:8081/OauthServlet-ch03?reqType=oauth"
response.sendRedirect(toOauthUrl)
第三点,使用访问令牌。
拿到令牌后去使用令牌,才是第三方软件的最终目的。然后我们看看如何使用令牌。目前 OAuth 2.0 的令牌只支持一种类型,那就是 bearer 令牌,即可以是任意字符串格式的令牌。
官方规范给出的使用访问令牌请求的方式,有三种,分别是:
- Form-Encoded Body Parameter(表单参数)
POST /resource HTTP/1.1
Host: server.example.com
Content-Type: application/x-www-form-urlencoded
access_token=b1a64d5c-5e0c-4a70-9711-7af6568a61fb
- URI Query Parameter(URI 查询参数)
GET /resource?access_token=b1a64d5c-5e0c-4a70-9711-7af6568a61fb HTTP/1.1
Host: server.example.com
- Authorization Request Header Field(授权请求头部字段)
GET /resource HTTP/1.1
Host: server.example.com
Authorization: Bearer b1a64d5c-5e0c-4a70-9711-7af6568a61fb
根据 OAuth 2.0 的官方建议,系统在接入 OAuth 2.0 之前信息传递的请求载体是 JSON 格式的,那么如果继续采用表单参数提交的方式,令牌就无法加入进去了,因为格式不符。
如果这时采用参数传递的方式呢,整个 URI 会被整体复制,安全性是最差的。而请求头部字段的方式就没有上述的这些“烦恼”,因此官方的建议是采用 Authorization 的方式来传递令牌。
第四点,使用刷新令牌。
之前讲授权服务的时候提到过,如果访问令牌过期了,小石榴软件总不能立马提示并让你重新授权一次,否则你的体验将会非常不好。为了解决这个问题就用到了刷新令牌。
使用刷新令牌的方式跟使用访问令牌是一样的,关于刷新令牌的使用,你最需要关心的是,什么时候你会来决定使用刷新令牌。
在小石榴打单软件收到访问令牌的同时,也会收到访问令牌的过期时间 expires_in。一个设计良好的第三方应用,应该将 expires_in 值保存下来并定时检测;如果发现 expires_in 即将过期,则需要利用 refresh_token 去重新请求授权服务,以便获取新的、有效的访问令牌。这种定时检测的方法可以提前发现访问令牌是否即将过期。
此外,还有一种方法是“现场”发现。比如小石榴软件访问你店铺订单的时候,突然收到一个访问令牌失效的响应,此时小石榴软件立即使用 refresh_token 来请求一个访问令牌,以便继续代表你使用他的数据。
综合来看的话,定时检测的方式,需要我们额外开发一个定时任务;而“现场”发现,就没有这种额外的工作量。具体采用哪一种方式,你可以结合自己的实际情况。不过我还是建议你采用定时检测这种方式,因为它可以带来“提前量”,以便让我们有更好的主动性,而现场发现就有点被动了。
说到这里,要再次提醒你注意的是,刷新令牌是一次性的,使用之后就会失效,但是它的有效期会比访问令牌要长。这个时候我们可能会想到,如果刷新令牌也过期了怎么办?在这种情况下,我们就需要将刷新令牌和访问令牌都放弃,相当于回到了系统的初始状态,只能让用户小明重新授权了。
构建受保护资源服务
实际上在整个开放授权的环境中,受保护资源最终指的还是 Web API,比如说,访问头像的 API、访问昵称的 API。对应到我们的打单软件中,受保护资源就是订单查询 API、批量查询 API 等。
在互联网上的系统之间的通信,基本都是以 Web API 为载体的形式进行。因此呢,当我们说受保护资源被授权服务保护着时,实际上说的是授权服务最终保护的是这些 Web API。
我们在构建受保护资源服务的时候,除了基本的要检查令牌的合法性,还需要做些什么呢?我认为最重要的就是权限范围了。在我们处理受保护资源服务中的逻辑的时候,校验权限的处理会占据很大的比重。访问令牌递过来,你肯定要多看看令牌到底能操作哪些功能、又能访问哪些数据。
我们把这些权限的类别总结归纳下来,最常见的大概有以下几类。
1、不同的权限对应不同的操作。
这里的操作,其实对应的是 Web API,比如目前淘宝开放平台提供有查询商品 API、新增商品 API、删除商品 API 这三种。如果小石榴软件请求过来的一个访问令牌 access_token 的 scope 权限范围只对应了查询商品 API、新增商品 API,那么包含这个 access_token 值的请求,就不能执行删除商品 API 的操作。
//不同的权限对应不同的操作
scope := OauthServlet.tokenScopeMap.get(accessToken)
var sbuf strings.Builder
for(i := 0;i < scope.length; i++){
sbuf.WriteString(scope[i])
sbuf.WriteString("|")
}
if strings.Contains(sbuf.String(), "query") {
queryGoods("")
}
if strings.Contains(sbuf.String(), "add") {
addGoods("")
}
if strings.Contains(sbuf.String(), "del") {
delGoods("")
}
2、不同的权限对应不同的数据。
这里的数据,就是指某一个 API 里包含的属性字段信息。比如,有一个查询你的信息的 API,返回的信息包括 Contact(email、phone、qq)、Like(Basketball、Swimming)、Personal Data(sex、age、nickname)。
如果小石榴软件请求过来的一个访问令牌 access_token 的 scope 权限范围只对应了 Personal Data,那么包含该 access_token 值的请求就不能获取到 Contact 和 Like 的信息,关于这部分的代码,实际跟不同权限对应不同操作的代码类似。
看到这里,你就明白了,这种权限范围的粒度要比“不同的权限对应不同的操作”的粒度要小。这正是遵循了最小权限范围原则。
3、不同的用户对应不同的数据。
这种权限实际上只是换了一种维度,将其定位到了用户上面。一些基础类信息,比如获取地理位置、获取天气预报等,不会带有用户归属属性,也就是说这些信息并不归属于某个用户,是一类公有信息。对于这样的信息,平台提供出去的 API 接口都是“中性”的,没有用户属性。
但是,更多的场景却是基于用户属性的。还是以小石榴打单软件为例,商家每次打印物流面单的时候,小石榴打单软件都要知道是哪个商家的订单。这种情况下,商家为小石榴软件授权,小石榴软件获取的 access_token 实际上就包含了商家这个用户属性。
淘宝开放平台的受保护资源服务每次接收到小石榴软件的请求时,都会根据该请求中 access_token 的值找到对应的商家 ID,继而根据商家 ID 查询到商家的订单信息,也就是不同的商家对应不同的订单数据。
在上面讲三种权限的时候,我举的例子实际上都属于一个系统提供了查询、添加、删除这样的所有服务。此时你可能会想到,现在的系统不已经是分布式系统环境了么,如果有很多个受保护资源服务,比如提供用户信息查询的用户资源服务、提供商品查询的商品资源服务、提供订单查询的订单资源服务,那么每个受保护资源服务岂不是都要把上述的权限范围校验执行一遍吗,这样不就会有大量的重复工作产生么?
为了应对这种情况,我们应该有一个统一的网关层来处理这样的校验,所有的请求都会经过 API GATEWAY 跳转到不同的受保护资源服务。这样呢,我们就不需要在每一个受保护资源服务上都做一遍权限校验的工作了,而只需要在 API GATEWAY 这一层做权限校验就可以了。系统结构如下图所示。
总结
截止到这一篇,我们已经把 OAuth 2.0 中授权码相关的流程所涉及到的内容都讲完了。总结下来,关键是以下两点:
- 对于第三方软件,比如小石榴打单软件来讲,它的主要目的就是获取访问令牌,使用访问令牌,这当然也是整个 OAuth 2.0 的目的,就是让第三方软件来做这两件事。
- 受保护资源系统,比如小石榴软件要访问开放平台的订单数据服务,它需要注意的是权限的问题,这个权限范围主要包括,不同的权限会有不同的操作,不同的权限也会对应不同的数据,不同的用户也会对应不同的数据。