OAuth 2.0
Auth 授权, O Open 开放, OAuth 开放授权
Last updated
Auth 授权, O Open 开放, OAuth 开放授权
Last updated
网关:承载整个开放平台的调用量,同时还要有足够的系统容错能力
授权:要支持更多的业务场景,授权同样重要
从 2007 年 OAuth 1.0 面世,到 2011 年发布 OAuth 2.0 草案,已经有很多关于 OAuth 的资料了。
《OAuth 2 in Action》 OAuth2.0 协议框架, OAuth 2.0 这个授权协议
OAuth 2.0 不再局限于一种授权机制,它扩充了授权许可机制类型,有了授权码许可机制、客户端凭据机制、资源拥有者凭据机制和隐式许可机制。这样的 OAuth 机制就能够很灵活地适应现实中的各种场景,比如移动应用的场景、官方应用的场景等。
4种授权许可类型:用好、用对
授权码许可(Authorization Code)类型的流程,包括 OAuth 2.0 内部组件之间的通信方式,以及授权服务、客户端(第三方软件)、受保护资源服务这三个组件的原理。
隐式许可(Implicit)
客户端凭据许可(Client Credentials)
资源拥有者凭据许可(Resource Owner Password Credentials)
问题,比如:
如何使用授权码流程
如何选择适合自己实际场景的授权类型
我要开发一个 Web 应用,当使用 OAuth 2.0 的时候,担心授权码被拦截
我要开发一款移动 App,当使用 OAuth 2.0 的时候,不确定是否需要 Server 端
高级部分:更安全地用、扩展地用 OAuth 2.0。
如何在移动 App 中使用 OAuth 2.0,因使用不当而导致的 OAuth 2.0 安全漏洞有哪些
如何利用 OAuth 2.0 实现一个 OpenID Connect 用户身份认证协议
此外,微服务技术领域的专家,分享一个架构案例,基于 OAuth 2.0/JWT 的微服务参考架构
开放平台:我收到了小明的授权,现在要给你生成一个授权码 code 值,我通过浏览器重定向到你的回调 URL 地址上面了。
第三方软件:好的,xxx开放平台。我现在从浏览器上拿到了授权码,现在就用这个授权码来请求你,请给我一个访问令牌 access_token 吧。
其中主要的动作,就是:生成授权码 –> 生成访问令牌 –> 使用访问令牌。
OAuth 2.0 授权的核心就是:颁发访问令牌、使用访问令牌(而且不管是哪种类型的授权流程都是这样)
OAuth 2.0 的核心是授权许可,更进一步说就是令牌机制。也就是说,第三方软件只有拿到了xxx颁发的访问令牌,也就是得到了授权许可,然后才可以代表用户访问他们的数据。
互联网中所有的受保护资源,几乎都是以 Web API 的形式来提供访问的,比如极客时间 App 要获取用户的头像、昵称、第三方软件要获取用户的店铺订单,我们说 OAuth 2.0 与安全相关,是用来保护 Web API 的。另外,第三方软件通过 OAuth 2.0 取得访问权限之后,用户便把这些权限委托给了第三方软件,我们说 OAuth 2.0 是一种委托协议,也没问题。
也正因为第三方软件每次都是用访问令牌而不是用户名和密码来请求用户的数据,才大大减少了安全风险上的“攻击面”。因为就不用每次都带着用户名和密码来访问数量众多的 Web API 了。因此,我们说 OAuth 2.0 的核心,就是颁发访问令牌和使用访问令牌。
OAuth 2.0 这种授权协议,就是保证第三方(软件)只有在获得授权之后,才可以进一步访问授权者的数据。因此,我们常常还会听到一种说法,OAuth 2.0 是一种安全协议。
在 OAuth 2.0 的体系里面有 4 种角色,按照官方的称呼它们分别是:资源拥有者、客户端/第三方软件、授权服务和受保护资源。
资源拥有者 -> 小明
第三方软件 -> xx软件
授权服务 -> xx授权服务
受保护资源 -> 小明店铺的订单
这 4 个角色是两两站队的:
资源拥有者和第三方软件:因为第三方软件要代表资源拥有者去访问受保护资源
授权服务和受保护资源:因为授权服务负责颁发访问令牌,受保护资源负责接收并验证访问令牌
授权码许可流程:两次重定向
第三方软件,把用户引导到授权服务上
授权服务,再把用户重定向回到第三方软件上,附带code值
为什么需要授权码?为什么要用授权码来换令牌?为什么不能直接颁发访问令牌呢?
为了重新建立起这样的一次连接,又不能让访问令牌暴露出去,就有了这样一个临时的、间接的凭证:授权码。因为第三方软件最终要拿到的是安全保密性要求极高的访问令牌,并不是授权码,而授权码是可以暴露在浏览器上面的。这样有了授权码的参与,访问令牌可以在后端服务之间传输,同时还可以重新建立小明与第三方软件之间的“连接”。这样通过一个授权码,既照顾到了小明的体验,又照顾了通信的安全。
倘若没有授权码:
如果这里直接返回访问令牌,那我们肯定不能使用重定向的方式。因为这样会把安全保密性要求极高的访问令牌暴露在浏览器上,从而将会面临访问令牌失窃的安全风险。显然这是不能被允许的
也就是说,如果没有授权码的话,我们就只能把访问令牌发送给第三方软件的后端服务。但这样小明就回不来第三方软件了
授权码许可类型的通信过程:在执行授权码流程的时候,授权码和访问令牌在第三方软件和授权服务之间到底是怎么流转的呢?
获取授权码:间接通信,第三方软件-浏览器-授权服务(第三方软件和授权服务之间并没有发生直接的通信-而是通过浏览器这个中间人来搭线的)
通过授权码换取访问令牌:直接通信。第三方软件获取到授权码 code 值后,向授权服务发起获取访问令牌 access_token 的通信请求。这个请求是第三方软件服务器跟授权服务的服务器之间的通信,都是在后端服务器之间的请求和响应,因此也叫作后端通信
其实,授权码许可流程,不一定要有浏览器的参与。因为 OAuth 2.0 实际上是一个授权理念,或者说是一种授权思维。它的授权码模式的思维可以移植到很多场景中,比如微信小程序。在开发微信小程序应用时,我们通过授权码模式获取用户登录信息,官方文档的地址示例中给出的 grant_type=authorization_code 就没有用到浏览器
GET https://api.weixin.qq.com/sns/jscode2session?appid=APPID&secret=SECRET&js_code=JSCODE&grant_type=authorization_code
这个过程并没有使用到浏览器,但确实按照授权码许可的思想走了一个完整的授权码许可流程:先通过小程序前端获取到 code 值,再通过小程序的后端服务使用 code 值换取 session_key 等信息,只不过是访问令牌 access_token 的值被换成了 session_key。
授权服务就是负责颁发访问令牌的服务。OAuth 2.0 的核心是授权服务,而授权服务的核心就是令牌。
授权的前提,需要第三方软件要去平台那里备案,也就是注册。注册成功了之后,xxx开放平台就会给第三方软件 app_id 和 app_secret 等信息,以方便后面授权时的各种身份校验。注册的时候,第三方软件也会请求受保护资源的可访问范围,即 scope(授权-就会涉及到授权范围)。
授权服务的工作,两大部分:颁发授权码 code、颁发访问令牌 access_token。
准备工作
1. 验证基本信息
验证:第三方软件是否合法、回调地址是否合法 因为在 Web 浏览器环境下,颁发 code 的整个请求过程,都是浏览器通过前端通信来完成——所有信息都有可能被冒充
2. 验证权限范围
对比:第三方软件传过来的 scope 参数,和第三方软件注册时申请的权限范围(这是第一次验证权限范围)
3. 生成授权请求页面
该页面是在授权服务上的
生成授权码 code
1. 验证权限范围
验证:小明进行授权之后的权限,和第三方软件注册的权限(因为这里有用户的输入)
2. 处理授权请求,生成授权码 code
授权服务会生成的授权码 code 值与 app_id、user 进行关系映射,并将它们之间的映射关系保存起来(以便在生成访问令牌 access_token 时使用)
3. 重定向至第三方软件
当第三方软件获取到授权码 code 值以后,就可以请求访问令牌 access_token 的值了
授权码 code 是临时的、一次性凭证。
OAuth 2.0 规范建议授权码 code 值有效期为 10 分钟,并且一个授权码 code 只能被使用一次。通常,在生产环境中 code 的有效期一般不会超过 5 分钟。同时,授权服务还需要将生成的授权码 code 跟已经授权的权限范围 rscope 进行绑定并存储,以便后续颁发访问令牌时,我们能够通过 code 值取出授权范围并与访问令牌绑定。因为第三方软件最终是通过访问令牌来请求受保护资源的。
rscope 和 scope 对应同一个权限。rscope 是受保护资源服务再次确认的权限,r 是 replay
生成授权码code + 绑定code和(app_id+user)的映射关系 + 设置code的有效期。
上面忽略了小明登录的过程,但只有用户登录了才可以对第三方软件进行授权,授权服务才能获得用户信息并最终生成 code 和 app_id(第三方软件的应用标识) + user(资源拥有者标识)之间的对应关系
一个授权码 code 表示某一个用户给某一个第三方软件进行授权。
授权码 code,只是一个换取访问令牌 access_token 的临时凭证。当第三方拿着授权码 code 来请求的时候,授权服务需要为之生成最终的请求访问令牌。
1. 验证第三方软件是否存在
2. 验证 code 值是否合法
code 是 app_id + user 的组合值。当确认过授权码 code 值有效以后,应该立刻从存储中删除当前的 code 值,以防止第三方软件恶意使用一个失窃的授权码 code 值来请求授权服务
3. 生成 access_token
关于按照什么规则来生成访问令牌 access_token 的值,OAuth 2.0 规范中并没有明确规定,但必须符合三个原则:唯一性、不连续性、不可猜性
将访问令牌 access_token 值存储起来,并将其与第三方软件的应用标识 app_id 和资源拥有者标识 user 进行关系映射。即一个访问令牌 access_token 表示某一个用户给某一个第三方软件进行授权
授权服务将授权范围 scope 跟访问令牌 access_token 做绑定
授权服务为该访问令牌设置一个过期时间 expires_in,比如 1 天
为什么要通过刷新令牌让第三方不断刷新 access_token 有效期,而不是直接给一个更长的有效期?
access token 在有效期内可以无限使用,而且可能被盗后用户或者第三方都不知晓,所以要尽可能短。但 refresh 只能用一次,造成危害小,而且被盗用后会被立即感知
例如如果 token 的格式是 jwt,服务端是无法主动让其失效的,我们能做的就是让 access_token 有效期尽量缩短,refresh_token可以稍微长一些。如果我们要让 token失效,只要 revoke refresh_token 让用户端不能再得到新的 access_token 就可以达到类似的目的
是为了安全性的考虑,access_token 的有效期不能太长
refresh_token 的有效期具体多长时间,同样在 OAuth 2.0 规范里面并没有给出确定值,一般比 access_token 的有效期长 1-7 天。
访问令牌 access_token 和刷新令牌 refresh_token,是一起生成一起返回的。一个刷新令牌被使用后,授权服务要将其废弃,并重新颁发一个刷新令牌。grant_type=refresh_token
若 access_token 未超时,那么进行 refresh_token 有两种方式:一种是不会改变 access_token,但超时时间会刷新,相当于续期 access_token;另一种是更换一个新的 access_token 值(推荐这种方式 ✅)
授权服务的核心就是:先颁发授权码 code 值,再颁发访问令牌 access_token 值
在颁发访问令牌 access_token 的同时还会颁发刷新令牌 refresh_token 值,这种机制即保证了安全,又可以在无须用户参与的情况下用于生成新的访问令牌
授权还要有授权范围 scope,不能让第三方软件获得比注册时权限范围还大的授权,也不能获得超出了用户授权的权限范围,始终确保最小权限安全原则
OAuth 2.0 规范并没有约束访问令牌内容的生成规则,只要符合唯一性、不连续性、不可猜性就够了。这就意味着,我们可以灵活选择令牌的形式,既可以是没有内部结构且不包含任何信息含义的随机字符串,也可以是具有内部结构且包含有信息含义的字符串。
在结构化令牌这方面,目前用得最多的就是 JWT 令牌了。
JSON Web Token(JWT)是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,用于作为 JSON 对象在各方之间安全地传输信息。
JWT 就是用一种结构化封装的方式来生成 token 的技术。结构化后的 token 可以被赋予非常丰富的含义,而这些恰恰是无结构化令牌所不具备的。
JWT 这种结构化体可以分为 HEADER(头部)、PAYLOAD(数据体)和 SIGNATURE(签名)三部分。经过签名之后的 JWT 的整体结构,是被句点符号分割的三段内容,结构为 header.payload.signature。
注意:JWT 内部没有换行,这里只是为了展示方便,才将其用三行来表示
虽然 JWT 看起来也是毫无意义的随机的字符串,但我们把它拷贝到 https://jwt.io/ 网站的在线校验工具中,就可以看到解码之后的数据:
HEADER
装载令牌类型和算法等信息
typ 表示第二部分 PAYLOAD 是 JWT 类型
alg 表示使用 HS256 对称签名的算法
PAYLOAD
JWT 的数据体(一组数据)
sub(令牌的主体,一般设为资源拥有者的唯一标识)
exp(令牌的过期时间戳)
iat(令牌颁发的时间戳)是 JWT 规范性的声明,代表的是常规性操作
更多通用声明,可参考 RFC 7519 开放标准
在一个 JWT 内可以包含一切合法的 JSON 格式的数据,也就是说,PAYLOAD 表示的一组数据允许我们自定义声明
SIGNATURE
对 JWT 信息的签名
为了不让以上两部分内容在网络上裸奔,还需要对其进行加密签名处理,而 SIGNATURE 就是对信息的签名结果,当受保护资源接收到第三方软件的签名后需要验证令牌的签名是否合法。
具体到 OAuth 2.0 的授权流程中,JWT 令牌是如何被使用的呢?
授权服务颁发令牌,受保护资源服务就要验证令牌。受保护资源来调用授权服务提供的检验令牌的服务,我们把这种校验令牌的方式称为令牌内检。有几种方式:
共享数据库:授权服务依赖一个数据库,受保护资源服务也依赖这个数据库
提供服务:在如今已经成熟的分布式以及微服务的环境下,不同的系统之间是依靠服务而不是数据库来通信了,比如授权服务给受保护资源服务提供一个 RPC 服务
JWT 令牌:授权服务“扔出”一个令牌,受保护资源服务“接住”这个令牌,然后自己开始解析令牌本身所包含的信息就可以了(因为 JWT 令牌本身就包含了之前所要依赖数据库或者依赖 RPC 服务才能拿到的信息,比如哪个用户为哪个软件进行了授权等信息)
很显然,JWT 令牌需要在公网上做传输。所以在传输过程中,JWT 令牌需要进行 Base64 编码以防止乱码,同时还需要进行签名及加密处理来防止数据信息泄露。
比如 JJWT(Java 开源的 JWT 工具)封装了 Base64URL 编码和对称 HMAC、非对称 RSA 的一系列签名算法。使用 JJWT 可以很方便地生成一个经过签名的 JWT 令牌、解析一个 JWT 令牌,我们只关注上层的业务逻辑实现,而无需关注编解码和签名算法的具体实现。
JWT 格式令牌的三大好处:
JWT 的核心思想,就是用计算代替存储,有点时间换空间的味道。当然,这种经过计算并结构化封装的方式,也减少了“共享数据库” 因远程调用而带来的网络传输消耗,所以也有可能是节省时间的。
加密。因为 JWT 令牌内部已经包含了重要的信息,所以在整个传输过程中都必须被要求是密文传输的,这样被强制要求了加密也就保障了传输过程中的安全性。这里的加密算法,既可以是对称加密,也可以是非对称加密。
使用 JWT 格式的令牌,有助于增强系统的可用性和可伸缩性。因为这种 JWT 格式的令牌,通过“自编码”的方式包含了身份验证需要的信息,不再需要服务端进行额外的存储,所以每次的请求都是无状态会话。这就符合了我们尽可能遵循无状态架构设计的原则,也就是增强了系统的可用性和伸缩性。
JWT 格式令牌的最大问题在于:覆水难收,也就是说,没办法在使用过程中修改令牌状态。
使用 JWT 格式令牌时,每次颁发的令牌都不会在服务端存储,这样当要改变令牌状态的时候就无能为力了,因为服务端并没有存储这个 JWT 格式的令牌。这就意味着,JWT 令牌在有效期内是横行无止的。
为了解决这个问题,通常有两种做法:
将每次生成 JWT 令牌时的秘钥粒度缩小到用户级别,也就是一个用户一个秘钥。这样,当用户取消授权或者修改密码后,就可以让这个密钥一起修改。一般情况下,这种方案需要配套一个单独的密钥管理服务。
在不提供用户主动取消授权的环境里面,如果只考虑到修改密码的情况,那么我们就可以把用户密码作为 JWT 的密钥。当然,这也是用户粒度级别的。这样一来,用户修改密码也就相当于修改了密钥。
JWT 令牌,生命周期的管理问题。万物皆有周期,令牌也不例外。无论是 JWT 结构化令牌还是普通的令牌。它们都有有效期,只不过,JWT 令牌可以把有效期的信息存储在本身的结构体中。具体到 OAuth 2.0 的令牌生命周期,通常会有三种情况。
授权服务将 OAuth 2.0 的复杂性都揽在了自己身上,那么,作为第三方软件和受保护资源服务,需要着重处理哪些工作呢?
首先,申请注册为开发者,再创建一个应用。
然后,开始使用授权服务,需要重点关注:
1. 注册信息
拥有自己的 app_id 和 app_serect 等信息
填写自己的回调地址 redirect_uri
申请权限
这种方式的注册,有时也称为静态注册,也就是第三方软件的研发人员提前登录到xxx开放平台进行手动注册,以便后续使用这些注册的相关信息来请求访问令牌。
2. 引导授权
第三方软件将用户引导至授权服务,让用户为第三方软件授权,得到了授权之后,第三方软件才可以代表用户去访问数据
3. 使用访问令牌
目前 OAuth 2.0 的令牌只支持一种类型——bearer 令牌(它可以是任意字符串格式的令牌)
官方规范给出的使用访问令牌请求的方式,有三种:
Form-Encoded Body Parameter(表单参数,POST)作者建议✅
URI Query Parameter(URI 查询参数)
Authorization Request Header Field(授权请求头部字段)官方建议✅
4. 使用刷新令牌
一个设计良好的第三方应用,应该将 expires_in 值保存下来,如果发现 expires_in 即将过期,则需要利用 refresh_token 去重新请求授权服务,以便获取新的、有效的访问令牌
刷新令牌是一次性的,使用之后就会失效,但是它的有效期会比访问令牌要长
当刷新令牌也过期了,应该再次引导用户重新授权
实际上,在整个开放授权的环境中,受保护资源最终指的还是 Web API(有的是网站)。
我们在构建受保护资源服务的时候,除了基本的要检查令牌的合法性,还需要校验权限范围了。
平台提供出去的 API,更多的场景却是基于用户属性的。为了应对这种情况,我们应该有一个统一的网关层来处理这样的校验,所有的请求都会经过 API GATEWAY 跳转到不同的受保护资源服务。这样呢,我们就不需要在每一个受保护资源服务上都做一遍权限校验的工作了,而只需要在 API GATEWAY 这一层做权限校验就可以了。