This commit is contained in:
louzefeng
2024-07-11 05:50:32 +00:00
parent bf99793fd0
commit d3828a7aee
6071 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,101 @@
<audio id="audio" title="01 | OAuth 2.0是要通过什么方式解决什么问题?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/9a/08/9a647b240689c769294d3736b5103c08.mp3"></audio>
你好,我是王新栋。
在课程正式开始之前我想先问你个问题。第一次使用极客时间App的时候你是直接使用了第三方帐号比如微信、微博登录还是选择了重新注册新用户如果你选择了重新注册用户那你还得上传头像、输入用户名等信息。但如果你选择了使用第三方帐号微信来登录那极客时间会直接使用你微信的这些信息作为基础信息你就能省心很多。
到这里,我估计你会问,这是怎么实现的?微信把我的个人信息给了极客时间,它又是怎么保证我的数据安全的呢?
其实微信这一系列授权背后的原理都可以归到一个词上那就是OAuth 2.0。今天这节课我们就来看看OAuth 2.0到底是什么、能干什么以及它是怎么干的。
## OAuth 2.0是什么?
用一句话总结来说OAuth 2.0就是一种授权协议。那如何理解这里的“授权”呢?
我举个咱们生活中的例子。假如你是一名销售人员,你想去百度拜访你的大客户王总。到了百度的大楼之后,保安拦住了你,问你要工牌。你说:“保安大哥啊,我是来拜访王总的,哪里有什么工牌”。保安大哥说:“那你要去前台做个登记”。
然后你就赶紧来到前台,前台美女问你是不是做了登记。你说王总秘书昨天有要你的手机号,说是已经做过预约。小姐姐确认之后往你的手机发了个验证码,你把验证码告诉了前台小姐姐之后,她给了你一张门禁卡,于是你就可以开心地去见王总了。
你看,这个例子里面就有一次授权。本来你是没有权限进入百度大楼的,但是经过前台小姐姐一系列的验证之后,她发现你确实是来拜访客户的,于是给了你一张临时工牌。这整个过程就是授权。
我再举一个电商的场景,你估计更有感觉。假如你是一个卖家,在京东商城开了一个店铺,日常运营中你要将订单打印出来以便给用户发货。但打印这事儿也挺繁琐的,之前你总是手工操作,后来发现有个叫“小兔”的第三方软件,它可以帮你高效率地处理这事。
但你想想小兔是怎么访问到这些订单数据的呢其实是这样京东商城提供了开放平台小兔通过京东商家开放平台的API就能访问到用户的订单数据。
只要你在软件里点击同意,小兔就可以拿到一个访问令牌,通过访问令牌来获取到你的订单数据帮你干活儿了。你看,这里也是有一次授权。你要是不同意,平台肯定不敢把这些数据给到第三方软件。
## 为什么用OAuth 2.0
基于上面两种场景的解决方案,关于授权我们最容易想到的方案就是提供钥匙。比如,你要去百度拜访王总,那前台小姐姐就给你张百度的工牌;小兔要获取你的订单信息,那你就把你的用户名和密码给它。但稍微有些安全意识,我们都不会这样做。
因为你有了百度工牌那以后都可以随时自由地进出了这显然不是百度想要的。所以百度有一套完整的机制通过给你一张临时工牌实现在保证安全的情况下还能让你去大楼里面见到王总。相应地小兔软件请求访问你的订单数据的过程也会涉及这样一套授权机制那就是OAuth 2.0。它通过给小兔软件一个访问令牌,而不是让小兔软件拿着你的用户名和密码,去获取你的订单数据帮你干活儿。
其实除了小兔软件这个场景在如今的互联网世界里用到OAuth 2.0的地方非常多只是因为它隐藏了实现细节需要我们多做分析才能发现它。比如当你使用微信登录其他网站或者App的时候当你开始使用某个小程序的时候你都在无感知的情况下用到了OAuth 2.0。
那总结来说,**OAuth 2.0这种授权协议,就是保证第三方(软件)只有在获得授权之后,才可以进一步访问授权者的数据**。因此我们常常还会听到一种说法OAuth 2.0是一种安全协议。现在你知道了,这种说法也是正确的。
现在访问授权者的数据主要是通过Web API所以凡是要保护这种对外的API时都需要这样授权的方式。而OAuth 2.0的这种颁发访问令牌的机制是再合适不过的方法了。同时这样的Web API还在持续增加所以OAuth 2.0是目前Web上重要的安全手段之一了。
## OAuth 2.0是怎样运转的?
现在我相信你已经对OAuth 2.0有了一个整体印象,接下来咱们再看看它是怎么运转的。
我们还是来看上面提到的小兔打单软件的例子吧。假如小明在京东上面开了一个店铺,小明要管理他的店铺里面的订单,于是选择了使用小兔软件。
现在,让我们把“小明”“小兔软件”“京东商家开放平台”放到一个对话里面,看看“他们”是怎么沟通的吧。
**小明**“你好小兔软件。我正在Google浏览器上面需要访问你来帮我处理我在京东商城店铺的订单。”
**小兔软件**:“好的,小明,我需要你给我授权。现在我把你引导到京东商家开放平台上,你在那里给我授权吧。”
**京东商家开放平台**:“你好,小明。我收到了小兔软件跳转过来的请求,现在已经准备好了一个授权页面。你登录并确认后,点击授权页面上面的授权按钮即可。”
**小明**:“好的,京东商家开放平台。我看到了这个授权页面,已经点授权按钮啦😄”
**京东商家开放平台**“你好小兔打单软件。我收到了小明的授权现在要给你生成一个授权码code值我通过浏览器重定向到你的回调URL地址上面了。”
**小兔软件**“好的京东商家开放平台。我现在从浏览器上拿到了授权码现在就用这个授权码来请求你请给我一个访问令牌access_token吧。”
**京东商家开放平台**:“好的,小兔打单软件,访问令牌已经发送给你了。”
**小兔打单软件**:“太好了,我现在就可以使用访问令牌来获取小明店铺的订单了。”
**小明**:“我已经能够看到我的订单了,现在就开始打单操作了。”
下面,为了帮助你理解,我再用一张图来描述整个过程:
<img src="https://static001.geekbang.org/resource/image/77/79/77197844a8f41a33cb68947b1dc9ee79.png" alt="" title="小明使用小兔软件打印订单的整体流程">
再分析下这个流程,我们不难发现小兔软件最终的目的,是要获取一个叫做“访问令牌”的东西。从最后一步也能够看出来,在小兔软件获取到**访问令牌**之后,才有足够的 “能力” 去请求小明的店铺的订单,也就是才能够帮助小明打印订单。
那么,小兔软件是怎么获取访问令牌的值的呢?我们会发现还有一个叫做“授权码”的东西,也就是说小兔软件是拿**授权码换取的访问令牌**。
小兔软件又是怎么拿到**授权码**的呢?从图中流程刚开始的那一步,我们就会发现,是在小明授权之后,才产生的授权码,上面流程中后续的一切动作,实际上都是在小明对小兔软件授权发生以后才产生的。其中主要的动作,就是生成授权码–&gt;生成访问令牌–&gt;使用访问令牌。
到这里,我们不难发现,**OAuth 2.0 授权的核心就是颁发访问令牌、使用访问令牌,**而且不管是哪种类型的授权流程都是这样。你一定要理解,或者记住这句话,它是整个流程的核心。你也可以再回想下,去百度拜访王总的例子。如果你是百度这套机制的设计者的话,会怎么设计这套授权机制呢。想清楚了这个问题,你再去理解令牌、授权码啥的也就简单了。
在小兔软件这个例子中呢我们使用的就是授权码许可Authorization Code类型。它是OAuth 2.0中最经典、最完备、最安全、应用最广泛的许可类型。除了授权码许可类型外OAuth 2.0针对不同的使用场景还有3种基础的许可类型分别是隐式许可Implicit、客户端凭据许可Client Credentials、资源拥有者凭据许可Resource Owner Password Credentials。相对而言这3种授权许可类型的流程在流程复杂度和安全性上都有所减弱我会在第6讲与你详细分析
因此在这个课程中我会频繁用授权码许可类型来举例。至于为什么称它为授权码许可为什么有两次重定向以及这种许可类型更详细的通信流程又是怎样的我会在第2讲给你深入分析你可以先不用关注。
## 总结
好了今天这节课就到这里。这节课咱们知识点不多我来回给你举例子其实就是希望你能理解OAuth到底是什么为什么需要它以及它大概的运行逻辑是怎样的。总结来说我需要你记住以下这3个关键点
<li>
OAuth 2.0的核心是授权许可,更进一步说就是令牌机制。也就是说,像小兔软件这样的第三方软件只有拿到了京东商家开放平台颁发的访问令牌,也就是得到了授权许可,然后才可以**代表**用户访问他们的数据。
</li>
<li>
互联网中所有的受保护资源几乎都是以Web API的形式来提供访问的比如极客时间App要获取用户的头像、昵称小兔软件要获取用户的店铺订单我们说OAuth 2.0与安全相关是用来保护Web API的。另外第三方软件通过OAuth 2.0取得访问权限之后,用户便把这些权限**委托**给了第三方软件我们说OAuth 2.0是一种委托协议,也没问题。
</li>
<li>
也正因为像小兔这样的第三方软件每次都是用访问令牌而不是用户名和密码来请求用户的数据才大大减少了安全风险上的“攻击面”。不然我们试想一下每次都带着用户名和密码来访问数量众多的Web API 是不是增加了这个“攻击面”。因此我们说OAuth 2.0的核心,就是颁发访问令牌和使用访问令牌。
</li>
## 思考题
好了,今天这一讲我们马上要结束了,我给你留个思考题。
你可以再花时间想下小兔软件获取用户订单信息的那个场景,如果让你来设计整个的授权流程,你会怎么设计?还有没有更好的方式?
欢迎你在留言区分享你的观点,也欢迎你把今天的内容分享给其他朋友,我们一起交流。

View File

@@ -0,0 +1,144 @@
<audio id="audio" title="02 | 授权码许可类型中,为什么一定要有授权码?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/73/65/7385f73c8987de2580aff0e3b43b6765.mp3"></audio>
你好,我是王新栋。
在上一讲我提到了OAuth 2.0 的授权码许可类型,在小兔打单软件的例子里面,小兔最终是通过**访问令牌**请求到小明的店铺里的订单数据。同时呢,我还提到了,这个**访问令牌是通过授权码换来的**。到这里估计你会问了,为什么要用授权码来换令牌?为什么不能直接颁发访问令牌呢?
你可以先停下来想想这个问题。今天咱们这节课,我会带着你深入探究下其中的逻辑。
## 为什么需要授权码?
在讲这个问题之前我先要和你同步下在OAuth 2.0的体系里面有4种角色按照官方的称呼它们分别是资源拥有者、客户端、授权服务和受保护资源。不过这里的客户端我更愿意称其为第三方软件而且在咱们这个课程中都是以第三方软件在举例子。所以在后续的讲解中我统一把它称为第三方软件。
所以,你在看官方资料的时候,可以自己对应下。为了便于你理解,我还是拿小兔软件来举例子,将官方的称呼 “照进现实”,对应关系就是,**资源拥有者-&gt;小明,第三方软件-&gt;小兔软件,授权服务-&gt;京东商家开放平台的授权服务,受保护资源-&gt;小明店铺在京东上面的订单**。
在理解了这些概念以后,让我们继续。
你知道,**OAuth诞生之初就是为了解决Web浏览器场景下的授权问题**,所以我基于浏览器的场景,在上一讲的小明使用小兔软件打印订单的整体流程的基础上,画了一个授权码许可类型的序列图。
当然了这里还是有小兔软件来继续陪伴着我们不过这次为了能够更好地表述授权码许可流程我会把小兔软件的前端和后端分开展示并把京东商家开放平台的系统按照OAuth 2.0的组件拆分成了授权服务和受保护资源服务。如下图所示:
<img src="https://static001.geekbang.org/resource/image/96/32/96973a6f5637fb3d1049f6d456702932.png" alt="" title="图1 以小兔软件为例,授权码许可类型的序列图">
突然看到这个序列图增加了这么多步骤的时候,你是不是有些紧张?那如果我告诉你再细分的话步骤还要更多,你是不是就更困惑了?
不过,别紧张,这没啥关系。一方面,咱们这一讲的重点就是跟授权码相关的流程,你只需关注这里的重点步骤,也就是两次重定向相关的步骤就够了。在下一讲中,我再教你如何将这些步骤进一步拆解。另一方面,我接下来还会用另一种视角来帮助你分析这个流程。
我们继续来看这张序列图。从图中看到在第4步授权服务生成了授权码code按照一开始我们提出来的问题如果不要授权码这一步实际上就可以直接返回访问令牌access_token了。
按着这个没有授权码的思路继续想,如果这里直接返回访问令牌,那我们肯定不能使用重定向的方式。因为**这样会把安全保密性要求极高的访问令牌暴露在浏览器上**,从而将会面临访问令牌失窃的安全风险。显然,这是不能被允许的。
也就是说,如果没有授权码的话,我们就只能把访问令牌发送给第三方软件小兔的后端服务。按照这样的逻辑,上面的流程图就会变成下面这样:
<img src="https://static001.geekbang.org/resource/image/f4/33/f44866070ee06bc3fcceac792570d433.png" alt="" title="图2 如果没有授权码,直接把访问令牌发送给第三方软件小兔的后端服务">
到这里,看起来天衣无缝。小明访问小兔软件,小兔软件说要打单你得给我授权,不然京东不干,然后小兔软件就引导小明跳转到了京东的授权服务。到授权服务之后,京东商家开放平台验证了小兔的合法性以及小明的登录状态后,生成了授权页面。紧接着,小明赶紧点击同意授权,这时候,京东商家开放平台知道可以把小明的订单数据给小兔软件。
于是京东商家开放平台没含糊赶紧生成访问令牌access_token并且通过后端服务的方式返回给了小兔软件。这时候小兔软件就能正常工作了。
这样,问题就来了,什么问题呢?**当小明被浏览器重定向到授权服务上之后,小明跟小兔软件之间的 “连接” 就断了**,相当于此时此刻小明跟授权服务建立了“连接”后,将一直“停留在授权服务的页面上”。**你会看到图2中问号处的时序上小明再也没有重新“连接”到小兔软件。**
但是,这个时候小兔软件已经拿到了小明授权之后的访问令牌,也使用访问令牌获取到了小明店铺里的订单数据。这时,考虑到“小明的感受”,小兔软件应该要通知到小明,但是如何做呢?现在“连接断了”,这事儿恐怕就没那么容易了。
OK为了让小兔软件能很容易地通知到小明**还必须让小明跟小兔软件重新建立起 “连接”**。这就是我们看到的第二次重定向,小明授权之后,又重新重定向回到了小兔软件的地址上,这样**小明就跟小兔软件有了新的 “连接”**。
到这里,你就能理解在授权码许可的流程中,为什么需要两次重定向了吧。
为了重新建立起这样的一次连接,我们又不能让访问令牌暴露出去,就有了这样一个**临时的、间接的凭证:授权码**。因为小兔软件最终要拿到的是安全保密性要求极高的访问令牌,并不是授权码,而授权码是可以暴露在浏览器上面的。这样有了授权码的参与,访问令牌可以在后端服务之间传输,同时呢还可以重新建立小明与小兔软件之间的“连接”。这样通过一个授权码,既“照顾”到了小明的体验,又“照顾”了通信的安全。
这下,你就知道为什么要有授权码了吧。
那么,在执行授权码流程的时候,授权码和访问令牌在小兔软件和授权服务之间到底是怎么流转的呢?要回答这个问题,就需要继续分析一下授权码许可类型的通信过程了。
## 授权码许可类型的通信过程
图1的通信过程中标识出来的步骤就有9个一步步地去分析看似会很复杂所以我会用另一个维度来分析以帮助你理解也就是从直接通信和间接通信的维度来分析。这里所谓的间接通信就是指获取授权码的交互而直接通信就是指通过授权码换取访问令牌的交互。
接下来,我们就一起分析下吧,看看哪些是间接通信,哪些又是直接通信。
### 间接通信
我们先分析下为什么是“间接”。
我们把图1中获取授权码code的流程 “放大”,并换个角度来看一看,也就是将浏览器这个代理放到第三方软件小兔和授权服务中间。于是,我们来到了下面这张图:
<img src="https://static001.geekbang.org/resource/image/9e/bf/9e4f51f1f77840bd0b8f756be40d42bf.jpg" alt="" title="图3 获取授权码的交互过程">
这个过程,仿佛有这样的一段对话。
>
小明:“你好,小兔软件,我要访问你了。”
>
小兔软件:“好的,我把你引到授权服务那里,我需要授权服务给我一个授权码。”
>
授权服务:“小兔软件,**我把授权码发给浏览器了**。”
>
小兔软件:“好的,我从浏览器拿到了授权码。”
不知道你注意到没有,第三方软件小兔和授权服务之间,并没有发生直接的通信,而是**通过浏览器这个“中间人” 来 “搭线”的**。因此,我们说这是一个间接通信的方式。
### 直接通信
那我们再分析下授权码换取访问令牌的交互为什么是“直接”的。我们再把图1中获取访问令牌的流程“放大”就得到了下面的图示
<img src="https://static001.geekbang.org/resource/image/84/9b/84dc2d6f578b6968b782a0280a73be9b.png" alt="" title="图4 授权码换取访问令牌的交互过程">
相比获取授权码过程的间接通信获取访问令牌的直接通信就比较容易理解了就是第三方软件小兔获取到授权码code值后向授权服务发起获取访问令牌access_token的通信请求。这个请求是第三方软件服务器跟授权服务的服务器之间的通信都是在后端服务器之间的请求和响应因此也叫作后端通信。
### 两个 “一伙”
了解了上面的通信方式之后不知道你有没有意识到OAuth 2.0 中的4个角色是 “两两站队” 的:资源拥有者和第三方软件“站在一起”,因为第三方软件要代表资源拥有者去访问受保护资源;授权服务和受保护资源“站在一起”,因为授权服务负责颁发访问令牌,受保护资源负责接收并验证访问令牌。
<img src="https://static001.geekbang.org/resource/image/1c/ff/1c86e21496882894d7f03b35a01972ff.jpg" alt="" title="图5 OAuth 2.0 中的4个角色是“两两站队”">
讲到这里的时候,你会发现在这一讲,介绍授权码流程的时候我都是以浏览器参与的场景来讲的,那么浏览器一定要参与到这个流程中吗?
其实,授权码许可流程,不一定要有浏览器的参与。接下来,我们就继续分析下其中的逻辑。
## 一定要有浏览器吗?
OAuth 2.0发展之初开放生态环境相对单薄以浏览器为代理的Web应用居多授权码许可类型 “理所当然” 地被应用到了通过浏览器才能访问的Web应用中。
但实际上OAuth 2.0 是一个授权理念,或者说是一种授权思维。它的授权码模式的思维可以移植到很多场景中,比如微信小程序。在开发微信小程序应用时,我们通过授权码模式获取用户登录信息,[官方文档的地址示例](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/login/auth.code2Session.html)中给出的 **grant_type=authorization_code** ,就没有用到浏览器。
根据微信官方文档描述,开发者获取用户登录态信息的过程正是一个授权码的许可流程:
- 首先开发者通过wx.login(Object object)方法获取到登录凭证code值这一步的流程是在小程序内部通过调用微信提供的SDK实现
- 然后再通过该code值换取用户的session_key等信息也就是官方文档的auth.code2Session方法同时该方法也是被强烈建议通过开发者的后端服务来调用的。
你可以看到这个过程并没有使用到浏览器但确实按照授权码许可的思想走了一个完整的授权码许可流程。也就是说先通过小程序前端获取到code值再通过小程序的后端服务使用code值换取session_key等信息只不过是访问令牌access_token的值被换成了session_key。
```
GET https://api.weixin.qq.com/sns/jscode2session?appid=APPID&amp;secret=SECRET&amp;js_code=JSCODE&amp;grant_type=authorization_code
```
你看,这整个过程体现的就是授权码许可流程的思想。
## 总结
这节课又接近尾声了,我再带你回顾下重点内容。
今天,我从为什么需要授权码这个问题开始讲起,并通过授权码把授权码许可流程整体的通信过程串了一遍,提到了授权码这种方式解决的问题,也提到了授权码流程的通信方式。总结来说,我需要你记住以下两点。
1. 授权码许可流程有两种通信方式。一种是前端通信,因为它通过浏览器促成了授权码的交互流程,比如京东商家开放平台的授权服务生成授权码发送到浏览器,第三方软件小兔从浏览器获取授权码。**正因为获取授权码的时候小兔软件和授权服务并没有发生直接的联系,也叫做间接通信**。另外一种是后端通信,在小兔软件获取到授权码之后,**在后端服务直接发起换取访问令牌的请求,也叫做直接通信**。
1. 在OAuth 2.0中访问令牌被要求有极高的安全保密性因此我们不能让它暴露在浏览器上面只能通过第三方软件比如小兔的后端服务来获取和使用以最大限度地保障访问令牌的安全性。正因为访问令牌的这种安全要求特性当需要前端通信比如浏览器上面的流转的时候OAuth 2.0才又提供了一个临时的凭证:授权码。**通过授权码的方式,可以让用户小明在授权服务上给小兔授权之后,还能重新回到小兔的操作页面上**。这样,在保障安全性的情况下,提升了小明在小兔上的体验。
从授权码许可流程中就可以看出来它完美地将OAuth 2.0 的4个角色组织了起来并保证了它们之间的顺畅通信。**它提出的这种结构和思想都可以被迁移到其他环境或者协议上,比如在微信小程序中使用授权码许可。**
不过也正是因为有了授权码的参与才使得授权码许可要比其他授权许可类型在授权的流程上多出了好多步骤让授权码许可类型成为了OAuth 2.0体系中迄今流程最完备、安全性最高的授权流程。在接下来的两讲中,我还会为你重点讲解授权码许可类型下的授权服务。
## 思考题
好了,今天这一讲我们马上要结束了,我给你留个思考题。
关于不需要浏览器参与的授权码许可流程,你还能列举出更多的应用场景吗?
欢迎你在留言区分享你的观点,也欢迎你把今天的内容分享给其他朋友,我们一起交流。

View File

@@ -0,0 +1,348 @@
<audio id="audio" title="03 | 授权服务:授权码和访问令牌的颁发流程是怎样的?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/22/db/227a3b35ec78177f64085a1f7f1bc6db.mp3"></audio>
你好,我是王新栋。
在上一讲我从为什么需要授权码这个问题开始为你串了一遍授权码许可流程整体的通信过程。在接下来的三讲中我会着重为你讲解关于授权服务的工作流程、授权过程中的令牌以及如何接入OAuth 2.0。这样一来,你就可以吃透授权码许可这一最经典、最完备、最常用的授权流程了,以后再处理授权相关的逻辑就更得心应手了。现在呢,让我们开始这一讲。
在介绍授权码许可类型时,我提到了很多次 “授权服务”。一句话概括,授权服务就是**负责颁发访问令牌**的服务。更进一步地讲OAuth 2.0的核心是授权服务,而授权服务的核心就是令牌。
为什么这么说呢?当第三方软件比如小兔,要想获取小明在京东店铺的订单,就必须先从京东商家开放平台的授权服务那里获取访问令牌,进而通过访问令牌来 **“代表”** 小明去请求小明的订单数据。这不恰恰就是整个OAuth 2.0授权体系的核心吗?
那么,授权服务到底是怎么生成访问令牌的,这其中包含了哪些操作呢?还有一个问题是,访问令牌过期了而用户又不在场的情况下,又如何重新生成访问令牌呢?
带着这两个问题,我们就以授权码许可类型为例,一起深入探索下授权服务这个核心组件吧。
## 授权服务的工作过程
开始之前,你还是要先回想下小明给小兔软件授权订单数据的整个流程。
我们说小兔软件先要让小明去京东商家开放平台那里给它授权数据,那这里是不是你觉得很奇怪?你总不能说,“嘿,京东,你把数据给小兔用吧”,那京东肯定会回复说,“小明,小兔是谁啊,没在咱家备过案,我不能给他,万一是骗子呢?”
对吧你想想是不是这个逻辑。所以授权这个大动作的前提肯定是小兔要去平台那里“备案”也就是注册。注册完后京东商家开放平台就会给小兔软件app_id和app_secret等信息以方便后面授权时的各种身份校验。
同时注册的时候第三方软件也会请求受保护资源的可访问范围。比如小兔能否获取小明店铺3个月以前的订单能否获取每条订单的所有字段信息等等。这个权限范围就是scope。后面呢我还会详细讲述范围控制。
文字说起来有点抽象咱们还是直接上代码吧。关于注册后的数据存储我们使用如下Java代码来模拟
```
Map&lt;String,String&gt; appMap = new HashMap&lt;String, String&gt;();//模拟第三方软件注册之后的数据库存储
appMap.put(&quot;app_id&quot;,&quot;APPID_RABBIT&quot;);
appMap.put(&quot;app_secret&quot;,&quot;APPSECRET_RABBIT&quot;);
appMap.put(&quot;redirect_uri&quot;,&quot;http://localhost:8080/AppServlet-ch03&quot;);
appMap.put(&quot;scope&quot;,&quot;nickname address pic&quot;);
```
备完案之后,咱们接着继续前进。小明过来让平台把他的订单数据给小兔,平台咔咔一查,对了下暗号,发现小兔是合法的,于是就要推进下一步了。
咱们上节课讲过,在授权码许可类型中,授权服务的工作,可以划分为两大部分,一个是**颁发授权码code**,一个是**颁发访问令牌access_token**。为了更能表达授权码和访问令牌的存在,我在图中用深色将其标注了出来:
<img src="https://static001.geekbang.org/resource/image/a5/11/a5d231c5b356ecf2031yy7d17207c011.png" alt="">
我们先看看颁发授权码code的流程。
### 过程一颁发授权码code
在这个过程中,授权服务需要完成两部分工作,分别是**准备工作**和**生成授权码code**。
你可能会问了,这个“准备”都包括哪些工作?我们可以想到,小明在给第三方软件小兔打单软件进行授权的时候,会看到授权页面上有一个授权按钮,但是授权服务在小明看到这个授权按钮之前,实际上已经做了一系列动作。
这些动作,就是所谓的准备工作,包括验证基本信息、验证权限范围(第一次)和生成授权请求页面这三步。我们具体分析下。
**第一步,验证基本信息。**
验证基本信息,包括对第三方软件小兔合法性和回调地址合法性的校验。
在 Web 浏览器环境下颁发code的整个请求过程都是浏览器通过前端通信来完成这就意味着所有信息都有被冒充的风险。因此授权服务必须对第三方软件的存在性做判断。
同样,回调地址也是可以被伪造的。比如,不法分子将其伪装成钓鱼页面,或者是带有恶意攻击性的软件下载页面。因此从安全上考虑,授权服务需要对回调地址做基本的校验。
```
if(!appMap.get(&quot;redirect_uri&quot;).equals(redirectUri)){
//回调地址不存在
}
```
在授权服务的程序中,这两步验证通过后,就会生成或者响应一个页面(**属于授权服务器上的页面**),以提示小明进行授权。
**第二步,验证权限范围(第一次)。**
既然是授权就会涉及范围。比如我们使用微信登录第三方软件的时候会看到微信提示我们第三方软件可以获得你的昵称、头像、性别、地理位置等。如果你不想让第三方软件获取你的某个信息那么可以不选择这一项。同样在小兔中也是一样当小明为小兔进行授权的时候也可以选择给小兔的权限范围比如是否授予小兔获取3个月以前的订单的访问权限。
这就意味着我们需要对小兔传过来的scope参数与小兔注册时申请的权限范围做比对。如果请求过来的权限范围大于注册时的范围就需要作出越权提示。**记住,此刻是第一次权限校验。**
```
String scope = request.getParameter(&quot;scope&quot;);
if(!checkScope(scope)){
//超出注册的权限范围
}
```
**第三步,生成授权请求页面。**
这个授权请求页面就是授权服务上的页面,如下图所示:
<img src="https://static001.geekbang.org/resource/image/5e/66/5e024b40a98b65a54082106a96734c66.png" alt="">
页面上显示了小兔注册时申请的today、history 两种权限小明可以选择缩小这个权限范围比如仅授予获取today信息的权限。
至此颁发授权码code的准备工作就完成了。你要注意哈我一直强调说这也是准备工作因为当用户点击授权按钮“approve”后才会**生成授权码code值和访问令牌acces_token值**,“一切才真正开始”。
这里需要说明下在上面的准备过程中我们忽略了小明登录的过程但只有用户登录了才可以对第三方软件进行授权授权服务才能够获得用户信息并最终生成code 和 app_id第三方软件的应用标识 + user资源拥有者标识之间的对应关系。你可以把登录部分的代码作为附加练习。
小明点击“approve”按钮之后生成授权码code的流程就正式开始了主要包括验证权限范围第二次、处理授权请求生成授权码code和重定向至第三方软件这三大步。接下来我们一起分析下这三步。
**第四步,验证权限范围(第二次)。**
在步骤二中生成授权页面之前授权服务进行的第一次校验是对比小兔请求过来的权限范围scope和注册时的权限做的比对。这里的第二次验证权限范围是用小明进行授权之后的权限再次与小兔软件注册的权限做校验。
那这里为什么又要校验一次呢?因为这相当于一次用户的输入权限。小明选择了一定的权限范围给到授权服务,对于权限的校验我们要重视对待,凡是输入性数据都会涉及到合法性检查。另外,这也是要求我们养成一种**在服务端对输入数据的请求,都尽可能做一次合法性校验的好习惯**。
```
String[] rscope =request.getParameterValues(&quot;rscope&quot;);
if(!checkScope(rscope)){
//超出注册的权限范围
}
```
**第五步处理授权请求生成授权码code。**
当小明同意授权之后授权服务会校验响应类型response_type的值。response_type有code和token两种类型的值。在这里我们是用授权码流程来举例的因此代码要验证response_type的值是否为code。
```
String responseType = request.getParameter(&quot;response_type&quot;);
if(&quot;code&quot;.equals(responseType)){
}
```
在授权服务中需要将生成的授权码code值与app_id、user进行关系映射。也就是说一个授权码code表示某一个用户给某一个第三方软件进行授权比如小明给小兔软件进行的授权。同时我们需要将code值和这种映射关系保存起来以便在生成访问令牌access_token时使用。
```
String code = generateCode(appId,&quot;USERTEST&quot;);//模拟登录用户为USERTEST
private String generateCode(String appId,String user) {
...
String code = strb.toString();
codeMap.put(code,appId+&quot;|&quot;+user+&quot;|&quot;+System.currentTimeMillis());
return code;
}
```
在生成了授权码code之后我们也按照上面所述绑定了响应的映射关系。这时你还记得我之前讲到的授权码是临时的、一次性凭证吗因此我们还需要为code设置一个有效期。
OAuth 2.0规范建议授权码code值有效期为10分钟并且**一个授权码code只能被使用一次**。不过根据经验呢在生产环境中code的有效期一般不会超过5分钟。关于授权码code相关的安全方面的内容我还会在第8讲中详细讲述。
同时,授权服务还需要**将生成的授权码code跟已经授权的权限范围rscope进行绑定并存储**以便后续颁发访问令牌时我们能够通过code值取出授权范围并与访问令牌绑定。因为第三方软件最终是通过访问令牌来请求受保护资源的。
```
Map&lt;String,String[]&gt; codeScopeMap = new HashMap&lt;String, String[]&gt;();
codeScopeMap.put(code,rscope);//授权范围与授权码做绑定
```
**第六步,重定向至第三方软件。**
生成授权码code值之后授权服务需要将该code值告知第三方软件小兔。开始时我们提到颁发授权码code是通过前端通信完成的因此这里采用重定向的方式。这一步的重定向也是我在上一讲中提到的第二次重定向。
```
Map&lt;String, String&gt; params = new HashMap&lt;String, String&gt;();
params.put(&quot;code&quot;,code);
String toAppUrl = URLParamsUtil.appendParams(redirectUri,params);//构造第三方软件的回调地址,并重定向到该地址
response.sendRedirect(toAppUrl);//授权码流程的“第二次”重定向
```
到此颁发授权码code的流程全部完成。当小兔获取到授权码code值以后就可以开始请求访问令牌access_token的值了也就是我们即将开始的过程二。
### 过程二颁发访问令牌access_token
我们在过程一中介绍了授权码code的生成流程但小兔最终是要获取到访问令牌access_token才可以去请求受保护资源。而授权码呢正如我在上一讲提到的只是一个换取访问令牌access_token的临时凭证。
当小兔拿着授权码code来请求的时候授权服务需要为之生成最终的请求访问令牌。这个过程主要包括验证第三方软件小兔是否存在、验证code值是否合法和生成access_token值这三大步。接下来我们一起分析下每一步。
**第一步,验证第三方软件是否存在。**
此时接收到的grant_type的类型为authorization_code。
```
String grantType = request.getParameter(&quot;grant_type&quot;);
if(&quot;authorization_code&quot;.equals(grantType)){
}
```
由于颁发访问令牌是通过后端通信完成的所以这里除了要校验app_id外还要校验app_secret。
```
if(!appMap.get(&quot;app_id&quot;).equals(appId)){
//app_id不存在
}
if(!appMap.get(&quot;app_secret&quot;).equals(appSecret)){
//app_secret不合法
}
```
**第二步验证授权码code值是否合法。**
授权服务在颁发授权码code的阶段已经将code值存储了起来此时对比从request中接收到的code值和从存储中取出来的code值。在我们给出的课程[相关代码](https://github.com/xindongbook/oauth2-code)中code值对应的key是app_id和user的组合值。
```
String code = request.getParameter(&quot;code&quot;);
if(!isExistCode(code)){//验证code值
//code不存在
return;
}
codeMap.remove(code);//授权码一旦被使用,须立即作废
```
这里我们一定要记住,**确认过授权码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天。
```
Map&lt;String,String[]&gt; tokenScopeMap = new HashMap&lt;String, String[]&gt;();
String accessToken = generateAccessToken(appId,&quot;USERTEST&quot;);//生成访问令牌access_token的值
tokenScopeMap.put(accessToken,codeScopeMap.get(code));//授权范围与访问令牌绑定
//生成访问令牌的方法
private String generateAccessToken(String appId,String user){
String accessToken = UUID.randomUUID().toString();
String expires_in = &quot;1&quot;;//1天时间过期
tokenMap.put(accessToken,appId+&quot;|&quot;+user+&quot;|&quot;+System.currentTimeMillis()+&quot;|&quot;+expires_in);
return accessToken;
}
```
正因为OAuth 2.0规范没有约束访问令牌内容的生成规则所以我们有更高的自由度。我们既可以像Demo中那样生成一个UUID形式的数据存储起来让授权服务和受保护资源共享该数据也可以将一些必要的信息通过结构化的处理放入令牌本身。**我们将包含了一些信息的令牌称为结构化令牌简称JWT**。在下一讲中我还会与你详细讲述JWT。
至此,授权码许可类型下授权服务的两大主要过程,也就是颁发授权码和颁发访问令牌的流程,我就与你讲完了。
接下来,你在阅读别人的授权流程代码,或者是使用诸如通过微信登录的第三方软件的时候,就会明白背后的原理了。同时,你在自己搭建一个授权服务流程时,也会更加得心应手。这一切的原因,都在于颁发授权码和颁发访问令牌,就是授权服务的核心。
到这里你应该还会注意到一个问题在生成访问令牌的时候我们还给它附加了一个过期时间expires_in这意味着访问令牌会在一定的时间后失效。访问令牌失效就意味着资源拥有者给第三方软件的授权失效了第三方软件无法继续访问资源拥有者的受保护资源了。
这时,如果你还想继续使用第三方软件,就只能重新点击授权按钮,比如小明给小兔软件授权以后,正在愉快地处理他店铺的订单数据,结果没过多久,突然间小兔软件再次让小明进行授权。此刻,我们可以替小明感受一下他的心情。
显然这样的用户体验非常糟糕。为此OAuth 2.0中引入了刷新令牌的概念也就是刷新访问令牌access_token的值。这就意味着有了刷新令牌用户在一定期限内无需重新点击授权按钮就可以继续使用第三方软件。
接下来,我们就一起看看刷新令牌的工作原理吧。
## 刷新令牌
刷新令牌也是给第三方软件使用的,同样需要遵循**先颁发再使用**的原则。因此,我们还是从颁发和使用两个环节来学习刷新令牌。不过,这个颁发和使用流程和访问令牌有些是相同的,所以我只会和你重点讲述其中的区别。
### 颁发刷新令牌
其实颁发刷新令牌和颁发访问令牌是一起实现的都是在过程二的步骤三生成访问令牌access_token中生成的。也就是说第三方软件得到一个访问令牌的同时也会得到一个刷新令牌
```
Map&lt;String,String&gt; refreshTokenMap = new HashMap&lt;String, String&gt;();
String refreshToken = generateRefreshToken(appId,&quot;USERTEST&quot;);//生成刷新令牌refresh_token的值
private String generateRefreshToken(String appId,String user){
String refreshToken = UUID.randomUUID().toString();
refreshTokenMap.put(refreshToken,appId+&quot;|&quot;+user+&quot;|&quot;+System.currentTimeMillis());
return refreshToken;
}
```
看到这里你可能要问了,为什么要一起生成访问令牌和刷新令牌呢?
其实,这就回到了刷新令牌的作用上了。刷新令牌存在的初衷是,在访问令牌失效的情况下,为了不让用户频繁手动授权,用来通过系统重新请求**生成一个新的访问令牌**。那么,如果访问令牌失效了,而“身边”又没有一个刷新令牌可用,岂不是又要麻烦用户进行手动授权了。所以,它必须得和访问令牌一起生成。
到这里,我们就解决了刷新令牌的颁发问题。
### 使用刷新令牌
说到刷新令牌的使用我们需要先明白一点。在OAuth 2.0规范中刷新令牌是一种特殊的授权许可类型是嵌入在授权码许可类型下的一种特殊许可类型。在授权服务的代码里当我们接收到这种授权许可请求的时候会先比较grant_type和 refresh_token的值然后做下一步处理。
这其中的流程主要包括如下两大步骤。
**第一步,接收刷新令牌请求,验证基本信息。**
此时请求中的grant_type值为refresh_token。
```
String grantType = request.getParameter(&quot;grant_type&quot;);
if(&quot;refresh_token&quot;.equals(grantType)){
}
```
和颁发访问令牌前的验证流程一样,这里我们也需要验证第三方软件是否存在。需要注意的是,这里需要同时验证刷新令牌是否存在,目的就是要保证传过来的刷新令牌的合法性。
```
String refresh_token = request.getParameter(&quot;refresh_token&quot;);
if(!refreshTokenMap.containsKey(refresh_token)){
//该refresh_token值不存在
}
```
另外,我们还需要验证刷新令牌是否属于该第三方软件。授权服务是将颁发的刷新令牌与第三方软件、当时的授权用户绑定在一起的,因此这里需要判断该刷新令牌的归属合法性。
```
String appStr = refreshTokenMap.get(&quot;refresh_token&quot;);
if(!appStr.startsWith(appId+&quot;|&quot;+&quot;USERTEST&quot;)){
//该refresh_token值不是颁发给该第三方软件的
}
```
**需要注意,一个刷新令牌被使用以后,授权服务需要将其废弃,并重新颁发一个刷新令牌。**
**第二步,重新生成访问令牌。**
生成访问令牌的处理流程,与颁发访问令牌环节的生成流程是一致的。授权服务会将新的访问令牌和新的刷新令牌,一起返回给第三方软件。这里就不再赘述了。
## 总结
今天的课马上又要结束了我和你讲了授权码许可类型下授权服务的工作原理。授权服务可以说是整个OAuth 2.0体系中的 “灵魂” 组件,任何一种许可类型都离不开它的支持,它也是最复杂的组件。
这是因为它将复杂性尽可能地“揽在了自己身上”才使得诸如小兔这样的第三方软件接入OAuth 2.0的时候更加便捷。那关于如何快速地接入OAuth 2.0我在第5讲中和你详细展开。
授权服务的步骤流程比较多,因此我把这节课配套的代码放到了[GitHub](https://github.com/xindongbook/oauth2-code/tree/master/src/com/oauth/ch03)上,可以帮助你更好地理解授权服务的流程。
总结来讲关于这一讲我希望你能记住以下3点。
1. 授权服务的核心就是,**先颁发授权码code值再颁发访问令牌access_token值**。
1. 在颁发访问令牌的**同时还会颁发刷新令牌refresh_token值这种机制可以在无须用户参与的情况下用于生成新的访问令牌**。正如我们讲到的小明使用小兔软件的例子,当访问令牌过期的时候,刷新令牌的存在可以大大提高小明使用小兔软件的体验。
1. 授权还要有授权范围,**不能让第三方软件获得比注册时权限范围还大的授权,也不能获得超出了用户授权的权限范围,始终确保最小权限安全原则。**比如,小明只为小兔软件授予了获取当天订单的权限,那么小兔软件就不能访问小明店铺里面的历史订单数据。
## 思考题
刷新令牌有过期时间吗,会一直有效吗?和我说说你的想法吧。
欢迎你在留言区分享你的观点,也欢迎你把今天的内容分享给其他朋友,我们一起交流。

View File

@@ -0,0 +1,166 @@
<audio id="audio" title="04 | 在OAuth 2.0中如何使用JWT结构化令牌" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/41/5b/419ee1b47a5225ffyybab770128ae65b.mp3"></audio>
你好,我是王新栋。
在上一讲,我们讲到了授权服务的核心就是**颁发访问令牌**而OAuth 2.0规范并没有约束访问令牌内容的生成规则,只要符合唯一性、不连续性、不可猜性就够了。这就意味着,我们可以灵活选择令牌的形式,既可以是没有内部结构且不包含任何信息含义的随机字符串,也可以是具有内部结构且包含有信息含义的字符串。
随机字符串这样的方式我就不再介绍了之前课程中我们生成令牌的方式都是默认一个随机字符串。而在结构化令牌这方面目前用得最多的就是JWT令牌了。
接下来我就要和你详细讲讲JWT是什么、原理是怎样的、优势是什么以及怎么使用同时我还会讲到令牌生命周期的问题。
## JWT结构化令牌
关于什么是JWT官方定义是这样描述的
>
JSON Web TokenJWT是一个开放标准RFC 7519它定义了一种紧凑的、自包含的方式用于作为JSON对象在各方之间安全地传输信息。
这个定义是不是很费解我们简单理解下JWT就是用一种结构化封装的方式来生成token的技术。结构化后的token可以被赋予非常丰富的含义这也是它与原先毫无意义的、随机的字符串形式token的最大区别。
结构化之后,令牌本身就可以被“塞进”一些有用的信息,比如小明为小兔软件进行了授权的信息、授权的范围信息等。或者,你可以形象地将其理解为这是一种“自编码”的能力,而这些恰恰是无结构化令牌所不具备的。
JWT这种结构化体可以分为HEADER头部、PAYLOAD数据体和SIGNATURE签名三部分。经过签名之后的JWT的整体结构是被**句点符号**分割的三段内容,结构为 header.payload.signature 。比如下面这个示例:
```
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.
eyJzdWIiOiJVU0VSVEVTVCIsImV4cCI6MTU4NDEwNTc5MDcwMywiaWF0IjoxNTg0MTA1OTQ4MzcyfQ.
1HbleXbvJ_2SW8ry30cXOBGR9FW4oSWBd3PWaWKsEXE
```
**注意JWT内部没有换行这里只是为了展示方便才将其用三行来表示。**
你可能会说这个JWT令牌看起来也是毫无意义的、随机的字符串啊。确实你直接去看这个字符串是没啥意义但如果你把它拷贝到[https://jwt.io/](https://jwt.io/) 网站的在线校验工具中,就可以看到解码之后的数据:
<img src="https://static001.geekbang.org/resource/image/aa/80/aa855e4fd4b15f2f5262e7a7f5af3080.png" alt="" title="图1 由在线校验工具解码后的JWT令牌">
再看解码后的数据你是不是发现它跟随机的字符串不一样了呢。很显然现在呈现出来的就是结构化的内容了。接下来我就具体和你说说JWT的这三部分。
**HEADER**表示装载令牌类型和算法等信息是JWT的头部。其中typ 表示第二部分PAYLOAD是JWT类型alg 表示使用HS256对称签名的算法。
**PAYLOAD**表示是JWT的数据体代表了一组数据。其中sub令牌的主体一般设为资源拥有者的唯一标识、exp令牌的过期时间戳、iat令牌颁发的时间戳是JWT规范性的声明代表的是常规性操作。更多的通用声明你可以参考[RFC 7519开放标准](https://tools.ietf.org/html/rfc7519)。不过在一个JWT内可以包含一切合法的JSON格式的数据也就是说PAYLOAD表示的一组数据允许我们自定义声明。
**SIGNATURE**表示对JWT信息的签名。那么它有什么作用呢我们可能认为有了HEADER和PAYLOAD两部分内容后就可以让令牌携带信息了似乎就可以在网络中传输了但是在网络中传输这样的信息体是不安全的因为你在“裸奔”啊。所以我们还需要对其进行加密签名处理而SIGNATURE就是对信息的签名结果当受保护资源接收到第三方软件的签名后需要验证令牌的签名是否合法。
现在我们知道了JWT的结构以及每部分的含义那么具体到OAuth 2.0的授权流程中JWT令牌是如何被使用的呢在讲如何使用之前呢我先和你说说“令牌内检”。
## 令牌内检
什么是令牌内检呢?授权服务颁发令牌,受保护资源服务就要验证令牌。同时呢,授权服务和受保护资源服务,它俩是“一伙的”,还记得我之前在[第2课](https://time.geekbang.org/column/article/256196)讲过的吧。受保护资源来调用授权服务提供的检验令牌的服务,**我们把这种校验令牌的方式称为令牌内检。**
有时候授权服务依赖一个数据库,然后受保护资源服务也依赖这个数据库,也就是我们说的“共享数据库”。不过,在如今已经成熟的分布式以及微服务的环境下,不同的系统之间是依靠**服务**而**不是数据库**来通信了比如授权服务给受保护资源服务提供一个RPC服务。如下图所示。
<img src="https://static001.geekbang.org/resource/image/96/bf/963bb5dfc504c700fce24c8aac0dd2bf.png" alt="" title="图2 授权服务提供接口服务,供受保护资源校验令牌">
那么在有了JWT令牌之后我们就多了一种选择因为JWT令牌本身就包含了之前所要依赖数据库或者依赖RPC服务才能拿到的信息比如我上面提到的哪个用户为哪个软件进行了授权等信息。
接下来就让我们看看有了JWT令牌之后整体的内检流程会变成什么样子。
## JWT是如何被使用的
有了JWT令牌之后的通信方式就如下面的图3所展示的那样了**授权服务“扔出”一个令牌受保护资源服务“接住”这个令牌然后自己开始解析令牌本身所包含的信息就可以了而不需要再去查询数据库或者请求RPC服务**。这样也实现了我们上面说的令牌内检。
<img src="https://static001.geekbang.org/resource/image/1a/39/1a4cf53349aeb5d588e27c608e06d539.png" alt="" title="图3 受保护资源服务可直接解析JWT令牌">
在上面这幅图中呢为了更能突出JWT令牌的位置我简化了逻辑关系。实际上授权服务颁发了JWT令牌后给到了小兔软件小兔软件拿着JWT令牌来请求受保护资源服务也就是小明在京东店铺的订单。很显然JWT令牌需要在公网上做传输。所以在传输过程中JWT令牌需要进行Base64编码以防止乱码同时还需要进行签名及加密处理来防止数据信息泄露。
如果是我们自己处理这些编码、加密等工作的话就会增加额外的编码负担。好在我们可以借助一些开源的工具来帮助我们处理这些工作。比如我在下面的Demo中给出了开源JJWTJava JWT的使用方法。
JJWT是目前Java开源的、比较方便的JWT工具封装了Base64URL编码和对称HMAC、非对称RSA的一系列签名算法。使用JJWT我们只关注上层的业务逻辑实现而无需关注编解码和签名算法的具体实现这类开源工具可以做到“开箱即用”。
这个Demo的代码如下使用JJWT可以很方便地生成一个经过签名的JWT令牌以及解析一个JWT令牌。
```
String sharedTokenSecret=&quot;hellooauthhellooauthhellooauthhellooauth&quot;;//密钥
Key key = new SecretKeySpec(sharedTokenSecret.getBytes(),
SignatureAlgorithm.HS256.getJcaName());
//生成JWT令牌
String jwts=
Jwts.builder().setHeaderParams(headerMap).setClaims(payloadMap).signWith(key,SignatureAlgorithm.HS256).compact()
//解析JWT令牌
Jws&lt;Claims&gt; claimsJws =Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(jwts);
JwsHeader header = claimsJws.getHeader();
Claims body = claimsJws.getBody();
```
使用JJWT解析JWT令牌时包含了验证签名的动作如果签名不正确就会抛出异常信息。我们可以借助这一点来对签名做校验从而判断是否是一个没有被伪造过的、合法的JWT令牌。
异常信息,一般是如下的样子:
```
JWT signature does not match locally computed signature. JWT validity cannot be asserted and should not be trusted.
```
以上就是借助开源工具将JWT令牌应用到授权服务流程中的方法了。到这里你是不是一直都有一个疑问为什么要绕这么大一个弯子使用JWT而不是使用没有啥内部结构也不包含任何信息的随机字符串呢JWT到底有什么好处
## 为什么要使用JWT令牌
别急我这就和你总结下使用JWT格式令牌的三大好处。
第一,**JWT的核心思想就是用计算代替存储有些 “时间换空间” 的 “味道”**。当然,这种经过计算并结构化封装的方式,也减少了“共享数据库” 因远程调用而带来的网络传输消耗,所以也有可能是节省时间的。
第二也是一个重要特性是加密。因为JWT令牌内部已经包含了重要的信息所以在整个传输过程中都必须被要求是密文传输的**这样被强制要求了加密也就保障了传输过程中的安全性**。这里的加密算法,既可以是对称加密,也可以是非对称加密。
第三,**使用JWT格式的令牌有助于增强系统的可用性和可伸缩性**。这一点要怎么理解呢我们前面讲到了这种JWT格式的令牌通过“自编码”的方式包含了身份验证需要的信息不再需要服务端进行额外的存储所以每次的请求都是无状态会话。这就符合了我们尽可能遵循无状态架构设计的原则也就是增强了系统的可用性和伸缩性。
万物皆有两面性JWT令牌也有缺点。
JWT格式令牌的最大问题在于 “覆水难收”,也就是说,没办法在使用过程中修改令牌状态。我们还是借助小明使用小兔软件例子,先停下来想一下。
小明在使用小兔软件的时候,是不是有可能因为某种原因修改了在京东的密码,或者是不是有可能突然取消了给小兔的授权?这时候,令牌的状态是不是就要有相应的变更,将原来对应的令牌置为无效。
使用JWT格式令牌时每次颁发的令牌都不会在服务端存储这样我们要改变令牌状态的时候就无能为力了。因为服务端并没有存储这个JWT格式的令牌。这就意味着JWT令牌在有效期内是可以“横行无止”的。
为了解决这个问题我们可以把JWT令牌存储到远程的分布式内存数据库中吗显然不能因为这会违背JWT的初衷将信息通过结构化的方式存入令牌本身。因此我们通常会有两种做法
- 一是将每次生成JWT令牌时的秘钥粒度缩小到用户级别也就是一个用户一个秘钥。这样当用户取消授权或者修改密码后就可以让这个密钥一起修改。一般情况下这种方案需要配套一个单独的密钥管理服务。
- 二是在不提供用户主动取消授权的环境里面如果只考虑到修改密码的情况那么我们就可以把用户密码作为JWT的密钥。当然这也是用户粒度级别的。这样一来用户修改密码也就相当于修改了密钥。
## 令牌的生命周期
我刚才讲了JWT令牌有效期的问题讲到了它的失效处理另外咱们在[第3讲](https://time.geekbang.org/column/article/257101)中提到,授权服务颁发访问令牌的时候,都会设置一个过期时间,其实这都属于令牌的生命周期的管理问题。接下来,我便向你讲一讲令牌的生命周期。
万物皆有周期这是自然规律令牌也不例外无论是JWT结构化令牌还是普通的令牌。它们都有有效期只不过JWT令牌可以把有效期的信息存储在本身的结构体中。
具体到OAuth 2.0的令牌生命周期,通常会有三种情况。
第一种情况是令牌的自然过期过程,这也是最常见的情况。这个过程是,从授权服务创建一个令牌开始,到第三方软件使用令牌,再到受保护资源服务验证令牌,最后再到令牌失效。同时,这个过程也不排除主动销毁令牌的事情发生,比如令牌被泄露,授权服务可以做主让令牌失效。
生命周期的第二种情况,也就是上一讲提到的,访问令牌失效之后可以使用刷新令牌请求新的访问令牌来代替失效的访问令牌,以提升用户使用第三方软件的体验。
生命周期的第三种情况,就是让第三方软件比如小兔,主动发起令牌失效的请求,然后授权服务收到请求之后让令牌立即失效。我们来想一下,什么情况下会需要这种机制,也就是想一下第三方软件这样做的 “动机”,毕竟一般情况下 “我们很难放弃已经拥有的事物”。
比如有些时候用户和第三方软件之间存在一种订购关系比如小明购买了小兔软件那么在订购时长到期或者退订且小明授权的token还没有到期的情况下就需要有这样的一种令牌撤回协议来支持小兔软件主动发起令牌失效的请求。作为平台一方比如京东商家开放平台也建议有责任的第三方软件比如小兔软件遵守这样的一种令牌撤回协议。
我将以上三种情况整理成了一份序列图,以便帮助你理解。同时,为了突出令牌,我将访问令牌和刷新令牌,特意用深颜色标识出来,并单独作为两个角色放进了整个序列图中。
<img src="https://static001.geekbang.org/resource/image/bc/65/bc5fde2c813d41c60d863e2919b65565.png" alt="" title="图4 令牌生命周期">
## 总结
OAuth 2.0 的核心是授权服务,更进一步讲是令牌,**没有令牌就没有OAuth**令牌表示的是授权行为之后的结果。
一般情况下令牌对第三方软件来说是一个随机的字符串,是不透明的。大部分情况下,我们提及的令牌,都是一个无意义的字符串。
但是人们“不甘于”这样的满足于是开始探索有没有其他生成令牌的方式也就有了JWT令牌这样一来既不需要通过共享数据库也不需要通过授权服务提供接口的方式来做令牌校验了。这就相当于通过JWT这种结构化的方式我们在做令牌校验的时候多了一种选择。
通过这一讲呢,我希望你能记住以下几点内容:
1. 我们有了新的令牌生成方式的选择这就是JWT令牌。这是一种结构化、信息化令牌**结构化可以组织用户的授权信息,信息化就是令牌本身包含了授权信息**。
1. 虽然我们这讲的重点是JWT令牌但是呢不论是结构化的令牌还是非结构化的令牌对于第三方软件来讲它都不关心因为**令牌在OAuth 2.0系统中对于第三方软件都是不透明的**。需要关心令牌的,是授权服务和受保护资源服务。
1. 我们需要注意JWT令牌的失效问题。我们使用了JWT令牌之后远程的服务端上面是不存储的因为不再有这个必要JWT令牌本身就包含了信息。那么如何来控制它的有效性问题呢本讲中我给出了两种建议**一种是建立一个秘钥管理系统,将生成秘钥的粒度缩小到用户级别,另外一种是直接将用户密码当作密钥。**
现在你已经对JWT有了更深刻的认识也知道如何来使用它了。当你构建并生成令牌的时候除了使用随机的、“任性的”字符串还可以采用这样的结构化的令牌以便在令牌校验的时候能解析出令牌的内容信息直接进行校验处理。
我把今天用到的代码放到了GitHub上你可以点击[这个链接](https://github.com/xindongbook/oauth2-code/tree/master/src/com/oauth/ch04)查看。
## 思考题
你还知道有哪些场景适合JWT令牌又有哪些场景不适合JWT令牌吗
欢迎你在留言区分享你的观点,也欢迎你把今天的内容分享给其他朋友,我们一起交流。

View File

@@ -0,0 +1,209 @@
<audio id="audio" title="05 | 如何安全、快速地接入OAuth 2.0" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/4d/ac/4db602aa5f4abc2a409a5a8af58a17ac.mp3"></audio>
你好,我是王新栋。
在[第3讲](https://time.geekbang.org/column/article/257101),我已经讲了授权服务的流程,如果你还记得的话,当时我特意强调了一点,就是**授权服务将OAuth 2.0的复杂性都揽在了自己身上**这也是授权服务为什么是OAuth 2.0体系的核心的原因之一。
虽然授权服务做了大部分工作但是呢在OAuth 2.0的体系里面除了资源拥有者是作为用户参与还有另外两个系统角色也就是第三方软件和受保护资源服务。那么今天这一讲我们就站在这两个角色的角度看看它们应该做哪些工作才能接入到OAuth 2.0的体系里面呢?
现在,就让我们来看看,作为第三方软件的小兔和京东的受保护资源服务,具体需要着重处理哪些工作吧。
>
注:另外说明一点,为了脱敏的需要,在下面的讲述中,我只是把京东商家开放平台作为一个角色使用,以便有场景感,来帮助你理解。
## 构建第三方软件应用
我们先来思考一下:如果要基于京东商家开放平台构建一个小兔打单软件的应用,小兔软件的研发人员应该做哪些工作?
是不是要到京东商家开放平台申请注册为开发者,在成为开发者以后再创建一个应用,之后我们就开始开发了,对吧?没错,一定是这样的流程。那么,开发第三方软件应用的过程中,我们需要重点关注哪些内容呢?
我先来和你总结下这些内容包括4部分分别是**注册信息、引导授权、使用访问令牌、使用刷新令牌。**
<img src="https://static001.geekbang.org/resource/image/ee/78/ee18ea7aab4fbee26cf23d7613801078.png" alt="" title="图1 开发第三方软件应用,应该关注的内容">
第一点,注册信息。
首先小兔软件只有先有了身份才可以参与到OAuth 2.0的流程中去。也就是说小兔软件需要先拥有自己的app_id和app_serect等信息同时还要填写自己的回调地址redirect_uri、申请权限等信息。
这种方式的注册呢,我们有时候也称它为**静态注册**,也就是小兔软件的研发人员提前登录到京东商家开放平台进行手动注册,以便后续使用这些注册的相关信息来请求访问令牌。
第二点,引导授权。
当用户需要使用第三方软件,来操作其在受保护资源上的数据,就需要第三方软件来引导授权。比如,小明要使用小兔打单软件来对店铺里面的订单发货打印,那小明首先访问的一定是小兔软件(原则上是直接访问第三方软件,不过我们在后面讲到服务市场这种场景的时候,会有稍微不同),不会是授权服务,更不会是受保护资源服务。
但是呢,小兔软件需要小明的授权,只有授权服务才能允许小明这样做。所以呢,小兔软件需要 “配合” 小明做的第一件事儿,就是将小明引导至授权服务,如下面代码所示。
那去做什么呢?其实就是让用户为第三方软件授权,得到了授权之后,第三方软件才可以代表用户去访问数据。也就是说,小兔打单软件获得授权之后,才能够代表小明处理其在京东店铺上的订单数据。
```
String oauthUrl = &quot;http://localhost:8081/OauthServlet-ch03?reqType=oauth&quot;;
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 ParameterURI查询参数
```
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的方式来传递令牌。
但是,**我建议你采用表单提交也就是POST的方式来提交令牌**类似如下代码所示。原因是这样的从官方的建议中也可以看出它指的是在接入OAuth 2.0之前如果你已经采用了JSON数据格式请求体的情况下不建议使用表单提交。但是刚开始的时候只要三方软件和平台之间约束好了大家一致采用表单提交就没有任何问题了。**因为表单提交的方式在保证安全传输的同时还不需要去额外处理Authorization头部信息**。
```
String protectedURl=&quot;http://localhost:8082/ProtectedServlet-ch03&quot;;
Map&lt;String, String&gt; paramsMap = new HashMap&lt;String, String&gt;();
paramsMap.put(&quot;app_id&quot;,&quot;APPID_RABBIT&quot;);
paramsMap.put(&quot;app_secret&quot;,&quot;APPSECRET_RABBIT&quot;);
paramsMap.put(&quot;token&quot;,accessToken);
String result = HttpURLClient.doPost(protectedURl,HttpURLClient.mapToStr(paramsMap));
```
第四点,使用刷新令牌。
我在讲授权服务的时候提到过,如果访问令牌过期了,小兔软件总不能立马提示并让小明重新授权一次,否则小明的体验将会非常不好。为了解决这个问题呢,就用到了刷新令牌。
使用刷新令牌的方式跟使用访问令牌是一样的,具体可以参照上面我们讲的访问令牌的方式。关于刷新令牌的使用,你最需要关心的是,什么时候你会来决定使用刷新令牌。
在小兔打单软件收到访问令牌的同时也会收到访问令牌的过期时间expires_in。一个设计良好的第三方应用**应该将expires_in值保存下来并定时检测**如果发现expires_in即将过期则需要利用refresh_token去重新请求授权服务以便获取新的、有效的访问令牌。
这种定时检测的方法可以提前发现访问令牌是否即将过期。此外还有一种方法是“现场”发现。也就是说比如小兔软件访问小明店铺订单的时候突然收到一个访问令牌失效的响应此时小兔软件立即使用refresh_token来请求一个访问令牌以便继续代表小明使用他的数据。
综合来看的话,定时检测的方式,需要我们额外开发一个定时任务;而“现场”发现,就没有这种额外的工作量啦。具体采用哪一种方式,你可以结合自己的实际情况。不过呢,我还是建议你采用定时检测这种方式,因为它可以带来“提前量”,以便让我们有更好的主动性,而现场发现就有点被动了。
说到这里,我要再次提醒你注意的是,**刷新令牌是一次性的,使用之后就会失效**,但是它的有效期会比访问令牌要长。这个时候我们可能会想到,如果刷新令牌也过期了怎么办?在这种情况下,我们就需要将刷新令牌和访问令牌都放弃,相当于回到了系统的初始状态,只能让用户小明重新授权了。
到这里,我们来总结下,在构建第三方应用时,你需要重点关注的就是注册、授权、访问令牌、刷新令牌。只要你掌握了这四部分内容,在类似京东这样的开放平台上开发小兔软件,就不再是什么困难的事情了。
## 服务市场中的第三方应用软件
在构建第三方应用的引导授权时,我们说用户第一次“触摸”到的一定是第三方软件,但这并不是绝对的。这个不绝对,就发生在服务市场这样的场景里。
那什么是服务市场呢?说白了,就是你开发的软件,比如小兔打单软件、店铺装修软件等,都发布到这样一个“市场”里面售卖。这样,当用户购买了这些软件之后,就可以在服务市场里面看到有个“立即使用”的按钮。点击这个按钮,用户就可以直接访问自己购买的第三方软件了。
比如,京东的京麦服务市场里有个“我的服务”目录,里面就存放了我购买的打单软件。小明就可以直接点击“立即使用”,继而进入小兔打单软件,如下图所示。
<img src="https://static001.geekbang.org/resource/image/14/15/140a4efb622e21b21fcc4ff57653a915.png" alt="" title="图2 京麦服务市场“我的服务”">
那么这里需要注意的是作为第三方开发者来构建第三方软件的时候在授权码环节除了要接收授权码code值之外还要接收用户的订购相关信息比如服务的版本号、服务代码标识等信息。
好了,以上就是关于构建第三方软件时需要注意的一些细节问题了。接下来,我们再谈谈构建受保护资源服务的时候,又需要重点处理哪些工作呢。
## 构建受保护资源服务
你先想一想实际上在整个开放授权的环境中受保护资源最终指的还是Web API比如说访问头像的API、访问昵称的API。对应到我们的打单软件中受保护资源就是订单查询API、批量查询API等。
在互联网上的系统之间的通信基本都是以Web API为载体的形式进行。因此呢当我们说受保护资源被授权服务保护着时实际上说的是授权服务最终保护的是这些Web API。我们在构建受保护资源服务的时候除了基本的要检查令牌的合法性还需要做些什么呢我认为**最重要的就是权限范围了。**
在我们处理受保护资源服务中的逻辑的时候,校验权限的处理会占据很大的比重。你想啊,访问令牌递过来,你肯定要多看看令牌到底能操作哪些功能、又能访问哪些数据吧。现在,我们把这些权限的类别总结归纳下来,最常见的大概有以下几类。
<img src="https://static001.geekbang.org/resource/image/e7/f6/e7b134686b9f2e824ffa8410e20f59f6.jpg" alt="" title="图3 3类权限类别">
接下来,我和你具体说说这些权限是如何使用的。
1. 不同的权限对应不同的操作。
这里的操作其实对应的是Web API比如目前京东商家开放平台提供有查询商品API、新增商品API、删除商品API这三种。如果小兔软件请求过来的一个访问令牌access_token的scope权限范围只对应了查询商品API、新增商品API那么包含这个access_token值的请求就不能执行删除商品API的操作。
```
//不同的权限对应不同的操作
String[] scope = OauthServlet.tokenScopeMap.get(accessToken);
StringBuffer sbuf = new StringBuffer();
for(int i=0;i&lt;scope.length;i++){
sbuf.append(scope[i]).append(&quot;|&quot;);
}
if(sbuf.toString().indexOf(&quot;query&quot;)&gt;0){
queryGoods(&quot;&quot;);
}
if(sbuf.toString().indexOf(&quot;add&quot;)&gt;0){
addGoods(&quot;&quot;);
}
if(sbuf.toString().indexOf(&quot;del&quot;)&gt;0){
delGoods(&quot;&quot;);
}
```
1. 不同的权限对应不同的数据。
这里的数据就是指某一个API里包含的属性字段信息。比如有一个查询小明信息的API返回的信息包括Contactemail、phone、qq、LikeBasketball、Swimming、Personal Datasex、age、nickname。如果小兔软件请求过来的一个访问令牌access_token的scope权限范围只对应了Personal Data那么包含该access_token值的请求就不能获取到Contact和Like的信息关于这部分的代码实际跟不同权限对应不同操作的代码类似。
看到这里,你就明白了,这种权限范围的粒度要比“不同的权限对应不同的操作”的粒度要小。这正是遵循了最小权限范围原则。
1. 不同的用户对应不同的数据。
这种权限是什么意思呢?其实,这种权限实际上只是换了一种维度,将其定位到了用户上面。
一些基础类信息比如获取地理位置、获取天气预报等不会带有用户归属属性也就是说这些信息并不归属于某个用户是一类公有信息。对于这样的信息平台提供出去的API接口都是“中性”的没有用户属性。
但是更多的场景却是基于用户属性的。还是以小兔打单软件为例商家每次打印物流面单的时候小兔打单软件都要知道是哪个商家的订单。这种情况下商家为小兔软件授权小兔软件获取的access_token实际上就包含了商家这个用户属性。
京东商家开放平台的受保护资源服务每次接收到小兔软件的请求时都会根据该请求中access_token的值找到对应的商家ID继而根据商家ID查询到商家的订单信息也就是不同的商家对应不同的订单数据。
```
//不同的用户对应不同的数据
String user = OauthServlet.tokenMap.get(accessToken);
queryOrders(user);
```
在上面讲三种权限的时候,我举的例子实际上都属于一个系统提供了查询、添加、删除这样的所有服务。此时你可能会想到,现在的系统不已经是分布式系统环境了么,如果有很多个受保护资源服务,比如提供用户信息查询的用户资源服务、提供商品查询的商品资源服务、提供订单查询的订单资源服务,那么每个受保护资源服务岂不是都要把上述的权限范围校验执行一遍吗,这样不就会有大量的重复工作产生么?
在这里我特别高兴你能想到这一点。为了应对这种情况我们应该有一个统一的网关层来处理这样的校验所有的请求都会经过API GATEWAY 跳转到不同的受保护资源服务。这样呢我们就不需要在每一个受保护资源服务上都做一遍权限校验的工作了而只需要在API GATEWAY 这一层做权限校验就可以了。系统结构如下图所示。
<img src="https://static001.geekbang.org/resource/image/a5/b0/a5175438e76411c808dd5e72d3d3dbb0.png" alt="" title="图4 由统一的网关层处理权限校验">
## 总结
截止到这一讲呢我们已经把OAuth 2.0 中授权码相关的流程所涉及到的内容都讲完了。通过02到05这4讲你可以很清晰地理解授权码流程的核心原理了也可以弄清楚如何使用以及如何接入这一授权流程了。
我在本讲开始的时候提到OAuth 2.0的复杂性实际上都给了授权服务来承担接着我从第三方软件和受保护资源的角度分别介绍了这两部分系统在接入OAuth 2.0的时候应该注意哪些方面。总结下来,我其实希望你能够记住以下两点。
1. 对于第三方软件,比如小兔打单软件来讲,**它的主要目的就是获取访问令牌,使用访问令牌**这当然也是整个OAuth 2.0的目的,就是让第三方软件来做这两件事。在这个过程中需要强调的是,第三方软件在使用访问令牌的时候有三种方式,我们建议在平台和第三方软件约定好的前提下,**优先采用Post表单提交的方式**。
1. 受保护资源系统,比如小兔软件要访问开放平台的订单数据服务,它需要注意的是权限的问题,这个权限范围主要包括,**不同的权限会有不同的操作,不同的权限也会对应不同的数据,不同的用户也会对应不同的数据**。
## 思考题
如果使用刷新令牌refresh_token请求回来一个新的访问令牌access_token按照一般规则授权服务上旧的访问令牌应该要立即失效但是如果在这之前已经有使用旧的访问令牌发出去的请求不就受到影响了吗这种情况下应该如何处理呢
欢迎你在留言区分享你的观点,也欢迎你把今天的内容分享给其他朋友,我们一起交流。

View File

@@ -0,0 +1,212 @@
<audio id="audio" title="06 | 除了授权码许可类型OAuth 2.0还支持什么授权流程?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/27/1a/275fa94b440c440cef067b204e58e71a.mp3"></audio>
你好,我是王新栋。
在前面几讲学习授权码许可类型的原理与工作流程时,不知道你是不是一直有这样一个疑问:授权码许可的流程最完备、最安全没错儿,但它适合所有的授权场景吗?在有些场景下使用授权码许可授权,是不是过于复杂了,是不是根本就没必要这样?
比如,小兔打单软件是京东官方开发的一款软件,那么小明在使用小兔的时候,还需要小兔再走一遍授权码许可类型的流程吗?估计你也猜到答案了,肯定是不需要了。
你还记得授权码许可流程的特点么?它通过授权码这种临时的中间值,让小明这样的用户参与进来,从而让小兔软件和京东之间建立联系,进而让小兔代表小明去访问他在京东店铺的订单数据。
现在小兔被“招安”了是京东自家的了是被京东充分信任的没有“第三方软件”的概念了。同时小明也是京东店铺的商家也就是说软件和用户都是京东的资产。这时显然没有必要再使用授权码许可类型进行授权了。但是呢小兔依然要通过互联网访问订单数据的Web API来提供为小明打单的功能。
于是为了保护这些场景下的Web API又为了让 OAuth 2.0 更好地适应现实世界的更多场景来解决比如上述小兔软件这样的案例OAuth 2.0体系中还提供了资源拥有者凭据许可类型。
## 资源拥有者凭据许可
从“资源拥有者凭据许可”这个命名上你可能就已经理解它的含义了。没错资源拥有者的凭据就是用户的凭据就是用户名和密码。可见这是最糟糕的一种方式。那为什么OAuth 2.0还支持这种许可类型而且编入了OAuth 2.0的规范呢?
我们先来思考一下。正如上面我提到的,小兔此时就是京东官方出品的一款软件,小明也是京东的用户,那么小明其实是可以使用用户名和密码来直接使用小兔这款软件的。原因很简单,那就是这里不再有“第三方”的概念了。
但是呢如果每次小兔都是拿着小明的用户名和密码来通过调用Web API的方式来访问小明店铺的订单数据甚至还有商品信息等在调用这么多API的情况下无疑增加了用户名和密码等敏感信息的攻击面。
如果是使用了token来代替这些“满天飞”的敏感信息不就能很大程度上保护敏感信息数据了吗这样小兔软件只需要使用一次用户名和密码数据来换回一个token进而通过token来访问小明店铺的数据以后就不会再使用用户名和密码了。
接下来,我们一起看下这种许可类型的流程,如下图所示:
<img src="https://static001.geekbang.org/resource/image/cd/e9/cd596cfd73a42449a39342f951c5cce9.png" alt="" title="图1 资源拥有者凭据许可类型的流程">
步骤1当用户访问第三方软件小兔时会提示输入用户名和密码。索要用户名和密码就是资源拥有者凭据许可类型的特点。
步骤2**这里的grant_type的值为password**,告诉授权服务使用资源拥有者凭据许可凭据的方式去请求访问。
```
Map&lt;String, String&gt; params = new HashMap&lt;String, String&gt;();
params.put(&quot;grant_type&quot;,&quot;password&quot;);
params.put(&quot;app_id&quot;,&quot;APPIDTEST&quot;);
params.put(&quot;app_secret&quot;,&quot;APPSECRETTEST&quot;);
params.put(&quot;name&quot;,&quot;NAMETEST&quot;);
params.put(&quot;password&quot;,&quot;PASSWORDTEST&quot;);
String accessToken = HttpURLClient.doPost(oauthURl,HttpURLClient.mapToStr(params));
```
步骤3授权服务在验证用户名和密码之后生成access_token的值并返回给第三方软件。
```
if(&quot;password&quot;.equals(grantType)){
String appSecret = request.getParameter(&quot;app_secret&quot;);
String username = request.getParameter(&quot;username&quot;);
String password = request.getParameter(&quot;password&quot;);
if(!&quot;APPSECRETTEST&quot;.equals(appSecret)){
response.getWriter().write(&quot;app_secret is not available&quot;);
return;
}
if(!&quot;USERNAMETEST&quot;.equals(username)){
response.getWriter().write(&quot;username is not available&quot;);
return;
}
if(!&quot;PASSWORDTEST&quot;.equals(password)){
response.getWriter().write(&quot;password is not available&quot;);
return;
}
String accessToken = generateAccessToken(appId,&quot;USERTEST&quot;);//生成访问令牌access_token的值
response.getWriter().write(accessToken);
}
```
到了这里你可以掌握到一个信息如果软件是官方出品的又要使用OAuth 2.0来保护我们的Web API那么你就可以使用小兔软件的做法采用资源拥有者凭据许可类型。
无论是我们的架构、系统还是框架都是致力于解决现实生产中的各种问题的。除了资源拥有者凭据许可类型外OAuth 2.0 体系针对现实的环境还提供了客户端凭据许可和隐式许可类型。接下来,让我们继续看看这两种授权许可类型吧。
## 客户端凭据许可
如果没有明确的资源拥有者换句话说就是小兔软件访问了一个不需要用户小明授权的数据比如获取京东LOGO的图片地址这个LOGO信息不属于任何一个第三方用户再比如其它类型的第三方软件来访问平台提供的省份信息省份信息也不属于任何一个第三方用户。
此时,在授权流程中,就不再需要资源拥有者这个角色了。当然了,**你也可以形象地理解为 “资源拥有者被塞进了第三方软件中” 或者 “第三方软件就是资源拥有者”**。这种场景下的授权便是客户端凭据许可第三方软件可以直接使用注册时的app_id和app_secret来换回访问令牌token的值。
我们还是以小明使用小兔软件为例,来看下客户端凭据许可的整个授权流程,如下图所示:
<img src="https://static001.geekbang.org/resource/image/cb/ff/cbc8cc1e03cb1d0a2f945ffd9dbb37ff.png" alt="" title="图2 客户端凭据许可授权流程">
另外一点呢因为授权过程没有了资源拥有者小明的参与小兔软件的后端服务可以随时发起access_token的请求所以这种授权许可也不需要刷新令牌。
这样一来,客户端凭据许可类型的关键流程,就是以下两大步。
步骤1第三方软件小兔通过后端服务向授权服务发送请求**这里grant_type的值为client_credentials**,告诉授权服务要使用第三方软件凭据的方式去请求访问。
```
Map&lt;String, String&gt; params = new HashMap&lt;String, String&gt;();
params.put(&quot;grant_type&quot;,&quot;client_credentials&quot;);
params.put(&quot;app_id&quot;,&quot;APPIDTEST&quot;);
params.put(&quot;app_secret&quot;,&quot;APPSECRETTEST&quot;);
String accessToken = HttpURLClient.doPost(oauthURl,HttpURLClient.mapToStr(params));
```
步骤2在验证app_id和app_secret的合法性之后生成access_token的值并返回。
```
String grantType = request.getParameter(&quot;grant_type&quot;);
String appId = request.getParameter(&quot;app_id&quot;);
if(!&quot;APPIDTEST&quot;.equals(appId)){
response.getWriter().write(&quot;app_id is not available&quot;);
return;
}
if(&quot;client_credentials&quot;.equals(grantType)){
String appSecret = request.getParameter(&quot;app_secret&quot;);
if(!&quot;APPSECRETTEST&quot;.equals(appSecret)){
response.getWriter().write(&quot;app_secret is not available&quot;);
return;
}
String accessToken = generateAccessToken(appId,&quot;USERTEST&quot;);//生成访问令牌access_token的值
response.getWriter().write(accessToken);
}
```
到这里,我们再小结下。在获取一种不属于任何一个第三方用户的数据时,并不需要类似小明这样的用户参与,此时便可以使用客户端凭据许可类型。
接下来,我们再一起看看今天要讲的最后一种授权许可类型,就是隐式许可类型。
## 隐式许可
让我们再想象一下如果小明使用的小兔打单软件应用没有后端服务就是在浏览器里面执行的比如纯粹的JavaScript应用应该如何使用OAuth 2.0呢?
其实,这种情况下的授权流程就可以使用隐式许可流程,可以理解为第三方软件小兔直接嵌入浏览器中了。
在这种情况下小兔软件对于浏览器就没有任何保密的数据可以隐藏了也不再需要应用密钥app_secret的值了也不用再通过授权码code来换取访问令牌access_token的值了。因为使用授权码的目的之一就是把浏览器和第三方软件的信息做一个隔离确保浏览器看不到第三方软件最重要的访问令牌access_token的值。
因此,**隐式许可授权流程的安全性会降低很多**。在授权流程中,没有服务端的小兔软件相当于是嵌入到了浏览器中,访问浏览器的过程相当于接触了小兔软件的全部,因此我用虚线框来表示小兔软件,整个授权流程如下图所示:
<img src="https://static001.geekbang.org/resource/image/c9/y0/c957860d09beb8777c59978f3b9e2yy0.png" alt="" title="图3 隐式许可授权流程">
接下来我使用Servlet的Get请求来模拟这个流程一起看看相关的示例代码。
步骤1用户通过浏览器访问第三方软件小兔。此时第三方软件小兔实际上是嵌入浏览器中执行的应用程序。
步骤2这个流程和授权码流程类似只是需要特别注意一点**response_type的值变成了token**是要告诉授权服务直接返回access_token的值。随着我们后续的讲解你会发现隐式许可流程是唯一在前端通信中要求返回access_token的流程。对就这么 “大胆”,但 “不安全”。
```
Map&lt;String, String&gt; params = new HashMap&lt;String, String&gt;();
params.put(&quot;response_type&quot;,&quot;token&quot;);//告诉授权服务直接返回access_token
params.put(&quot;redirect_uri&quot;,&quot;http://localhost:8080/AppServlet-ch02&quot;);
params.put(&quot;app_id&quot;,&quot;APPIDTEST&quot;);
String toOauthUrl = URLParamsUtil.appendParams(oauthUrl,params);//构造请求授权的URl
response.sendRedirect(toOauthUrl);
```
步骤3生成acccess_token的值通过前端通信返回给第三方软件小兔。
```
String responseType = request.getParameter(&quot;response_type&quot;);
String redirectUri =request.getParameter(&quot;redirect_uri&quot;);
String appId = request.getParameter(&quot;app_id&quot;);
if(!&quot;APPIDTEST&quot;.equals(appId)){
return;
}
if(&quot;token&quot;.equals(responseType)){
//隐式许可流程模拟DEMO CODE注意该流程全部在前端通信中完成
String accessToken = generateAccessToken(appId,&quot;USERTEST&quot;);//生成访问令牌access_token的值
Map&lt;String, String&gt; params = new HashMap&lt;String, String&gt;();
params.put(&quot;redirect_uri&quot;,redirectUri);
params.put(&quot;access_token&quot;,accessToken);
String toAppUrl = URLParamsUtil.appendParams(redirectUri,params);//构造第三方软件的回调地址,并重定向到该地址
response.sendRedirect(toAppUrl);//使用sendRedirect方式模拟前端通信
}
```
如果你的软件就是直接嵌入到了浏览器中运行而且还没有服务端的参与并且还想使用OAuth 2.0流程的话,也就是像上面我说的小兔这个例子,那么便可以直接使用隐式许可类型了。
## 如何选择?
现在我们已经理解了OAuth 2.0的4种授权许可类型的原理与流程。那么我们应该如何选择到底使用哪种授权许可类型呢
这里,我给你的建议是,在对接 OAuth 2.0 的时候先考虑授权码许可类型,其次再结合现实生产环境来选择:
- 如果小兔软件是官方出品,那么可以直接使用资源拥有者凭据许可;
- 如果小兔软件就是只嵌入到浏览器端的应用且没有服务端,那就只能选择隐式许可;
- 如果小兔软件获取的信息不属于任何一个第三方用户,那可以直接使用客户端凭据许可类型。
## 总结
好了我们马上要结束这篇文章了在这之前呢我们一直讲的是授权码许可类型你已经知道了这是一种流程最完备、安全性最高的授权许可流程。不过呢现实世界总是有各种各样的变化OAuth 2.0也要适应这样的变化,所以才有了我们今天讲的另外这三种许可类型。同时,关于如何来选择使用这些许可类型,我前面也给了大家一个建议。
加上前面我们讲的授权码许可类型我们一共讲了4种授权许可类型它们最显著的区别就是**获取访问令牌access_token的方式不同**。最后,我通过一张表格来对比下:
<img src="https://static001.geekbang.org/resource/image/3e/4d/3ee0ceff6c543157a51aae985756454d.jpg" alt="" title="图4 OAuth 2.0的4种授权许可类型对比">
除了上面这张表格所展现的4种授权许可类型的区别之外我希望你还能记住以下两点。
1. 所有的授权许可类型中,授权码许可类型的安全性是最高的。因此,只要具备使用授权码许可类型的条件,我们一定要首先授权码许可类型。
1. 所有的授权许可类型都是为了解决现实中的实际问题,因此我们还要结合实际的生产环境,在保障安全性的前提下选择最合适的授权许可类型,比如使用客户端凭据许可类型的小兔软件就是一个案例。
我把今天用到的代码放到了GitHub上你可以点击[这个链接](https://github.com/xindongbook/oauth2-code/tree/master/src/com/oauth/ch06)查看。
## 思考题
如果受限于应用特性所在的环境,比如在没有浏览器参与的情况下,我们应该如何选择授权许可类型呢,还可以使用授权码许可流程吗?
欢迎你在留言区分享你的观点,也欢迎你把今天的内容分享给其他朋友,我们一起交流。