OAuth 2.0 的探险之旅
OAuth 2.0 的探险之旅


前言


OAuth 2.0 全称是 Open Authorization 2.0, 是用于授权(authorization)的行业标准协议。OAuth 2.0 专注于客户端开发人员的简单性,同时为 Web 应用程序、桌面应用程序、移动设备应用等提供了特定的授权流程。它在 2012 年取代了 OAuth 1.0, 并且 OAuth 2.0 协议不向后兼容 OAuth 1.0。


需要注意的是,OAuth 2.0 是一个授权(authorization)协议,而不是身份验证(authentication )协议。


Roles 角色


首先还需要了解一些概念, 因为整个 OAuth 授权流程都是围绕这些抽象的概念展开的, 角色是 OAuth2.0 授权框架核心规范的一部分, OAuth 定义了以下 4 种角色


Resource Owner
资源所有者, 这里通常是拥有资源权限的用户或者系统。
Client
客户端应用, 它可以通过访问令牌(Token)访问受保护资源, 可以是 Web 浏览器上的网站也可以是桌面应用或者手机 App。
Authorization Server
授权服务器, 在经过用户的授权后, 向客户端应用发放访问令牌(Access Token)。
Resource Server
资源服务器, 存放受保护资源的服务器, 接受来自客户端(Client)请求的有效访问令牌(Access Token), 然后返回对应的资源。


Client Types 客户端类型


OAuth 2.0 核心规范定义了两种客户端类型, confidential 机密的, 和 public 公开的, 区分这两种类型的方法是, 判断这个客户端是否有能力维护自己的机密性凭据(password, client_secret)。


confidential 对于一个普通的 web 站点来说,虽然用户可以访问到前端页面, 但是数据都来自服务器的后端 api 服务, 前端只是获取授权码 code, 通过 code 换取 access_token 这一步是在后端的 api 完成的, 由于是内部的服务器, 客户端有能力维护密码或者密钥信息, 这种是机密的的客户端。
public 对于一个没有后端的纯前端应用来说(比如 SPA), 数据的展示和操作都是在前端完成的, 包括获取令牌和操作令牌, 把一个客户端密码或者密钥放在纯前端应用是不安全的, 这种是公开的客户端。


Client Authentication 客户端身份认证


前面已经说过了, OAuth 2.0 是授权协议, 那为什么还要对 OAuth 2.0 客户端进行身份验证呢?身份验证和授权有什么区别?简单说身份验证确认用户是否是本人, 而授权则是授予用户访问资源的权限, 授权的前提条件一定是要先通过身份认证, 而且接下来的内容中, 也有用到了身份认证, 为了方便理解, 所以对认证做了简单的介绍。


授权服务器对客户端进行身份验证可以保证把令牌颁发给了合法的客户端, 但是认证其实已经超出了 OAuth2.0 的协议范围, 在 [RFC 6749] 中也只是简单介绍了以下 2 种认证方式:


第一种是使用 HTTP Basic [RFC2617] 中定义的身份验证方案进行身份认证, 这种方式叫 client_secret_basic, 首先需要对 username,password 或者 client_id, client_secret 用冒号进行拼接。


{username}:{password} 或者 {client_id}:{client_secret} 就像这样 admin:123456
然后对字符串进行 Base64 编码, 然后设置为请求 Header 中的 Authorization, 注意前面要拼接一个 Basic 和空格, 如下


POST /token HTTP/1.1Host: www.authorization-server.com Authorization: Basic YWRtaW46MTIzNDU2Content-Type: application/x-www-form-urlencoded


第二种方式就更简单粗暴了, 直接在请求体中添加 client_id 和 client_secret 参数, 如下


POST /token HTTP/1.1Host: www.authorization-server.com Content-Type: application/x-www-form-urlencodedgrant_type=refresh_token&refresh_token=tGzv3JOkF0XG5Qx2TlKWIA&client_id=s6BhdRkqt3&client_secret=7Fjfp0ZBr1KtDRbnfVdmIw


Protocol Flow 协议流程


OAuth 2.0 的探险之旅


上图是抽象的授权协议流程, 也展示了 4 种角色(Role)之间的交互, 具体的过程如下


(A) 客户端向资源所有者(用户)发起授权请求, 资源所有者选择授予权限或者取消, 这个过程中, 授权服务器充当中介的角色, user —> authorization server —-> client.
(B) 客户端收到授权许可(code),这是一个代表资源所有者授权的凭证。
(C) 客户端通过授权许可(code)向授权服务器发起请求, 并期望获取一个访问令牌(access token)。
(D) 授权服务器对客户端进行身份验证并验证授权许可,如果有效,则颁发访问令牌(access token)并返回。
(E) 客户端通过访问令牌向资源服务器请求受保护的资源。
(F) 资源服务器验证访问令牌, 如果有效, 则返回相应的资源。


Access Token 访问令牌


access token 是一个用来访问受保护资源的凭证, 它是由授权服务器(Authorization Server)颁发给客户端(Client)的, 通常是字符串形式, access token 拥有特定的访问范围(scope), 并且有时间限制, 访问令牌可以有不同的格式、结构, 这点并没有限制。


Refresh Token 刷新令牌


refresh token 是一个用来获取 access token 的凭证, 同样它是由授权服务器(Authorization Server)颁发给客户端(Client)的, 刷新令牌的时效性比访问令牌要长, 当访问令牌过期的时候, 可以直接用刷新令牌去授权服务器获取新的访问令牌, 而无需重新登录。和访问令牌不同的是, 授权服务器颁发访问令牌是必须的, 而颁发刷新令牌则是可选的, 并且访问令牌还会和资源服务器交互, 而刷新令牌只和授权服务器交互。


刷新令牌的设计非常巧妙, 它是用户体验和安全两方面取舍的一个平衡。


OAuth 2.0 的探险之旅


(A) 客户端向授权服务器发起请求, 并提供授权许可。
(B) 授权服务器对客户端进行身份验证并验证授权许可,如果有效,则颁发访问令牌和刷新令牌。
(C) 客户端请求受保护资源并提供访问令牌。
(D) 资源服务器验证这个访问令牌,如果有效, 返回相应的内容。
(E) 重复步骤 (C) 和 (D),直到访问令牌过期。如果客户端知道了访问令牌已经过期,它跳到步骤(G), 如果不知道, 继续向资源服务器发起请求。
(F) 由于访问令牌无效,资源服务器返回无效的令牌错误。
(G) 客户端发起获取刷新令牌的请求, 同时要带上当前的刷新令牌。
(H) 授权服务器对客户端进行认证并验证刷新令牌,如果有效,则发出新的访问令牌和一个可选的新的刷新令牌。


Authorization Grant 授权许可


授权许可是一个资源所有者授权的凭证, 客户端通过它去获取访问令牌(access token), OAuth 2.0 定义了以下四种许可模式。


Authorization Code 授权码Implicit 隐式Resource Owner Password Credentials 密码Client Credentials 客户端凭证


Authorization Code Grant 授权码模式


授权码模式是最常用的一种授权许可模式, 也是最经典的一种, 这种模式可以获取到访问令牌和刷新令牌。还有一个特点是, 授权码模式是基于 Web 重定向的流程。


OAuth 2.0 的探险之旅


(A) 客户端提供一个授权链接, 引导用户点击跳转到授权服务的 /authorize 端点, 如下


https://www.authorization-server.com/oauth2/authorize?response_type=code&client_id=s6BhdRkqt3&scope=user&state=8b815ab1d177f5c8e &redirect_uri=https://www.client.com/callback


参数说明如下:


response_type:必选项, 表示响应类型,此处的值固定为”code”client_id:必选项, 客户端的身份标识redirect_uri 可选项, 经过用户允许授权后, 授权服务器跳转到客户端的回调地址scope 可选项, 希望用户同意授权的权限范围state 可选项, 推荐使用, 客户端可以维护一个在请求和回调之间的状态, 授权服务器重定向到回调地址时, 会带上这个参数, state 可以防止跨站点请求伪造-CSRF 攻击。


(B) 授权服务器提供授权页面, 用户选择同意授权或者拒绝来自客户端的请求, 如下所示


OAuth 2.0 的探险之旅


(C) 假如用户同意了授权, 授权服务器会通过 url 重定向到客户端的回调地址, 并且会带上一个授权码 code 和 state 参数(如果之前客户端的请求中传递了 state 参数的话)


https://www.client.com/callback?code=d8c2afe6ecca004eb4bd7024&state=8b815ab1d177f5c8e 


(D) 现在已经拿到了授权码 code 并获得了用户的授权, 接下来需要用 code 来换取 访问令牌 access_token, 可以向授权服务的 /token 端点发送 POST 请求。


POST /token HTTP/1.1 Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JWContent-Type: application/x-www-form-urlencodedhttps://www.authorization-server.com/oauth2/token?grant_type=authorization_code&code=d8c2afe6ecca004eb4bd7024&redirect_uri=https://www.client.com/callback


参数说明如下:


grant_type: 必选项,表示授权类型, 此处的值固定为”authorization_code”
code: 必选项,授权码, 这是上一步从授权服务器传给回调地址(redirect_uri)的参数
redirect_uri: 必选项, 客户端的回调地址, 注意要和(A)步骤中的 redirect_uri 一致。
client_id: 必选项,客户端的身份标识


注意, 上面使用了 Http Basic 身份认证(Authorization: Basic ...), 在本文的 “客户端身份认证” 部分有介绍, 主要是为了验证 Client 的合法性。


通过 code 换取 access_token 步骤中,还有一种比较常见的身份验证做法是, 直接在请求体中传入 client_id, client_secret, 如下:


POST /token HTTP/1.1  Content-Type: application/x-www-form-urlencodedhttps://www.authorization-server.com/oauth2/token?grant_type=authorization_code&code=d8c2afe6ecca004eb4bd7024&client_id=s6BhdRkqt3&client_secret=ecca004eb4bd7024c2afe6ecc&redirect_uri=https://www.client.com/callback


(E) 授权服务器对 client,code 验证通过后, 会返回 access_token 和一个可选的 refresh_token, 如下:


HTTP/1.1 200 OKContent-Type: application/json;charset=UTF-8Cache-Control: no-storePragma: no-cache{  "access_token":"2YotnFZFEjr1zCsicMWpAA",  "token_type":"bearer",  "expires_in":3600,  "refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA" }


参数介绍:


access_token: 必选项,访问令牌
token_type: 令牌类型, 通常是 Bearer [RFC6750], 访问受保护资源需要在请求头设置 (Authorization:Bearer …)
expires_in: 访问令牌的有效期, 以秒为单位
refresh_token:可选的刷新令牌


(F) 客户端使用 access_token 向资源服务器发起请求
(G) 资源服务器验证 access_token, 验证通过后, 返回受保护的资源


这里有一个问题是, 文章上面说 access_token 只是一个字符串, 那么资源服务器如何来验证该令牌?在 OAuth 2.0 核心协议中, 关于这点并没有提及。


访问令牌主要分为两种, 一种是没有意义的随机字符串, 比如 2YotnFZFEjr1zCsicMWpAA, 这种情况客户端本身是不能鉴别令牌是否有效, 只能去授权服务器发起请求来验证该令牌, 这种安全性高,但性能差, 可以参考 RFC 7662.


第二种就是很常见的 JWT 令牌, 可以参考 RFC 7519, 令牌本身就包含了一些用户信息, 资源服务器可以通过加密算法和签名验证令牌是否有效, 而且不需要和授权服务器进行交互, 但是缺点是, 如果令牌在到期前被撤销, 资源服务器是没办法知道的。


Implicit Grant 隐式授权模式


OAuth 2.0 的探险之旅


上面是隐式授权的流程图, 它和授权码模式很像, 区别在于, 授权码模式是先拿到 code,然后再换取 access_token, 而隐式授权只用一次请求就拿到了 access_token, 通过 url 参数的形式返回, 令牌也直接暴露在了浏览器地址栏, 实际上这种模式是 OAuth 2.0 对公开(public)的客户端的授权流程进行了优化, 上面说到了客户端分为两种, 机密的的和公开的, 因为公开的客户端没有能力维护自己的机密凭证, 所以适合这种模式, 并且授权码模式需要客户端认证 (通过 code 换取 access_token 的时候,需要使用 Http Basic 认证,或者传入 client_secret) , 而隐式授权在整个流程中并没有客户端认证,所以是不安全也不推荐使用的。


请求参数:


response_type 这里固定是 token


GEThttps://www.authorization-server.com/oauth2/authorize?response_type=token&client_id=s6BhdRkqt3&scope=user&state=8b815ab1d177f5c8e &redirect_uri=https://www.client.com/callback


响应参数:


https://www.client.com/callback#access_token=2YotnFZFEjr1zCsicMWpAA&state=8b815ab1d177f5c8e&token_type=Bearer&expires_in=3600


这里注意 access_token 实际上并不是一个 url 参数, 它前面是 # 号, 表示一个 fragment, # 有别于 ?? 后面的查询字符串会被网络请求发送到服务器,而 fragment 则不会发送到服务器, 但是 js 是可以解析到 fragment 的值, 也就是 access_token, 这个设计很巧妙!


Resource Owner Password Credentials Grant 密码凭证模式


OAuth 2.0 的探险之旅


密码模式就更简单粗暴了, 用户直接把账号密码告诉客户端, 客户端向授权服务器发起 POST 请求, 并携带用户名和密码, 授权服务器验证通过后, 返回访问令牌和可选的刷新令牌, 这种模式的特点是, 用户和客户端是高度信任的。


请求参数:


POST /token HTTP/1.1Host: www.authorization-server.comAuthorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JWContent-Type: application/x-www-form-urlencodedgrant_type=password&username=johndoe&password=A3ddj3w


响应参数:


HTTP/1.1 200 OKContent-Type: application/json;charset=UTF-8Cache-Control: no-storePragma: no-cache{  "access_token":"2YotnFZFEjr1zCsicMWpAA",  "token_type":"Bearer",  "expires_in":3600,  "refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA" }


Client Credentials Grant 客户端凭证模式


OAuth 2.0 的探险之旅


客户端凭证模式的特点是, 客户端就是资源所有者, 客户端访问资源也不需要用户的授权, 因为这个过程中没有用户, 资源本身就属于客户端, 通过在请求体中传入 client_id,client_secret 参数或者 Http Basic 进行客户端认证, 这种模式很适合后端服务或者 api 之间调用的场景。


请求参数:


此处的 grant_type 固定是 client_credentials


POST /token HTTP/1.1Host: www.authorization-server.comAuthorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JWContent-Type: application/x-www-form-urlencodedgrant_type=client_credentials


响应参数:


HTTP/1.1 200 OKContent-Type: application/json;charset=UTF-8Cache-Control: no-storePragma: no-cache{  "access_token":"2YotnFZFEjr1zCsicMWpAA",  "token_type":"Bearer",  "expires_in":3600,  "example_parameter":"example_value"}


总结


本文介绍了 OAuth 2.0 核心协议, 主要参考 RFC 6749 (The OAuth 2.0 Authorization Framework) 核心协议 , 相信读完本文, 你会发现有些流程其实是不安全的, 没错, 其中的隐式授权和密码授权模式已经不再建议使用, 因为隐式授权从一开始就没有真正安全过, 这里介绍一下背景, 当时 OAuth 2.0 出现的时间点在 2010 年左右, 移动端应用是全新的,单页面应用程序(SPA) 也才刚开始出现, 当时的 Web 生态和现在还是差别很大, 由于技术问题, 并不能使用常规的 OAuth 模式进行授权。对于现在来说, 推荐使用专门为移动设备应用而设计的 PKCE (RFC 7636) 模式, 它是 OAuth 2.0 核心的一个扩展协议, 也是最近几年移动设备应用授权的最佳实践。


目前 OAuth 2.1 也是一项正在进行中的工作, 它围绕 OAuth 2.0 对其授权功能进行加强和优化, 下篇文章我会继续介绍 OAuth 2.1 的新功能。


References


https://www.rfc-editor.org/rfc/rfc6749https://www.rfc-editor.org/rfc/rfc6750https://www.rfc-editor.org/rfc/rfc7662https://www.rfc-editor.org/rfc/rfc5849https://www.rfc-editor.org/rfc/rfc2617https://www.rfc-editor.org/rfc/rfc7519https://oauth.net/2/https://www.youtube.com/watch?v=CHzERullHe8https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics-13#section-3.4


😃 欢迎关注微信公众号【全球技术精选】


OAuth 2.0 的探险之旅

© 版权声明

☆ END ☆
喜欢就点个赞吧
点赞0 分享
图片正在生成中,请稍后...