从事互联网开发的同学们应该或多或少听说过 OAuth2.0 协议,例如使用微信或支付宝账户登录第三方App,这是 OAuth2.0 最为开发人员所熟知的一个用途,但是围绕着 OAuth2.0 协议其实还有很多有意思的内容可以挖掘,我们可以用它以及它的扩展协议来做许多有用的东西。因此我打算写一个系列来详细介绍 OAuth2.0 以及围绕它所产生的一些扩展协议和优良实践。
如果你正想要设计一个基于 OAuth2.0 协议的授权服务架构来对外提供一些资源或服务,那么本系列文章将有助于你实现这一目标。
本文是本系列的第一篇,我会比较详细地带你解读 OAuth2.0 协议。本文会从基础概念和术语开始讲起,让你逐渐了解 OAuth2.0 的核心理念以及它所要解决的问题,然后将会用一多半的篇幅来详细阐述4大流程模式中使用最广的授权码模式,我会介绍它的流程、接口、错误处理、注意事项等。
注:本文涉及到 OAuth2.0 协议的所有内容全部来自于 RFC 6749 The OAuth 2.0 Authorization Framework ,也就是说全部都是第一手的解读。
什么是授权?要解释这个问题,先要引入一个概念——资源。信息或者数据就是一种资源,例如用户的身份信息、用户的相册数据、评论记录、收藏的网站等等,除了信息或数据之外,完成某项特定操作的能力也是一种资源,通常以API的形式出现,例如发送短信、上传头像、发布vlog等等,这些都是API也是资源。有些资源需要用户的授权(如获得用户身份信息),而另一些资源不需要得到用户的授权(如发送短信),我们今天所讨论的,是需要用户授权的那一类——毕竟这些资源的所有权属于用户。
在什么时候需要授权呢?我们来假设一个场景,我们大部分开发者都有Github账号(假装大家都有),但是由于某些原因,Github以前并不提供移动版的官方客户端,但广大群众需要在移动端使用Github的呼声很强烈,这怎么办呢?Github的做法是,虽然我不提供官方的app,但是我提供用于管理用户资源的APIs,这些APIs包含了用户认证信息(user)、仓库(repositories)、订阅者(followers)、已订阅(following)、星标(starred)记录等等几乎全部的数据,这样就可以吸引那些热心的开发者为他编写非官方的客户端程序,或者其它有用的应用。那么是不是所有人或程序都能毫无限制的去使用这些 APIs 呢?显然是不可以的,那么Github就需要通过某种机制来保护这些APIs,使得调用者必须要在用户的允许(consent)之下才能够进行调用。那么授权的含义就很容易理解了——即调用者(client)获取用户的“允许”以访问用户的数据的过程就称为“授权”。
我们现在明白什么是授权了,那么授权认证是什么意思呢?授权认证其实就是授权当中的一类特殊情况,即调用者(client)所要申请访问的资源属于用户身份信息(或称为认证信息)且调用者(client)使用此资源的目的在于帮助其(client)对用户进行认证时,这种特殊的授权行为又称为授权认证。
什么是资源外放?在上文中,Github网站提供了用于访问用户数据的API的行为就属于资源外放。资源外放通常要搭配授权协议一起实施。
什么是协议?协议就是标准,就是一套规则体系。上文中,用于规定授权操作的流程、接口、错误处理方式等等规则的集合,就是一个授权协议。目前互联网上使用最广的授权协议是OAuth2.0。
OAuth2.0是由IETF(互联网工程小组)编纂并维护的一套授权协议,是目前业界的事实标准。
国内的微信、支付宝,国外的微软、谷歌等等巨头,都采用了此协议来对外提供API资源。
在这里我有必要强调一点,OAuth2.0 并不是一个认证协议,它不是被设计为解决认证问题的,如果你希望设计一个认证架构,那么你应该考虑 OpenID Connect (OIDC) 协议,它是 OAuth2.0 的衍生协议,由 OpenID 发起并维护,目前已得到很多大型互联网公司的支持,例如谷歌、微软、苹果、亚马逊。而如果你要学习 OIDC 协议,那么我建议你也应该从 OAuth2.0 协议开始。
从这一段开始,我们来正式讲解 OAuth2 协议,首先来介绍一下此协议有哪些重要的概念或术语。
Resource Owner
资源拥有者,即用户
Resource Server
资源服务器,即存储用户资源的服务器,通常以 API 的形式暴露服务,它会在验证 Access Token 通过后为调用者提供用户资源
Client
客户端,即申请用户授权的主体,可以是前端程序 —— 如 JAVAScript 应用、移动端原生 App 等等,也可以是服务器端应用程序 —— 如 Java 程序
Authorization Server
授权服务器(签发授权码和访问令牌)
Access Token
访问令牌,拿到此令牌即可访问 Resource Server 中的用户资源
Refresh Token
刷新令牌,用来获得新的 Access Token ,有时 Authorization Server 会在签发 Access Token 的同时也附加一个 Refresh Token ,这样 Client 就不必每次都去询问用户获得授权了
Client Type
客户端类型,分为 Public 和 Confidential 两种
Client Identifier
客户端标识,即 client_id ,唯一标识一个 Client
Client Authentication
客户端认证,对 Public 类型的 Client 采用 PKCE 的方式认证,而对 Confidential 类型的 Client 一般会采用 Basic 方式或基于非对称算法的 JWT 方式来进行认证。
尽管网络上很多作者都会直接开始讲解授权码模式的流程图,但我认为还是有必要先了解 OAuth2.0 最最基本的抽象流程图,它是所有具体流程模式的抽象概括版,可以让我们跳出具体的流程模式并站在一定高度上来审视 OAuth2.0 协议究竟是什么,解决了哪些问题,对于你理解一些关于 OAuth2.0 的误区是有帮助的,如下图:
OAuth2.0 抽象流程图
图片来自 RFC6749 - The OAuth 2.0 Authorization Framework
上图展现了 OAuth2.0 协议的本质,它概括为3个步骤:申请授权、获得授权、访问资源。 OAuth2.0 的所有流程模式(除了早先的四大模式之外还有谷歌的设备码模式)都是这3个步骤的具体表现形式。
网络上很多教程会一上来就给你看授权码模式的流程图,而我必须要说明一点,授权码模式不等同于 OAuth2.0 协议的全部,尽管该模式的确是使用最多的一种流程模式,但它只是一种流程模式,不是此协议的本质。
如果跳过上面的抽象流程图而直接开始讲授权码模式、简化模式等流程模式,那么有一个很大的坏处就是,它会使你误认为 OAuth2.0 是一个认证协议,因为授权码模式(还有简化模式)中带有认证过程,即 Authorization Server 会对 User 进行认证,认证之后才会有授权码 code 和 Access Token ,这会让你误以为拿到 code 或 Access Token 就代表用户已得到认证,你就可以拿到用户的身份信息了,但在抽象流程图中并没有出现“认证 Authentication”的字眼。Access Token 压根就不是一个表示认证信息的令牌,它是一个授权令牌,它只能表示一个 Client 确实获得了某用户的某项资源的访问权限,但这既不能体现用户的登录状态,亦不表示一定能够获得用户的身份信息,例如使用 Refresh Token 就可以刷新得到一个新的 Access Token ,而这个过程是不需要用户参与的。
基本流程
授权码模式是应用最广泛的一种流程模式,它可以应用于任何有 Web 支持的平台,例如传统 Web 网站,单页应用 SPA ,移动端原生 App 等等,它的流程图如下:
OAuth2.0 授权码模式流程图
分步骤讲解:
A) 客户端程序( Client )向资源拥有者( Resource Owner )申请授权,并指定一个回跳地址用于接收接下来所产生的授权码或错误信息
B) 资源拥有者同意授权,并返回一个凭证给客户端程序,这个凭证就是授权码 code
C) 客户端程序拿到授权凭证(即授权码 code )后向授权服务器(Authorization Server)申请 Access Token
D) 授权服务器验证客户端程序提供的授权凭证后返回 Access Token 给到客户端 Client
E) 客户端程序 Client 使用 Access Token 访问资源服务器(Resource Server)中的用户资源
F) 资源服务器验证 Access Token 后返回受保护的资源
以上描述中注意客户端程序并不是指 C-S 架构下的 Client 客户端,它在这里既可以指前端程序(如 SPA 或 APP ),也可以指WEB网站的后台服务器(如 Java Web 应用)
在授权码模式中,我们可以看到 Authorization Server 提供了2个接口( Endpoint ),即申请授权接口与获得授权令牌接口,对应的分别是申请授权与获得授权这两个步骤。下面我们来详细看一下这两个接口都定义了哪些内容以及它们的使用方法。
注意:OAuth2.0 及其扩展协议中所涉及的所有接口( Endpoint )全部都是 HTTP 接口
申请授权接口 Authorization Endpoint
申请授权接口的请求 Authorization Request
首先,Client 需要先构建一个用于跳转的 uri 地址,这个地址是由 Authorization Server 即授权服务器提供的,Client 需要在它的后面拼接如下这些参数(使用 application/x-www-form-urlencoded 编码方式对参数进行编码):
申请授权接口的请求参数
然后 Client 要控制用户的 User-Agent (一般就是浏览器)跳转到刚刚构建的 uri 上去,下面是一个例子:
申请授权接口的请求例子
在此接口中,有如下几点规则需要注意:
申请授权接口的响应(授权成功的情况) Authorization Response
申请授权接口的响应参数(同意授权的情况)
申请授权接口的响应例子(同意授权的情况)
申请授权接口的响应(授权失败的情况) Authorization Error Response
申请授权接口的响应参数(授权失败的情况)
申请授权接口的错误代码值
申请授权接口的响应例子(授权失败的情况)
很多人在实现 OAuth2.0 的过程中往往会忽视对于错误情况的处理,其实 OAuth2.0 已经规定了错误处理方式了,如果你的目标是要建立一个标准的 OAuth2.0 服务,那么别忽视错误处理这一环。
返回错误信息时要特别注意的几点
获取访问令牌接口 Access Token Endpoint
获取访问令牌接口的请求 Access Token Request
获取访问令牌接口的请求参数
获取访问令牌接口的请求例子
注意:在请求中,注意编码格式 Content-Type 是 application/x-www-form-urlencoded ,并且 method 是 POST ,这与下面的响应是不一样的。
在此接口中,授权服务器 Authorization Server 必须满足以下几点:
获取访问令牌接口的响应
获取访问令牌接口的响应参数(成功的情况)
注意, expires_in 的单位是秒,如果你不返回此参数,则应当与 Client 约定一个默认值,并在文档中注明。
获取访问令牌接口的响应例子(成功的情况)
关于获取访问令牌接口的响应,有以下几点要注意:
上面是授权成功的情况,那么授权失败的情况呢?
获取访问令牌接口的响应参数(失败的情况)
获取访问令牌接口的错误代码值
获取访问令牌接口的响应例子(错误的情况)
关于本接口在错误情况下的响应,有以下几点要注意:
现在 Client 获得了 Access Token 之后,就可以去访问受保护资源了,而 Resource Server 要如何校验 Access Token 呢?其实这一点最开始 ITEF 并没有归纳到协议中,而是通过另外一个协议来将此内容补充上了,请参见 RFC 7662 - OAuth 2.0 Token Introspection
至此,我们已经介绍完了 OAuth2.0 协议的基础知识和授权码模式,其实 OAuth2.0 协议还有非常多值得挖掘的细节,以及很多扩展或衍生协议,来帮助你在各种不同的平台上使用这个协议来完成授权或授权认证的工作,我会在其它文章中陆续为大家解读 OAuth2.0 协议的方方面面。
如果本文中有哪些地方你没有读懂,或希望重点展开的,欢迎给我留言。另外如果你发现本文存在纰漏,也欢迎随时给我留言,共同学习。