oauth2.0

314 阅读21分钟

“OAuth 2.0 到底是什么呢?我们先从字面上来分析下。OAuth 2.0 一词中的 “Auth” 表示 “授权”,字母 “O” 是 Open 的简称,表示 “开放” ,连在一起就表示 “开放授权”。这也是为什么我们使用 OAuth 的场景,通常发生在开放平台的环境下”

“OAuth 2.0 授权的核心就是颁发访问令牌、使用访问令牌,而且不管是哪种类型的授权流程都是这样。你一定要理解,或者记住这句话,它是整个流程的核心。”

授权码许可(Authorization Code)类型是 OAuth 2.0 中最经典、最完备、最安全、应用最广泛的许可类型。除了授权码许可类型外,OAuth 2.0 针对不同的使用场景,还有 3 种基础的许可类型,分别是隐式许可(Implicit)、客户端凭据许可(Client Credentials)、资源拥有者凭据许可(Resource Owner Password Credentials)。相对而言,这 3 种授权许可类型的流程,在流程复杂度和安全性上都有所减弱”

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

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

授权码许可类型的具体流程介绍

“OAuth 2.0 的体系里面有 4 种角色,按照官方的称呼它们分别是资源拥有者、客户端、授权服务和受保护资源”,这里的客户端也可以成为第三方软件,4 个角色是 “两两站队” 的:资源拥有者和第三方软件“站在一起”,因为第三方软件要代表资源拥有者去访问受保护资源;授权服务和受保护资源“站在一起”,因为授权服务负责颁发访问令牌受保护资源负责接收并验证访问令牌

小明使用小兔软件打印订单 为例,这里 “资源拥有者 -> 小明,第三方软件 -> 小兔软件,授权服务 -> 京东商家开放平台的授权服务,受保护资源 -> 小明店铺在京东上面的订单。”

image.png

关于为什么要两次通信,不能直接返回access_token

如果不要授权码,返回授权码的这一步实际上就可以直接返回访问令牌 access_token 了,按着没有授权码的思路继续想,如果这里直接返回访问令牌,那我们肯定不能使用重定向的方式。因为这样会把安全保密性要求极高的访问令牌暴露在浏览器上,从而将会面临访问令牌失窃的安全风险。显然,这是不能被允许的,所以如果没有授权码的话,我们就只能把访问令牌发送给第三方软件小兔的后端服务。按照这样的逻辑,上面的流程图就会变成下面这样:

image.png

这样看似没有问题,授权服务直接把access_token返回给后端,后端服务拿着access_token去访问资源就能拿到订单数据了,但是会有一个问题

当小明被浏览器重定向到授权服务上之后,小明跟小兔软件之间的 “连接” 就断了,相当于此时此刻小明跟授权服务建立了“连接”后,将一直“停留在授权服务的页面上”。你会看到图 2 中问号处的时序上,小明再也没有重新“连接”到小兔软件。但是,这个时候小兔软件已经拿到了小明授权之后的访问令牌,也使用访问令牌获取到了小明店铺里的订单数据。这时,考虑到“小明的感受”,小兔软件应该要通知到小明,但是如何做呢?现在“连接断了”,这事儿恐怕就没那么容易了。OK,为了让小兔软件能很容易地通知到小明,还必须让小明跟小兔软件重新建立起 “连接”。这就是我们看到的第二次重定向,小明授权之后,又重新重定向回到了小兔软件的地址上,这样小明就跟小兔软件有了新的 “连接”。到这里,你就能理解在授权码许可的流程中,为什么需要两次重定向了吧。为了重新建立起这样的一次连接,我们又不能让访问令牌暴露出去,就有了这样一个临时的、间接的凭证:授权码。因为小兔软件最终要拿到的是安全保密性要求极高的访问令牌,并不是授权码,而授权码是可以暴露在浏览器上面的。这样有了授权码的参与,访问令牌可以在后端服务之间传输,同时呢还可以重新建立小明与小兔软件之间的“连接”。这样通过一个授权码,既“照顾”到了小明的体验,又“照顾”了通信的安全

在介绍授权码许可类型时,我提到了很多次 “授权服务”。一句话概括,授权服务就是负责颁发访问令牌的服务。更进一步地讲,OAuth 2.0 的核心授权服务,而授权服务的核心就是令牌

授权工作是怎么开展的呢

想要获取授权平台的信任,第三方软件肯定是要去平台那里“备案”,也就是注册。比如注册完后,京东商家开放平台就会给小兔软件 app_id 和 app_secret 等信息,以方便后面授权时的各种身份校验。同时,注册的时候,第三方软件也会请求受保护资源的可访问范围。比如,小兔能否获取小明店铺 3 个月以前的订单,能否获取每条订单的所有字段信息等等。这个权限范围,就是 scope。后面呢,我还会详细讲述范围控制。文字说起来有点抽象,咱们还是直接上代码吧。关于注册后的数据存储,我们使用如下 Java 代码来模拟:

image.png

授权工作主要分成两个部分,一是颁发授权码,二是颁发访问令牌,流程如下

image.png

我们先看颁发授权码的流程

在这个过程中,授权服务需要完成两部分工作,分别是准备工作生成授权码 code

你可能会问了,这个“准备”都包括哪些工作?我们可以想到,小明在给第三方软件小兔打单软件进行授权的时候,会看到授权页面上有一个授权按钮,但是授权服务在小明看到这个授权按钮之前,实际上已经做了一系列动作。这些动作,就是所谓的准备工作,包括验证基本信息、验证权限范围(第一次)和生成授权请求页面这三步。我们具体分析下。

第一步,验证基本信息。

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

image.png

在授权服务的程序中,这两步验证通过后,就会生成或者响应一个页面(属于授权服务器上的页面),以提示小明进行授权。

第二步,验证权限范围(第一次)

既然是授权,就会涉及范围。比如,我们使用微信登录第三方软件的时候,会看到微信提示我们,第三方软件可以获得你的昵称、头像、性别、地理位置等。如果你不想让第三方软件获取你的某个信息,那么可以不选择这一项。同样在小兔中也是一样,当小明为小兔进行授权的时候,也可以选择给小兔的权限范围,比如是否授予小兔获取 3 个月以前的订单的访问权限。这就意味着,我们需要对小兔传过来的 scope 参数,与小兔注册时申请的权限范围做比对。如果请求过来的权限范围大于注册时的范围,就需要作出越权提示。记住,此刻是第一次权限校验。

image.png

第三步,生成授权请求页面。

这个授权请求页面就是授权服务上的页面,如下图所示

image.png

页面上显示了小兔注册时申请的 today、history 两种权限,小明可以选择缩小这个权限范围,比如仅授予获取 today 信息的权限。至此,颁发授权码 code 的准备工作就完成了。你要注意哈,我一直强调说这也是准备工作,因为当用户点击授权按钮“approve”后,才会生成授权码 code 值和访问令牌 acces_token 值,“一切才真正开始”。

这里需要说明下:在上面的准备过程中,我们忽略了小明登录的过程,但只有用户登录了才可以对第三方软件进行授权,授权服务才能够获得用户信息并最终生成 code 和 app_id(第三方软件的应用标识) + user(资源拥有者标识)之间的对应关系。你可以把登录部分的代码,作为附加练习。小明点击“approve”按钮之后,生成授权码 code 的流程就正式开始了,主要包括验证权限范围(第二次)、处理授权请求生成授权码 code 和重定向至第三方软件这三大步。接下来,我们一起分析下这三步。

第四步,验证权限范围(第二次)。

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

image.png

第五步,处理授权请求,生成授权码 code。

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

image.png

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

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

image.png 第六步,重定向至第三方软件。

生成授权码 code 值之后,授权服务需要将该 code 值告知第三方软件小兔。开始时我们提到,颁发授权码 code 是通过前端通信完成的,因此这里采用重定向的方式。

image.png 到此,颁发授权码 code 的流程全部完成。当小兔获取到授权码 code 值以后,就可以开始请求访问令牌 access_token 的值了,也就是我们即将开始的过程二。

**过程二:颁发访问令牌 **

access_token我们在过程一中介绍了授权码 code 的生成流程,但小兔最终是要获取到访问令牌 access_token,才可以去请求受保护资源。而授权码呢,正如我在上一讲提到的,只是一个换取访问令牌 access_token 的临时凭证。当小兔拿着授权码 code 来请求的时候,授权服务需要为之生成最终的请求访问令牌。这个过程主要包括验证第三方软件小兔是否存在、验证 code 值是否合法和生成 access_token 值这三大步。接下来,我们一起分析下每一步。

第一步,验证第三方软件是否存在。

此时,接收到的 grant_type 的类型为 authorization_code。

image.png 由于颁发访问令牌是通过后端通信完成的,所以这里除了要校验 app_id 外,还要校验 app_secret。

image.png

第二步,验证授权码 code 值是否合法。

授权服务在颁发授权码 code 的阶段已经将 code 值存储了起来,此时对比从 request 中接收到的 code 值和从存储中取出来的 code 值。在我们给出的课程相关代码中,code 值对应的 key 是 app_id 和 user 的组合值。

image.png

这里我们一定要记住,确认过授权码 code 值有效以后,应该立刻从存储中删除当前的 code 值,以防止第三方软件恶意使用一个失窃的授权码 code 值来请求授权服务。第三步,生成访问令牌 access_token 值。关于按照什么规则来生成访问令牌 access_token 的值,OAuth 2.0 规范中并没有明确规定,但必须符合三个原则:唯一性、不连续性、不可猜性。在我们给出的 Demo 中,我们是使用 UUID 来作为示例的。和授权码 code 值一样,我们需要将访问令牌 access_token 值存储起来,并将其与第三方软件的应用标识 app_id 和资源拥有者标识 user 进行关系映射。也就是说,一个访问令牌 access_token 表示某一个用户给某一个第三方软件进行授权。同时,授权服务还需要将授权范围跟访问令牌 access_token 做绑定。最后,还需要为该访问令牌设置一个过期时间 expires_in,比如 1 天。

image.png

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

上面就是授权服务的两个工作过程

刷新令牌

颁发的时机:颁发刷新令牌和颁发访问令牌是一起实现的,都是在上面过程二的步骤三生成访问令牌 access_token 中生成的。也就是说,第三方软件得到一个访问令牌的同时,也会得到一个刷新令牌

image.png

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

第一步,接收刷新令牌请求,验证基本信息。

此时请求中的 grant_type 值为 refresh_token。

image.png

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

image.png 另外,我们还需要验证刷新令牌是否属于该第三方软件。授权服务是将颁发的刷新令牌与第三方软件、当时的授权用户绑定在一起的,因此这里需要判断该刷新令牌的归属合法性。

image.png

image.png 需要注意,一个刷新令牌被使用以后,授权服务需要将其废弃,并重新颁发一个刷新令牌。

第二步,重新生成访问令牌。

生成访问令牌的处理流程,与颁发访问令牌环节的生成流程是一致的。授权服务会将新的访问令牌和新的刷新令牌,一起返回给第三方软件。

关于用JWT来作为令牌

大多数情况下,授权服务都是返回一个无意义的随机值来作为令牌,OAuth 2.0 规范并没有约束访问令牌内容的生成规则,只要符合唯一性、不连续性、不可猜性就够了,这就意味着,我们可以灵活选择令牌的形式,既可以是没有内部结构且不包含任何信息含义的随机字符串,也可以是具有内部结构且包含有信息含义的字符串。在结构化令牌这方面,目前用得最多的就是 JWT 令牌了

JWT 这种结构化体可以分为 HEADER(头部)、PAYLOAD(数据体)和 SIGNATURE(签名)三部分。经过签名之后的 JWT 的整体结构,是被句点符号分割的三段内容,结构为 header.payload.signature 。比如下面这个示例

image.png

这个 JWT 令牌看起来也是毫无意义的、随机的字符串啊。确实,你直接去看这个字符串是没啥意义,但如果你把它拷贝到jwt.io/ 网站的在线校验工具中,就可以看到解码之后的数据:

image.png

HEADER 表示装载令牌类型和算法等信息,是 JWT 的头部。其中,typ 表示第二部分 PAYLOAD 是 JWT 类型,alg 表示使用 HS256 对称签名的算法。

PAYLOAD 表示是 JWT 的数据体,代表了一组数据。其中,sub(令牌的主体,一般设为资源拥有者的唯一标识)、exp(令牌的过期时间戳)、iat(令牌颁发的时间戳)是 JWT 规范性的声明,代表的是常规性操作。更多的通用声明,你可以参考RFC 7519 开放标准。不过,在一个 JWT 内可以包含一切合法的 JSON 格式的数据,也就是说,PAYLOAD 表示的一组数据允许我们自定义声明。

SIGNATURE 表示对 JWT 信息的签名。那么,它有什么作用呢?我们可能认为,有了 HEADER 和 PAYLOAD 两部分内容后,就可以让令牌携带信息了,似乎就可以在网络中传输了,但是在网络中传输这样的信息体是不安全的,因为你在“裸奔”啊。所以,我们还需要对其进行加密签名处理,而 SIGNATURE 就是对信息的签名结果,当受保护资源接收到第三方软件的签名后需要验证令牌的签名是否合法。

用JWT令牌的好处是什么

第一,JWT 的核心思想,就是用计算代替存储,有些 “时间换空间” 的 “味道”。当然,这种经过计算并结构化封装的方式,也减少了“共享数据库” 因远程调用而带来的网络传输消耗,所以也有可能是节省时间的。也就是说资源服务不需要调用授权服务的接口来查询令牌是否合法,可以自己直接解析令牌

第二,也是一个重要特性,是加密。因为 JWT 令牌内部已经包含了重要的信息,所以在整个传输过程中都必须被要求是密文传输的,这样被强制要求了加密也就保障了传输过程中的安全性。这里的加密算法,既可以是对称加密,也可以是非对称加密。(这里没有很懂,是因为有重要信息才加密的,如果没有信息,不加密又有什么问题呢)

第三,使用 JWT 格式的令牌,有助于增强系统的可用性和可伸缩性。这一点要怎么理解呢?我们前面讲到了,这种 JWT 格式的令牌,通过“自编码”的方式包含了身份验证需要的信息,不再需要服务端进行额外的存储,所以每次的请求都是无状态会话。这就符合了我们尽可能遵循无状态架构设计的原则,也就是增强了系统的可用性和伸缩性

JWT令牌的缺点是什么

JWT 格式令牌的最大问题在于 “覆水难收”,也就是说,没办法在使用过程中修改令牌状态。我们还是借助小明使用小兔软件例子,先停下来想一下。小明在使用小兔软件的时候,是不是有可能因为某种原因修改了在京东的密码,或者是不是有可能突然取消了给小兔的授权?这时候,令牌的状态是不是就要有相应的变更,将原来对应的令牌置为无效。但,使用 JWT 格式令牌时,每次颁发的令牌都不会在服务端存储,这样我们要改变令牌状态的时候,就无能为力了。因为服务端并没有存储这个 JWT 格式的令牌。这就意味着,JWT 令牌在有效期内,是可以“横行无止”的

怎么解决这个问题

首先我们不可以把 JWT 令牌存储到远程的分布式内存数据库中来解决这个问题,因为这会违背 JWT 的初衷(将信息通过结构化的方式存入令牌本身);通常都是通过修改密匙来导致令牌校验失效的思路来做

一是,将每次生成 JWT 令牌时的秘钥粒度缩小到用户级别,也就是一个用户一个秘钥。这样,当用户取消授权或者修改密码后,就可以让这个密钥一起修改。一般情况下,这种方案需要配套一个单独的密钥管理服务。

二是,在不提供用户主动取消授权的环境里面,如果只考虑到修改密码的情况,那么我们就可以把用户密码作为 JWT 的密钥。当然,这也是用户粒度级别的。这样一来,用户修改密码也就相当于修改了密钥。(这里其实有个问题,密码当作密钥的话,校验的时候不还是要去数据库找密钥,做一次数据库查询吗)