阅读 487

小程序(一)授权流程与OAuth规范

这是我参与更文挑战的第22天,活动详情查看: 更文挑战

OAuth 2.0 是一种 安全协议,今天聊一聊微信小程序的常规登录授权流程和背后的原理。

前言

登录认证是一个完整应用必备的模块,小程序和微信是一种类似应用与平台的关系,小程序属于微信公众平台,同一个平台下还有微信公众号:

在技术角度上,小程序与微信的关系比公众号更密切,因为公众号的文章本质上是一个 H5 网页,对微信底层的依赖比小程序弱;

从产品角度上,二者与微信的关系一致,都是运行在微信平台上的第三方应用。

根据微信官方文档描述,开发者获取用户登录态信息的过程正是一个授权码的许可流程:首先,开发者通过 wx.login(Object object) 方法获取到登录凭证 code 值,这一步的流程是在小程序内部通过调用微信提供的 SDK 实现;然后,再通过该 code 值换取用户的 session_key 等信息,也就是官方文档的auth.code2Session 方法,同时该方法也是被强烈建议通过开发者的后端服务来调用的。

这个过程并没有使用到浏览器,但确实按照授权码许可的思想走了一个完整的授权码许可流程。也就是说,先通过小程序前端获取到 code 值,再通过小程序的后端服务使用 code 值换取 session_key 等信息,只不过是访问令牌 access_token 的值被换成了 session_key。

既然小程序是微信平台的第三方应用,那么在接入微信登录时就要严格遵守官方的接入规范。而在互联网技术领域,对于支持第三方应用接入的平台,在登录授权上有一套标准的技术规范:OAuth 2.0。

应用场景

开始之前,你还是要先回想下生活场景,用户小明登录某电商小程序,并且想要访问自己双十一的订单,这时候第三方软件玄武授权订单数据的整个流程。我们说第三方软件先要让小明去微信商家开放平台那里给它授权数据,那这里是不是你觉得很奇怪?

你总不能说,“嘿,微信平台,你把数据给第三方软件玄武(你想象成某快递软件也可以)用吧”,那微信平台肯定会回复说,“小明,玄武是谁啊,没在咱家备过案,我不能给他,万一是骗子呢?”对吧,你想想是不是这个逻辑。所以,授权这个大动作的前提,肯定是玄武要去微信平台那里“备案”,也就是注册。注册完后,微信商家开放平台就会给玄武软件 app_id 和app_secret 等信息,以方便后面授权时的各种身份校验。同时,注册的时候,第三方软件也会请求受保护资源的可访问范围。比如,玄武能否获取小明店铺 3 个月以前的订单,能否获取每条订单的所有字段信息等等。这个权限范围,就是 scope。 备完案之后,咱们接着继续前进。小明过来让平台把他的订单数据给第三方软件玄武,平台咔咔一查,对了下暗号,发现第三方软件玄武是合法的,于是就要推进下一步了。 在授权码许可类型中,授权服务的工作,可以划分为两大部分,一个是颁发授权码 code,一个是颁发访问令牌 access_token。

image.png

微信小程序的登录流程

通过简单的授权码场景可以了解到,微信为宿主的小程序使用微信登录是一件理所当然的事儿,但其实小程序并没有强制要求只能使用微信登录,也可以跟 Web 网站一样使用账户密码模式、邮箱、手机号等登录方式。既然如此,为什么我们一定要使用微信登录呢?因为除了便利性以外,微信登录更重要的优势是整合了微信庞大的生态系统,以及对于产品策略的加持。

一.微信小程序使用微信登录的优势

小程序是微信平台上的一款第三方应用,在登录方式的选择上有很高的自由度,微信登录仅仅是其中一个选择,你完全可以使用跟其他应用(网站、App)一样的登录方式,比如手机号、邮箱、用户名密码等。 事实上微信小程序仅仅是众多Web产品矩阵中的一个应用端,同时存在的还有网站、App 等应用端。从生态系统的角度上,相对于其他应用端,以微信为入口的小程序最大的优势是拥有微信完善的生态系统,用户使用微信登录后可以使用微信提供给小程序的各种平台级能力,比如订阅消息、微信支付、小程序直播、音视频对话等。

从用户体验的角度上,使用微信登录,用户能够很大程度上降低登录的复杂程度,无形中提高用户的登录成本和用户体验。从产品策略的角度上,使用微信登录小程序能够根据用户的来源,制定特殊的产品策略,比如对于小程序的用户发放商家优惠券、订阅消息等等。

所以由此得出小程序使用微信登录的三个主要优势:

融入微信生态;
提高用户体验;
制定产品策略。

这说明了小程序接入微信登录的必要性,那它的登录流程是什么呢?如下图:

image.png

二.微信小程序登录流程

整个登录流程中描述了三种角色和六个术语,了解它们的定位和作用,是理解小程序登录流程的基础。

image.png

登录流程里的三个角色,客户端在整个登录流程中主要承担两种行为:

作为整个流程的发起者,获取临时登录凭证 code;
作为整个流程的终结者,存储登录态令牌 token。

不过客户端的所有信息和网络请求几乎都是可以被破解或拦截的,所以出于安全的考虑,小程序登录流程中的一些接口被限制不能在客户端中直接调用,而是需要在服务端发起,开发者服务的工作便是处理这些安全敏感的网络请求,体现为上图中使用code 获取 openid 和 session_key的请求,这个请求使用了微信提供的 auth.code2Session 接口。

而微信接口服务的工作对于开发者来说是不透明的,你需要做的仅仅是根据接口的规范,组装网络请求发送给它,然后根据返回的接口执行分发逻辑。微信服务器会验证网络请求的合法性,对于合法请求下发密钥 session_key 和用户 openid。

三.登录流程的六个术语

1、code

它是在小程序内通过 API wx.login 获取的,然后通过 HTTP 请求发送给开发者服务器。code 的作用体现在“临时”两字上,它的有效期限仅有 5 分钟,并且仅能够使用一次(即请求一次 auth.code2Session 接口)。

2、appid

每个微信小程序在注册时就会生成一个唯一的appid,这个 ID 标记了小程序的唯一性,等同于网站的URL(经过备案的)、App 的包名等标记应用唯一性的信息。

3、appsecret

它是小程序的密钥,可以在微信公众平台的后台管理系统中获取。appsecret 是非常私密的信息,所以微信在制定小程序登录的流程时,将携带此信息的网络请求限制在只能通过开发者服务器发送给微信接口服务,这样对于客户端来说是不可见的,进而降低了被泄露的可能性。与appid 不同的是,appsecret 可以被重置,但每次重置之后,历史的 appsecret 便会失效。

4、openid

微信对于用户 openid 的定义是:微信号在某个应用程序中的唯一 ID。这里的“某个应用程序”(比如兰蔻小程序)指的是小程序、公众号、接入开放平台的应用。微信生态中目前有公众平台和开放平台两种,其中公众平台又细分为小程序和公众号,开放平台可以接入网站、移动应用等。同一个微信号在不同的应用程序中有不同的 openid。

在微信生态下另外有一个标记微信号的唯一 ID:UnionId。这个 ID 跟应用程序无关。所以,可以简单地理解为 openid 是 UnionId 与 appid 综合加密后的结果。

UnionId 通常用来关联在不同应用程序中各个 openid ,比如同一个微信号在小程序和公众号内需要配置同样的权限,仅通过 openid 无法实现,便需要获取此微信号的 UnionId。

5、session_key

session_key 是对用户数据进行加密签名的密钥,微信服务器使用它将用户的数据进行加密和解密。你可以简单地将 session_key 理解为获取用户数据的“绿卡”,登录之后所有涉及访问微信服务器的请求一般都需要带上它,微信服务器会校验 session_key 的合法性。

其实到这一步(即拿到了 openid 和 session_key)已经完成了小程序的登录流程,但对于一个应用程序来说,用户进行登录操作应该是“一劳永逸”的,即登录过一次之后在一定时间之内的后续操作都不需要再次登录,用技术语言描述就是应该保存用户的登录态。这个时候就需要用到接下来的一个术语:token。

6、token

登录态是个逻辑词汇,token 可以理解为登录态的具象化、数据化。在小程序的登录流程图中,你可以看出,token是由开发者服务器创建的一个字符串,而且需要跟 openid 和 session_key 相关联。其实这里并不是强制关联 openid,因为 openid 并不算是私密信息,可以放心地下发到客户端(即小程序)。但是 session_key 是非常私密的信息,一旦泄露有很大的安全隐患,所以强烈建议不要把它下发到客户端。

image.png 在获取到 openid 和 session_key 之后,开发者服务器创建一个 token,然后与 openid 和session_key 进行关联,具体的方法根据服务器编程语言的不同有多种实现方案。咱们以JavaScript 语言作为示例,可以创建一个对象,对象的 key 是 token 的值,value 是一个包含 openid 和 session_key 的对象,如下:

{
    "token_1": {
        "openid": "获取到的openid 1",
        "session_key": "获取到的session_key 1"
    },
    "token_2": {
        "openid": "获取到的openid 2",
        "session_key": "获取到的session_key 2"
    },
}
复制代码

关联完成之后开发者服务器将 token下发到客户端,客户端保存在本地,后续的所有请求均需要携带此 token,携带的方法并没有既定的规范,可以通过 URL Query、HTTP Body、Header 等,但通常建议通过 Header 传递,这样相对来说更安全一些。

以上,便是小程序接入微信登录的全部流程。

OAuth 2.0规范

OAuth 2.0 这种授权协议,就是保证第三方(软件)只有在获得授权之后,才可以进一步访问授权者的数据。这里的授权服务和受保护资源服务都是微信商家开放平台(不过应该是对应授权服务和订单服务)、客户端(第三方软件)对应的是玄武软件、资源的拥有者小明通过OAuth2.0的授权码模式授权后,第三方软件玄武可以访问对应的订单,

1.OAuth 2.0 规范中的角色划分

咱们先思考一个问题:小程序登录之后如果需要访问用户的数据(比如昵称、地域、性别等)需要得到谁的授权?是微信?还是用户? 答案是用户。用户的数据虽然存放在微信的服务器之上,但是这些数据的所有权属于用户自己,而不是微信。这里其实引出了 OAuth 2.0 规范中的两个基本概念。

Resource Owner:资源所有者,即用户;
Resource Server:资源服务器,即微信。

而小程序在获取用户数据中的角色是作为微信平台的第三方应用程序,在 OAuth 2.0 规范中的术语为 Third-party application。 除了以上三种角色之外,OAuth 2.0规范中还有另外三种角色:

小程序依托于微信提供的底层技术平台,微信为小程序提供了与用户(即Resource Owner)沟通的工具,它在 OAuth 2.0 规范中的角色被称为 User Agent(用户代理)。 微信服务器不仅仅作为 Resource Server 保存用户数据,同时在登录授权过程中又提供了HTTP服务以及授权认证功能,这两个功能的角色在 OAuth 2.0 规范中分别被称为 HTTP Service(HTTP服务提供商)和Authorization server(认证服务器)。

以上便是 OAuth 2.0 规范中的所有角色,为了加强了解,我们再梳理一遍:

  • Resource Owner(资源所有者):在小程序场景下代表小程序的用户。

  • Resource Server(资源服务器,即存放用户数据、资源的服务器):在小程序场景下这个角色由微信服务器承担。

  • Third-party application(第三方应用程序/又称客户端):在小程序场景下代表小程序。

  • User Agent(用户代理):在小程序场景下代表微信。

  • Authorization server(认证服务器):在小程序场景下,这个角色由微信服务器承担。

  • HTTP Service(HTTP 服务提供商):在小程序场景下,这个角色由微信服务器承担。

看到这里,可能觉得 OAuth 2.0 规范中的角色与小程序登录流程中角色不一样?

其实,小程序登录流程中的三个角色是按照实体划分的,而 OAuth 2.0 规范的角色是按照功能划分的,同一个实体可以担任一种或多种功能。 在小程序登录流程中的 3 个实体角色中,微信同时担任 Third-party application 和User Agent 的功能;微信服务器同时担任 Resource Server、Authorization server 和HTTP Service 的功能;开发者服务器比较特殊,它即担任 HTTP Service 的功能,同时在认证流程中由于需要转发和关联 token,所以也充当了客户端的一部分功能。

OAuth 2.0 规范要解决什么问题

比如你向邻居借了衣架忘了还,某天邻居着急使用所才打电话向你要回,不巧的是你正在外地出差家里没人。但好在你家的门锁是智能门锁,你可以将密码告诉邻居让他自己去你家里取。但是你本着“防人之心不可无”的心理,担心邻居是否会趁机记下甚至修改你家的门锁密码。左右为难的时候,你突然想起来你家的智能门锁可以创建临时密码,这种临时密码只能在 10 分钟之内有效,而且没有修改原本密码的权限。所以,最终你在手机上创建了一个临时智能门锁的密码发给你的邻居。

OAuth 2.0 规范要解决的问题与上面提到的这个现实案例非常相似,简单概括就是:OAuth 2.0是一个授权机制,资源所有者告诉认证服务器,临时授予某个第三方应用访问资源服务器获取资源的权限,认证服务器给第三方应用颁发一个临时令牌,拥有这个令牌便可以获取资源数据,一旦令牌过期或失效便收回权限。

OAuth 2.0 规范中的令牌与小程序登录场景下的 token 作用是一致的,只不过 OAuth 规范只定义了令牌的作用,并没有限制它的具体使用方法,微信把 token 与 session_key 相关联,开发者服务器通过 token 取到 session_key 进而解密用户资源数据。

简单梳理一下,授权服务的核心就是,先颁发授权码 code 值,再颁发访问令牌 access_token 值。在颁发访问令牌的同时还会颁发刷新令牌 refresh_token 值,这种机制可以在无须用户参与的情况下用于生成新的访问令牌。正如我们讲到的小明使用第三方软件玄武的例子,当访问令牌过期的时候,刷新令牌的存在可以大大提高小明使用第三方软件玄武的体验。

授权还要有授权范围,不能让第三方软件获得比注册时权限范围还大的授权,也不能获得超出了用户授权的权限范围,始终确保最小权限安全原则。比如,小明只为第三方软件玄武授予了获取当天订单的权限,那么第三方软件玄武就不能访问小明店铺里面的历史订单数据。

刷新令牌有过期时间吗,会一直有效吗?答案是刷新令牌也有有效期,若access_token未超时,那么进行refresh_token有两种方式,(1)不会改变access_token,但超时时间会刷新,相当于续期access_token(2)更新access_token的值,我们建议【统一更新access_token的值】。

无限续期会增加access被破解的风险,延期access_token并不是一个最好的方式,尽管有的开放平台是这么做的。不过当刷新令牌也过期了,只能重新登录再授权。

授权码的必要性

在 OAuth 2.0 的体系里面有 4 种角色,按照官方的称呼它们分别是资源拥有者、客户端、授权服务和受保护资源。我们不难发现,OAuth 2.0 授权的核心就是颁发访问令牌、使用访问令牌。 OAuth 2.0 的核心是授权许可,更进一步说就是令牌机制。也就是说,像第三方软件玄武这样的第三方软件只有拿到了微信商家开放平台颁发的访问令牌,也就是得到了授权许可,然后才可以代表用户访问他们的数据。

  • 1.互联网中所有的受保护资源,几乎都是以 Web API 的形式来提供访问的,比如极客时间App 要获取用户的头像、昵称,第三方软件玄武要获取用户的店铺订单,我们说OAuth 2.0 与安全相关,是用来保护 Web API 的。另外,第三方软件通过OAuth 2.0 取得访问权限之后,用户便把这些权限委托给了第三方软件,我们说 OAuth 2.0 是一种委托协议,也没问题。

  • 2.也正因为像玄武这样的第三方软件,每次都是用访问令牌而不是用户名和密码来请求用户的数据,才大大减少了安全风险上的“攻击面”。不然,我们试想一下,每次都带着用户名和密码来访问数量众多的 Web API ,是不是增加了这个“攻击面”。因此,我们说 OAuth 2.0 的核心,就是颁发访问令牌和使用访问令牌。

授权码和访问令牌的颁发流程

授权服务就是负责颁发访问令牌的服务。更进一步地讲,OAuth 2.0 的核心是授权服务,而授权服务的核心就是令牌。颁发授权码 code 的流程如下:

一、颁发授权码 code

在这个过程中,授权服务需要完成两部分工作,分别是准备工作和生成授权码 code。比如在点击微信小程序授权Button时,实际上已经做了一系列动作。包括验证基本信息、验证权限范围(第一次)和生成授权请求页面这三步等等。

① 验证基本信息

验证基本信息,包括对第三方软件合法性和回调地址合法性的校验。在 Web 浏览器环境下发放 code 的整个请求过程,都是浏览器通过前端通信来完成,这就意味着所有信息都有被冒充的风险。因此,授权服务必须对第三方软件的存在性做判断。同样,回调地址也是可以被伪造的。比如,不法分子将其伪装成钓鱼页面,或者是带有恶意攻击性的软件下载页面。因此从安全上考虑,授权服务需要对回调地址做基本的校验。在授权服务的程序中,这两步验证通过后,就会生成或者响应一个页面(属于授权服务器上的页面),以提示用户进行授权。

② 验证权限范围(第一次)

既然是授权,就会涉及范围。比如,我们使用微信登录第三方软件的时候,会看到微信提示我们,第三方软件可以获得你的昵称、头像、性别、地理位置等。如果你不想让第三方软件获取你的某个信息,那么可以不选择这一项。

这就意味着,我们需要对第三方软件玄武传过来的 scope 参数,与第三方软件玄武注册时申请的权限范围做比对。如果请求过来的权限范围大于注册时的范围,就需要作出越权提示。记住,此刻是第一次权限校验。

③ 生成授权请求页面或弹窗

当小程序或web页面出现不同权限的button授权弹窗时,用户可以选择缩小这个权限范围,比如仅授予获取 地理位置或手机号 的权限。至此,颁发授权码 code 的准备工作就完成了。你要注意哈,我一直强调说这也是准备工作,因为当用户点击授权按钮“确定”后,才会生成授权码 code 值和访问令牌acces_token 值,“一切才真正开始”。 在上面的准备过程中,我们忽略了小明登录的过程,但只有用户登录了才可以对第三方软件进行授权,授权服务才能够获得用户信息并最终生成 code 和app_id(第三方软件的应用标识) + user(资源拥有者标识)之间的对应关系。小明点击“同意”按钮之后,生成授权码 code 的流程就正式开始了,主要包括验证权限范围(第二次)、处理授权请求生成授权码 code 和重定向至第三方软件这三大步。

④ 验证权限范围(第二次)

在步骤二中,生成授权页面之前授权服务进行的第一次校验,是对比第三方软件玄武请求过来的权限范围 scope 和注册时的权限做的比对。这里为什么又要校验一次呢?因为这相当于一次用户的输入权限。用户选择了一定的权限范围给到授权服务,对于权限的校验我们要重视对待,凡是输入性数据都会涉及到合法性检查。另外,这也是要求我们养成一种在服务端对输入数据的请求,都尽可能做一次合法性校验的好习惯。

⑤ 处理授权请求,生成授权码 code

当用户同意授权之后,授权服务会校验响应类型 response_type 的值。response_type 有code 和 token 两种类型的值。在这里,我们是用授权码流程来举例的,因此代码要验证response_type 的值是否为 code。

在授权服务中,需要将生成的授权码 code 值与 app_id、user 进行关系映射。也就是说,一个授权码 code,表示某一个用户给某一个第三方软件进行授权,比如小明给第三方软件玄武进行的授权。同时,我们需要将 code 值和这种映射关系保存起来,以便在生成访问令牌 access_token 时使用。

在生成了授权码 code 之后,我们也按照上面所述绑定了响应的映射关系。这时,你还记得我之前讲到的授权码是临时的、一次性凭证吗?因此,我们还需要为 code 设置一个有效期。 OAuth 2.0 规范建议授权码 code 值有效期为 10 分钟,并且一个授权码 code 只能被使用一次。不过根据经验呢,在生产环境中 code 的有效期一般不会超过 5 分钟。同时,授权服务还需要将生成的授权码 code 跟已经授权的权限范围 rscope 进行绑定并存储,以便后续颁发访问令牌时,我们能够通过 code 值取出授权范围并与访问令牌绑定。因为第三方软件最终是通过访问令牌来请求受保护资源的。

⑥ 重定向至第三方软件

生成授权码 code 值之后,授权服务需要将该 code 值告知第三方软件玄武。开始时我们提到,颁发授权码 code 是通过前端通信完成的,因此这里采用重定向的方式。到此,颁发授权码 code 的流程全部完成。当第三方软件玄武获取到授权码 code 值以后,就可以开始请求访问令牌 access_token 的值。

二、颁发访问令牌 access_token

当第三方软件玄武拿着授权码 code 来请求的时候,授权服务需要为之生成最终的请求访问令牌。这个过程主要包括验证第三方软件是否存在、验证 code 值是否合法和生成 access_token 值这三大步。

① 验证第三方软件是否存在

此时,接收到的 grant_type 的类型为 authorization_code。由于颁发访问令牌是通过后端通信完成的,所以这里除了要校验 app_id 外,还要校验 app_secret。

String grantType = request.getParameter("grant_type");
  if("authorization_code".equals(grantType)){
}

if (!appMap.get("app_id").equals(appId)) {
  //app_id不存在
}
if (!appMap.get("app_secret").equals(appSecret)) {
  //app_secret不合法
}
复制代码
② 验证授权码 code 值是否合法

授权服务在颁发授权码 code 的阶段已经将 code 值存储了起来,此时对比从 request 中 接收到的 code 值和从存储中取出来的 code 值。这里我们一定要记住,确认过授权码 code 值有效以后,应该立刻从存储中删除当前的 code 值,以防止第三方软件恶意使用一个失窃的授权码 code 值来请求授权服务。

③ 生成访问令牌 access_token 值

关于按照什么规则来生成访问令牌 access_token 的值,OAuth 2.0 规范中并没有明确规定,但必须符合三个原则:唯一性、不连续性、不可猜性。和授权码 code 值一样,我们需要将访问令牌 access_token 值存储起来,并将其与第三方软件的应用标识 app_id 和资源拥有者标识 user 进行关系映射。也就是说,一个访问令牌 access_token 表示某一个用户给某一个第三方软件进行授权。 同时,授权服务还需要将授权范围跟访问令牌 access_token 做绑定。最后,还需要为该访问令牌设置一个过期时间 expires_in,比如 1 天。

正因为 OAuth 2.0 规范没有约束访问令牌内容的生成规则,所以我们有更高的自由度。我们既可以像 Demo 中那样生成一个 UUID 形式的数据存储起来,让授权服务和受保护资源共享该数据;也可以将一些必要的信息通过结构化的处理放入令牌本身。我们将包含了一些信息的令牌,称为结构化令牌,简称 JWT。

至此,授权码许可类型下授权服务的两大主要过程,也就是颁发授权码和颁发访问令牌的流程。接下来,你在分析微信登录的第三方软件的时候,就会明白很快背后的原理了。同时,你在自己搭建一个授权服务流程时,也会更加得心应手。这一切的原因,都在于颁发授权码和颁发访问令牌,就是授权服务的核心。

为什么一定要引入刷新令牌的概念。比如用户给微信小程序某电商应用授权以后,正在愉快地处理他的双十一的订单数据,结果没过多久,突然间授权过期,第三方电商应用再次让用户进行授权。此刻用户的体验是非常糟糕的。为此,OAuth 2.0 中引入了刷新令牌的概念,也就是刷新访问令牌 access_token 的值。这就意味着,有了刷新令牌,用户在一定期限内无需重新点击授权按钮,就可以继续使用第三方软件。

刷新令牌

刷新令牌也是给第三方软件使用的,同样需要遵循先颁发再使用的原则。因此,我们还是从颁发和使用两个环节来学习刷新令牌。

颁发刷新令牌

其实,颁发刷新令牌和颁发访问令牌是一起实现的,都是在过程二的步骤三生成访问令牌access_token 中生成的。也就是说,第三方软件得到一个访问令牌的同时,也会得到一个刷新令牌。 看到这里你可能要问了,为什么要一起生成访问令牌和刷新令牌呢?

其实,这就回到了刷新令牌的作用上了。刷新令牌存在的初衷是,在访问令牌失效的情况下,为了不让用户频繁手动授权,用来通过系统重新请求生成一个新的访问令牌。那么,如果访问令牌失效了,而“身边”又没有一个刷新令牌可用,岂不是又要麻烦用户进行手动授权了。所以,它必须得和访问令牌一起生成。

使用刷新令牌

在 OAuth 2.0 规范中,刷新令牌是一种特殊的授权许可类型,是嵌入在授权码许可类型下的一种特殊许可类型。在授权服务的代码里,当我们接收到这种授权许可请求的时候,会先比较 grant_type 和 refresh_token 的值,然后做下一步处理。

① 接收刷新令牌请求,验证基本信息

此时请求中的 grant_type 值为 refresh_token。和颁发访问令牌前的验证流程一样,这里我们也需要验证第三方软件是否存在。需要注意的是,这里需要同时验证刷新令牌是否存在,目的就是要保证传过来的刷新令牌的合法性。

另外,我们还需要验证刷新令牌是否属于该第三方软件。授权服务是将颁发的刷新令牌与第三方软件、当时的授权用户绑定在一起的,因此这里需要判断该刷新令牌的归属合法性。 需要注意,一个刷新令牌被使用以后,授权服务需要将其废弃,并重新颁发一个刷新令牌。

② 重新生成访问令牌

生成访问令牌的处理流程,与颁发访问令牌环节的生成流程是一致的。授权服务会将新的访问令牌和新的刷新令牌,一起返回给第三方软件。 很多电商小程序都会设置token失效自动刷新请求,做法如下:

/** 刷新Token, 默认只刷新一次 */
function refreshToken(params: AxiosRequestConfig) {
  return promisify(wx.login)()
    .then((res: WechatMiniprogram.LoginSuccessCallbackResult) => promisify(wx.request)({
      url: config.loginUrl + res.code
    }))
    .then((res: any) => {
      wx.setStorageSync('token', res.data.jwtString)
      return refreshRequest(params)
    }, (err: any) => {
      return Promise.reject(err)
    })
}

/** 重新发起请求 */
function refreshRequest(config: AxiosRequestConfig) {
  return promisify(wx.request)({
    url: config.url,
    header: Object.assign({}, config.headers, {
      'Authorization': wx.getStorageSync('token')
    }),
    data: config.data,
    method: config.method,
    timeout: config.timeout
  }).then((res: any) => {
    const response: AxiosResponse = {
      data: res.data,
      status: res.statusCode,
      statusText: res.errMsg,
      headers: res.header,
      config: config,
      cookies: res.cookies
    }
    return response
  }, (err: any) => {
    return Promise.reject(err)
  })
}
复制代码

特别注意

授权码许可流程有两种通信方式。一种是前端通信,因为它通过浏览器促成了授权码的交互流程,比如微信商家开放平台的授权服务生成授权码发送到浏览器,第三方软件玄武从浏览器获取授权码。正因为获取授权码的时候第三方软件玄武和授权服务并没有发生直接的联系,也叫做间接通信。另外一种是后端通信,在第三方软件玄武获取到授权码之后,在后端服务直接发起换取访问令牌的请求,也叫做直接通信。 为什么要用授权码换取token,而不是直接获取token?

直接获取token的场景是有的,客户端凭据许可类型就是这样的使用场景,授权码许可类型是OAuth 2.0 最安全 最完备的许可类型。 以Web场景为例,第一次用户是跟第三方软件建立的“联系”,三方软件要把用户引导到平台一方去授权,这个时候用户实际上跟三方软件就失去了“联系”,平台如果这个时候直接把令牌给了三方软件,因为没了“联系”,三方软件就不能很方便的告诉用户。

总结

在 OAuth 2.0 中,访问令牌被要求有极高的安全保密性,因此我们不能让它暴露在浏览器上面,只能通过第三方软件(比如玄武)的后端服务来获取和使用,以最大限度地保障访问令牌的安全性。正因为访问令牌的这种安全要求特性,当需要前端通信,比如浏览器上面的流转的时候,OAuth 2.0 才又提供了一个临时的凭证:授权码。通过授权码的方式,可以让用户小明在授权服务上给第三方软件玄武授权之后,还能重新回到第三方软件玄武的操作页面上。

从授权码许可流程中就可以看出来,它完美地将 OAuth 2.0 的 4 个角色组织了起来,并保证了它们之间的顺畅通信。它提出的这种结构和思想都可以被迁移到其他环境或者协议上,比如在微信小程序中使用授权码许可。

不过,也正是因为有了授权码的参与,才使得授权码许可要比其他授权许可类型,在授权的流程上多出了好多步骤,让授权码许可类型成为了 OAuth 2.0 体系中迄今流程最完备、安全性最高的授权流程。

参考文献

《OAuth 2.0实战课》王新栋老师

《云原生微信小程序开发实战》周俊鹏

文章分类
前端
文章标签