OAuth 2.0 协议是一种三方授权协议, 目前大部分的第三方登录与开放授权都是基于该协议的标准或改进实现 OAuth 1.0 版本于 2007 年发布, 2.0 版本则在 2011 年发布, 其中 2.0 版本取消了所有 token 的加密过程, 并简化了授权流程, 但因强制使用 HTTPS 协议, 被认为安全性高于之前的版本
项目地址: https://github.com/procyon-lo...
一. 小场景带你感受 OAuth 2.0 的交互过程
为了让你对 OAuth 2.0 协议有一个整体上的感知, 这里先设置一个小场景对协议的交互过程进行模拟话说阿冰在花果山上有几亩果园, 种了各种各样的水果, 有苹果荔枝西瓜等等, 并由管理员老王进行看管
夏天到了, 果园里的的水果涨势喜人, 阿冰的好朋友小桂子想去果园摘一些西瓜和荔枝解解馋, 于是小桂子提着果篮吭哧吭哧就跑到了花果山, 结果被管理员老王一把拦住, 呵斥道: 要摘水果, 必须经过阿冰的同意, 快出示相关凭证, 小桂子当然没有, 还纳闷去哪搞什么鬼凭证这时候老王拎着小桂子来到了一个茅草房前, 只见上面写着花果山街道办事处老王告诉小桂子这里可以开具凭证
小桂子来到柜台前, 业务人员询问了其姓名, 并要求出示身份证件核实身份之后, 业务人员询问小桂子要去谁的果园, 采摘什么水果? 小桂子如实答复, 不一会只见业务人员打印出了一张凭证, 并将其丢入一个时光通道, 凭证上写着:
小桂子请求在您的果园采摘以下水果:
- 采摘您的西瓜 2 个
- 采摘您的荔枝 3 斤
通道那头的阿冰收到凭证之后盖上自己的印章以示同意, 然后将其投回了时光通道最终小桂子拿到了经过阿冰授权的凭证, 跳着奔向果园管理员老王验证了小桂子出示的凭证, 并摘了 2 个西瓜和 3 斤荔枝交到了小桂子手上
一个星期后, 小桂子嘴又馋了, 拿着上次的凭证直奔花果山, 到了果园门口又被老王拦住了, 老王说: 你这凭证已经过期了, 必须再次请求阿冰授权小桂子满脸委屈, 嘟囔着: 不就想吃俩西瓜嘛, 怎么就管的这么严
小编不才, 实在编不下去了, 就这样结束吧
在这个故事中, 凭证限制了小桂子是否可以获得水果, 以及获得哪些水果, 每种水果多少斤此外凭证还具备生命周期, 超出凭证范围的请求都会被老王拒绝通过凭证, 小桂子可以获取到自己想要的水果, 阿冰也不需要亲临花果山, 在做好果园管理的同时, 又不影响阿冰的生活
回到正题, 对于 OAuth 2.0 协议(以下简称 OAuth 协议), 我相信大部分读者都有所接触, 最常见的就是使用微信登录第三方应用的确, OAuth 协议被广泛应用于第三方授权登录中, 借助第三方登录可以让用户免于再次注册之苦, 支持第三方登录也对这些网站 APP 起到了积极的作用, 免去了复杂的注册过程, 用户体验更佳这样在提高留存率的同时, 也更加易于收集用户的一些非敏感信息等, 另外还可以借助一些社交类的第三方帐号进行站点推广
二. OAuth 2.0 中的基本概念与授权流程
作为第三方登录服务提供方, 其核心矛盾点是 既要让用户在对接服务的 APP 上完成登录, 同时还不能让该 APP 拿到用户的密码凭证 解决这一矛盾的利器就是 token(中文译为令牌), 而 OAuth 协议的最终目的就是给第三方应用下发 token, 它记录了用户的登录或授权状态通过将 token 下发给第三方应用, 既能让 APP 登录并拿到用户许可的数据, 也可以将用户的密码凭证牢牢拽在服务自己手里
上面的论述可能侧重了第三方登录, 实际上登录只是一个授权的过程, OAuth 2.0 协设计的目的在于开放授权对于一个应用, 其最终目的还是在于拿到用户存储在资源服务器上的用户数据, 所以登录授权还只是第一步, 后续 APP 还需要携带 token 去资源服务器请求用户数据, 这个时候是一个鉴权的过程, OAuth 协议的主要目的在于授权, 至于鉴权, 实现上主要是还是对请求传递过来的 token 进行解析和验证, 这一块相对要简单一些, 所以本文主要讲解 OAuth 授权的过程
2.1 OAuth 2.0 定义的 4 种角色
资源所有者(resource owner)
受保护资源所属的实体, 比如资源的所有人, 下文的用户即为资源所有者
资源服务器(resource server)
托管受保护资源的服务器, 能够响应持有访问令牌的资源访问请求, 可以与授权服务器是同一台服务器, 也可以分开
客户端(client)
客户端是 OAuth 服务的接入方, 其目的是希望请求用户存储在资源服务器上的受保护资源
授权服务器(authorization server)
授权服务器的主要职责是验证资源所有者的身份, 并依据资源所有者的许可向客户端下发访问令牌
在之前的故事中, 果园中的水果就是资源, 而资源所有者是阿冰, 果园可以看做是资源服务器, 小桂子就是客户端, 而街道办事处是整个流程的授权中心, 也就是上面的授权服务器
2.2 基本概念
2.2.1 访问令牌(access token)
还记得故事中老王问小桂子要的凭证吗? 凭证限制了小桂子只能摘 2 个西瓜和 3 斤荔枝, 并且凭证还是具备生命周期的, 一个星期之后小桂子再拿着过期的凭证老王也不认了
实际上故事中的凭证对应的是 OAuth 2.0 中的访问令牌, 访问令牌是在用户授权许可下, 授权服务器下发给客户端的一个授权凭证, 该令牌所要表达的意思是 用户授予该 APP 在多少时间范围内允许访问哪些与自己相关的服务或数据 , 所以访问令牌主要在 时间范围 和 权限范围 两个维度进行控制, 此外访问令牌对于客户端来说是非透明的, 外在表现就是一个字符串, 客户端无法知晓字符串背后所隐藏的用户信息, 因此不用担心用户的密码凭证会因此泄露
2.2.2 刷新令牌(refresh token)
故事中小桂子最后之所以觉得委屈是因为意识到自己需要再重复走一次授权过程, 这让小桂子觉得很麻烦, 专业点说就是用户体验太差, 解决之道就是引入刷新令牌
刷新令牌的作用在于更新访问令牌, 访问令牌的生命周期一般较短, 这样可以保证在发生访问令牌泄露时, 不至于造成太坏的影响, 但是访问令牌有效期设置太短导致的副作用就是用户需要频繁授权, 虽然可以通过一定的机制进行静默授权, 但是频繁的调用授权接口之于授权服务器也是一种压力此时可以在下发访问令牌的同时下发一个刷新令牌, 刷新令牌的生命周期明显长于访问令牌, 这样在访问令牌失效时, 可以利用刷新令牌去授权服务器换取新的访问令牌, 不过协议对于刷新令牌没有强制规定, 是否需要该令牌客户端可以自行选择
2.2.3 回调地址(redirect uri)
OAuth 2.0 是一类基于回调的授权协议, 以授权码模式为例, 整个授权需要分为两步进行, 第一步下发授权码, 第二步根据授权码请求授权服务器下发访问令牌 OAuth 在第一步下发授权码时, 是将授权码以参数的形式添加到回调地址后面, 并以 302 跳转的形式进行下发, 这样简化了客户端的操作, 不需要再主动去触发一次请求, 即可进入下一步流程
回调的设计存在一定的安全隐患, 坏人可以利用该机制引导用户到一个恶意站点, 继而对用户发起攻击对于授权服务器而言, 也存在一定的危害, 坏人可以利用该机制让授权服务器变成肉鸡, 以授权服务器为代理请求目标地址, 这样在消耗授权服务器资源的同时, 也对目标地址服务器产生 DDOS 攻击
为了避免上述安全隐患, OAuth 协议强制要求客户端在注册时填写自己的回调地址, 其目的是为了让回调请求能够到达客户端自己的服务器, 从而可以走获取访问令牌的流程客户端可以同时配置多个回调地址, 并在请求授权时携带一个地址, 服务器会验证客户端传递上来的回调地址是否与之前注册的回调地址相同, 或者前者是后者集合的一个元素, 只有在满足这一条件下才允许下发授权码, 同时协议还要求两步请求客户端携带的回调地址必须一致, 通过这些措施来保证回调过程能够正常到达客户端自己的服务器, 并继续后面拿授权码换取访问令牌的过程
2.2.4 权限范围(scope)
访问令牌自带过期时间, 可以在时间维度上对授权进行控制, 而在权限范围上, OAuth 引入了一个 scope 的概念 Scope 可以看做是一个对象, 包含一个权限的 ID, 名称, 以及描述信息等, 比如 获取您的基本资料 (头像昵称) 应该在接入 OAuth 服务时必须向服务提供方申请相应的 scope, 并在请求授权时指明该参数, 这些权限在用户确认授权时, 必须毫无保留的展示给用户, 以让用户知道本次请求需要访问用户的哪些数据或服务
在之前的故事中凭证允许小桂子只能摘取 2 个西瓜和 3 斤荔枝, 这里就对应两个 scope, 这些信息是写入到凭证 (访问令牌) 中的, 从而限制凭证的权限范围
2.3 基本授权流程
OAuth 2.0 协议定义了 4 种授权模式, 其中最具代表性的是授权码模式, 我们将在 3.1 节中详细介绍, 这里先简单体会一下 OAuth 2.0 的授权流程, 交互时序图如下:
假设整个流程开始之前, 用户已经登录, 那么整个授权流程如下:
客户端请求资源所有者 (用户) 授权, 一般都是由授权服务器进行引导
资源所有者实施授权(采用 4 种授权模式中的一种), 客户端拿到用户的授权凭证
客户端携带用户授权凭证请求授权服务器下发访问令牌
授权服务器验证客户端出示的授权凭证, 并下发访问令牌
客户端携带访问令牌请求存储在资源服务器上的用户受保护资源
资源服务器验证客户端出示的访问令牌, 通过则响应客户端的请求
整个过程中, 客户端都无法接触到用户的密码凭证信息, 客户端通过访问令牌请求受保护资源, 用户可以通过对授权操作的控制来间接控制客户端对于受保护资源的访问权限范围和周期
三. 四种授权模式
OAuth 2.0 相对于 1.0 版本在授权模式上做了更多的细化, 已定义的授权模式分为四种:
授权码模式(Authorization Code Grant)
隐式授权模式(Implicit Grant)
资源所有者密码凭证模式(Resource Owner Password Credentials Grant)
客户端凭证模式(Client Credentials Grant)
3.1 授权码授权模式(Authorization Code Grant)
授权码模式在整个授权流程上与 1.0 版本最为贴近, 但是流程上还是要简化许多, 也是 OAuth 2.0 中最标准, 应用最为广泛的授权模式这类授权模式非常适用于具备服务端的应用, 当然现在大多数 APP 都有自己的服务端, 所以授权码模式拥有最广泛的应用场景, 下图为授权码各个角色之间的交互时序:
整个授权流程说明如下(具体参数释义见下文):
客户端携带 client_id, scope, redirect_uri, state 等信息请求授权服务器下发 code
授权服务器验证客户端身份, 通过则询问用户是否同意授权(此时会跳转到用户能够直观看到的授权页面, 等待用户点击确认授权)
假设用户同意授权, 此时授权服务器会将 code 和 state 拼接在 redirect_uri 后面, 并以 302 形式下发 code
客户端携带 code, redirect_uri, 以及 client_secret 请求授权服务器下发 access_token
授权服务器验证客户端身份, 同时验证 code, 以及 redirect_uri 是否与第一步相同, 通过则下发 access_token, 并选择性下发 refresh_token
3.1.1 获取授权码
授权码是授权流程的一个中间临时凭证, 是对用户确认授权这一操作的一个短暂性表征, 其生命周期一般较短, 协议建议最大不要超过 10 分钟, 在这一有效时间内, 客户端可以通过授权码去授权服务器请求换取访问令牌, 授权码应该采取防重放措施
请求参数说明:
名称 | 是否必须 | 描述信息 |
---|---|---|
response_type | 必须 | 对于授权码模式来说 response_type=code |
client_id | 必须 | 客户端 ID,用于标识一个客户端,在注册应用时生成 |
redirect_uri | 可选 | 授权回调地址,具体参见 2.2.3 小节 |
scope | 可选 | 权限范围,用于对客户端的权限进行控制,如果客户端没有传递该参数,那么服务器则以该应用被许可的所有权限代替 |
state | 推荐 | 用于维持请求和回调过程中的状态,防止 CSRF 攻击 ,服务器不对该参数做任何处理,如果客户端携带了该参数,则服务器在响应时原封不动的进行返回 |
请求参数示例:
- GET /authorize?response_type=code&client_id=s6BhdRkqt3&state=xyz&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb HTTP/1.1
- Host: server.example.com
客户端携带上述参数请求授权服务器, 授权服务器会验证客户端的身份以及相关参数, 并在确认用户已登录的前提下弹出授权页询问用户是否同意授权, 如果用户同意则会将授权码 (code) 和 state 信息添加到回调地址后面, 并以 302 的形式下发
成功响应参数说明:
名称 | 是否必须 | 描述信息 |
---|---|---|
code | 必须 | 授权码,授权码代表用户确认授权的暂时性凭证,推荐最大生命周期不超过 10 分钟 |
state | 可选 | 如果客户端传递了该参数,则必须原封不动返回 |
成功响应示例:
- HTTP/1.1 302 Found
- Location: https://client.example.com/cb?code=SplxlOBeZQQYbYS6WxSbIA&state=xyz
如果请求参数错误, 或者服务器端响应错误, 那么需要将错误信息添加在回调地址后面, 同样以 302 形式下发(回调地址错误, 或客户端标识无效除外)
错误响应参数说明:
名称 | 是否必须 | 描述信息 |
---|---|---|
error | 必须 | 错误代码 |
error_description | 可选 | 具备可读性的错误描述信息 |
error_uri | 可选 | 错误描述信息页面地址 |
state | 可选 | 如果客户端传递了该参数,则必须原封不动返回 |
错误响应示例:
- HTTP/1.1 302 Found
- Location: https://client.example.com/cb?error=access_denied&state=xyz
3.1.2 下发访问令牌
授权服务器的授权端点在以 302 形式下发 code 之后, 用户 User-Agent, 比如浏览器, 将携带对应的 code 回调请求用户指定的 redirect_url, 这个地址应该能够保证请求打到应用服务器的对应接口, 该接口可以由此拿到 code, 并附加相应参数请求授权服务器的令牌端点, 授权端点验证 code 和相关参数, 验证通过则下发 access_token
请求参数说明:
名称 | 是否必须 | 描述信息 |
---|---|---|
grant_type | 必须 | 对于授权码模式 grant_type=authorization_code |
code | 必须 | 上一步骤获取的授权码 |
redirect_uri | 必须 | 授权回调地址,具体参见 2.2.3 小节,如果上一步有设置,则必须相同 |
client_id | 必须 | 客户端 ID,用于标识一个客户端,在注册应用时生成 |
如果在注册应用时有下发客户端凭证信息(client_secret), 那么客户端必须携带该参数以让授权服务器验证客户端的真实性针对客户端凭证需要多说的一点就是不能将其存储或传递到客户端, 客户端无法保证 client_secret 的安全, 应该始终将其存储在应用的服务器端, 当下发授权码回调请求到应用服务器时, 在服务器端携带上 client_secret 继续请求下发令牌
请求参数示例:
- POST /token HTTP/1.1
- Host: server.example.com
- Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
- Content-Type: application/x-www-form-urlencoded
- grant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb
授权服务器需要验证客户端的真实性, 以及是否与之前请求授权码的客户端属同一个(请求授权时的信息可以记录在授权码中, 或以授权码为 key 建立缓存), 授权服务器还要保证授权码处于生命周期内, 且只能被使用一次验证通过之后, 授权服务器生成 access_token, 并选择性下发 refresh_token,OAuth 2.0 协议明确了 token 的下发策略, 对于 token 的生成策略没有做太多说明, 不过相关 RFC 补充文档为生成 token 提供了指导, 目前主要的 token 有 BEARERMAC 等类型
成功响应参数说明:
名称 | 是否必须 | 描述信息 |
---|---|---|
access_token | 必须 | 访问令牌 |
token_type | 必须 | 访问令牌类型,比如 BEARER、MAC 等 |
expires_in | 推荐 | 访问令牌的生命周期,以秒为单位,表示令牌下发后多久时间过期,如果没有指定该项,则使用默认值 |
refresh_token | 可选 | 刷新令牌,选择性下发,参见 2.2.2 |
scope | 可选 | 权限范围,如果最终下发的访问令牌对应的权限范围与实际应用指定的不一致,则必须在下发访问令牌时用该参数指定说明 |
最后访问令牌以 JSON 格式响应, 并要求指定响应首部
Cache-Control: no-store
和 Pragma: no-cache
成功响应示例:
- HTTP/1.1 200 OK
- Content-Type: application/json;charset=UTF-8
- Cache-Control: no-store
- Pragma: no-cache
- {
- "access_token":"2YotnFZFEjr1zCsicMWpAA",
- "token_type":"example",
- "expires_in":3600,
- "refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA",
- "example_parameter":"example_value"
- }
错误响应参数说明:
名称 | 是否必须 | 描述信息 |
---|---|---|
error | 必须 | 错误代码 |
error_description | 可选 | 具备可读性的错误描述信息 |
error_uri | 可选 | 错误描述信息页面地址 |
错误响应示例:
- HTTP/1.1 400 Bad Request
- Content-Type: application/json;charset=UTF-8
- Cache-Control: no-store
- Pragma: no-cache
- {
- "error":"invalid_request"
- }
3.1.3 对于授权码模式的一点小感悟
授权码授权模式是 OAuth 2.0 协议已定义 4 种模式中最严谨的模式, 其余 3 中模式都是建立在一些特殊场景下, 并对这些场景做了一些妥协和优化授权码授权流程分为两步走, 将用户授权与下发访问令牌分开, 这给授权带来了更多的灵活性, 正常授权过程中必须经过用户登录这一步骤, 在用户已登录的前提下, 可以直接询问用户是否同意授权, 但是在一些场景下, 比如内部走 SSO(单点登录)的应用集成了基于 OAuth 授权的第三方应用, 这个时候在 OAuth 授权登录第三方应用时, 用户体验较好的流程是不需要用户再次输入用户名和密码的, 这就需要将外围 APP 的登录态传递给嵌套的应用, 但是这样是存在安全问题的, 用户的登录态必须把握在走 SSO 登录流程的应用手上, 这样的场景下授权码授权模式的两步走流程就可以满足在不交出用户登录态的情况下, 无需再次登录即可授权
内部应用可以拿着第三方应用的 client_id 等信息代替第三方应用去请求获取 code, 因为自己持有用户的登录态, 所以过程中无需用户再次输入用户名和密码, 拿到 code 之后将其交给第三方应用, 第三方应用利用 code 和自己的 client_secret 信息去请求授权服务器下发 token, 整个流程内部应用不需要交出自己持有的用户登录态, 第三方应用也无需交出自己的 client_secret 信息, 最终却能够实现在保护用户密码凭证的前提下无需再次登录即可完成整个授权流程
3.2 隐式授权模式(Implicit Grant)
对于一些纯客户端应用, 往往无法妥善的保管其客户端凭证, 同时因为没有服务器端, 所以无法向授权服务器传递凭证信息, 并且纯客户端应用在请求交互上要弱于有服务器的应用, 这时候减少交互可以让应用的稳定性和用户体验更好, 隐式授权模式是对这一应用场景的优化
隐式授权模式在安全性上要弱于授权码模式, 因为无法对当前客户端的真实性进行验证, 同时对于下发的 access_token 存在被同设备上其它应用窃取的风险, 为了降低这类风险, 隐式授权模式强制要求不能下发 refresh_token, 这一强制要求的另外一个考量是因为 refresh_token 的生命周期较长, 而客户端无法安全的对其进行存储和保护下图为授权码各个角色之间的交互时序:
整个授权流程说明如下:
客户端携带 client_id, scope, redirect_uri, state 等信息请求授权服务器下发 access_token
授权服务器验证客户端身份, 通过则询问用户是否同意授权(此时会跳转到用户能够直观看到的授权页面, 等待用户点击确认授权)
假设用户同意授权, 此时授权服务器会将 access_token 和 state 等信息以 URI Fragment 形式拼接在 redirect_uri 后面, 并以 302 形式下发
客户端利用脚本解析获取 access_token
3.2.1 请求获取访问令牌
不同于授权码模式的分两步走, 隐式授权码模式一步即可拿到访问令牌
请求参数说明:
名称 | 是否必须 | 描述信息 |
---|---|---|
response_type | 必须 | 对于授权码模式 response_type=token |
client_id | 必须 | 客户端 ID,用于标识一个客户端,在注册应用时生成 |
redirect_uri | 可选 | 授权回调地址,具体参见 2.2.3 小节 |
scope | 可选 | 权限范围,用于对客户端的权限进行控制,如果客户端没有传递该参数,那么服务器则以该应用被许可的所有权限代替 |
state | 推荐 | 用于维持请求和回调过程中的状态,防止 CSRF 攻击 ,服务器不对该参数做任何处理,如果客户端携带了该参数,则服务器在响应时原封不动的返回 |
请求参数示例:
- GET /authorize?response_type=token&client_id=s6BhdRkqt3&state=xyz&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb HTTP/1.1
- Host: server.example.com
成功响应参数说明:
名称 | 是否必须 | 描述信息 |
---|---|---|
access_token | 必须 | 访问令牌 |
token_type | 必须 | 访问令牌类型,比如 BEARER,MAC 等 |
expires_in | 推荐 | 访问令牌的生命周期,以秒为单位,表示令牌下发后多久时间过期,如果没有指定该项,则使用默认值 |
scope | 可选 | 权限范围,如果最终下发的访问令牌对应的权限范围与实际应用指定的不一致,则必须在下发访问令牌时用该参数指定说明 |
state | 可选 | 如果客户端传递了该参数,则必须原封不动返回 |
隐式授权模式不下发刷新令牌, 访问令牌以 URI Fragment 的形式拼接在授权回调地址后面以 302 形式下发, 并要求指定响应首部
Cache-Control: no-store
和 Pragma: no-cache
成功响应示例:
- HTTP/1.1 302 Found
- Location: http://example.com/cb#access_token=2YotnFZFEjr1zCsicMWpAA&state=xyz&token_type=example&expires_in=3600
错误响应参数说明:
名称 | 是否必须 | 描述信息 |
---|---|---|
error | 必须 | 错误代码 |
error_description | 可选 | 具备可读性的错误描述信息 |
error_uri | 可选 | 错误描述信息页面地址 |
state | 可选 | 如果客户端传递了该参数,则必须原封不动返回 |
授权服务器将上述元素以 URI Fragment 形式拼接在授权回调地址后面以 302 形式下发(redirect_uri 或 client_id 错误除外)
错误响应参数示例:
- HTTP/1.1 302 Found
- Location: https://client.example.com/cb#error=access_denied&state=xyz
3.3 资源所有者密码凭证授权模式(Resource Owner Password Credentials Grant)
资源所有者密码凭证授权模式建立在资源所有者充分信任客户端的前提下, 因为该模式客户端可以拿到用的登录凭证, 从而在用户无感知的情况下完成整个授权流程, 毕竟都有用户的登录凭证了, 再弹窗让用户确认授权也是多此一举
这里可能有一个比较疑惑的地方是既然已经拿到了用户的登录凭证, 为什么还需要绕一大圈子走 OAuth 授权, 拿到令牌再去请求用户的受保护资源呢? 实际中事情可能并不会这么简单, 拿到用户登录凭证的不一定是用户本身, 而且这里协议指的用户登录凭证是用户的用户名和密码, 实际中还可以是走 SSO 登录下发的 token,token 在持有权限上要小于等于用户的用户名和密码, 这是从客户端角度出发对于资源服务器来说, 有些敏感数据需要在用户级别做权限控制, 对于服务级别的控制粒度太粗, 所以这些服务往往需要服务携带 access_token 来请求某一个用户的敏感数据
举个例子来说, 比如有一个服务是获取某个用户的通讯录, 这是用户十分敏感的数据, 且一般只能授予内部应用, 如果是在服务级别进行控制, 那么只要拿到服务权限, 该应用可以请求获取任何一个用户的通讯录数据, 这是一件十分危险的事情如果基于 access_token 做鉴权, 那么就可以将粒度控制在用户级别, 前面讲的两种授权方式在这里应用时都有一个共同的缺点, 需要弹出授权页让用户确认授权, 要知道这样的场景往往是发生在内部应用里面, 内部应用是可以持有用户登录态的, 这里的确认授权对于一个用户体验好的 APP 来说就应该发生在用户登录时, 通过用户协议等方式直接告诉用户, 从而让用户在一次登录过程中可以让应用拿到用户的登录态和访问令牌资源所有者密码凭证授权模式的交互时序如下:
整个授权流程说明如下:
用于授予客户端登录凭证(比如用户名和密码信息, 亦或是 token)
客户端携带用户的登录凭证和 scope 等信息请求授权服务器下发 access_token
授权服务器验证用户的登录凭证和客户端信息的真实性, 通过则下发 access_token, 并选择性下发 refresh_token
3.3.1 用户授予登录凭证
用于登录凭证如何传递给客户端这一块协议未做说明, 实际中该类授权一般用于内部应用, 这类应用的特点就是为用户提供登录功能, 当用户登录之后, 这类应用也就持有了用户的登录态, 可以是用户登录的 session 标识, 也可以是走 SSO 下发的 token 信息
3.3.2 请求获取访问令牌
请求参数说明:
名称 | 是否必须 | 描述信息 |
---|---|---|
grant_type | 必须 | 对于本模式 grant_type=password |
username | 必须 | 用户名 |
password | 必须 | 用户密码 |
scope | 可选 | 权限范围,如果最终下发的访问令牌对应的权限范围与实际应用指定的不一致,则必须在下发访问令牌时用该参数指定说明 |
如果在注册应用时有下发客户端凭证信息(client_secret), 那么客户端必须携带该参数以让授权服务器验证客户端的真实性
请求参数示例:
- POST /token HTTP/1.1
- Host: server.example.com
- Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
- Content-Type: application/x-www-form-urlencoded
- grant_type=password&username=johndoe&password=A3ddj3w
成功响应参数说明:
名称 | 是否必须 | 描述信息 |
---|---|---|
access_token | 必须 | 访问令牌 |
token_type | 必须 | 访问令牌类型,比如 BEARER、MAC 等 |
expires_in | 推荐 | 访问令牌的生命周期,以秒为单位,表示令牌下发后多久时间过期,如果没有指定该项,则使用默认值 |
refresh_token | 可选 | 刷新令牌,选择性下发,参见 2.2.2 |
scope | 可选 | 权限范围,如果最终下发的访问令牌对应的权限范围与实际应用指定的不一致,则必须在下发访问令牌时用该参数指定说明 |
最后访问令牌以 JSON 格式响应, 并要求指定响应首部
Cache-Control: no-store
和 Pragma: no-cache
成功响应参数示例:
- HTTP/1.1 200 OK
- Content-Type: application/json;charset=UTF-8
- Cache-Control: no-store
- Pragma: no-cache
- {
- "access_token":"2YotnFZFEjr1zCsicMWpAA",
- "token_type":"example",
- "expires_in":3600,
- "refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA",
- "example_parameter":"example_value"
- }
错误响应参数说明:
名称 | 是否必须 | 描述信息 |
---|---|---|
error | 必须 | 错误代码 |
error_description | 可选 | 具备可读性的错误描述信息 |
error_uri | 可选 | 错误描述信息页面地址 |
错误响应示例:
- HTTP/1.1 400 Bad Request
- Content-Type: application/json;charset=UTF-8
- Cache-Control: no-store
- Pragma: no-cache
- {
- "error":"invalid_request"
- }
3.4 客户端凭证授权模式(Client Credentials Grant)
客户端凭证授权模式基于客户端持有的证书去请求用户的受保护资源, 如果把这里的受保护资源定义得更加宽泛一点, 比如说是对一个内网接口权限的调用, 那么这类授权方式可以被改造为内网权限验证服务客户端凭证授权模式的交互时序如下:
整个授权流程说明如下:
客户端携带客户端凭证和 scope 等信息请求授权服务器下发 access_token
授权服务器验证客户端真实性, 通过则下发 access_token
3.4.1 请求获取访问令牌:
请求参数说明:
名称 | 是否必须 | 描述信息 |
---|---|---|
grant_type | 必须 | 对于本模式 grant_type=client_credentials |
scope | 可选 | 权限范围,如果最终下发的访问令牌对应的权限范围与实际应用指定的不一致,则必须在下发访问令牌时用该参数指定说明 |
请求参数示例:
- POST /token HTTP/1.1
- Host: server.example.com
- Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
- Content-Type: application/x-www-form-urlencoded
- grant_type=client_credentials
成功响应参数说明:
名称 | 是否必须 | 描述信息 |
---|---|---|
access_token | 必须 | 访问令牌 |
token_type | 必须 | 访问令牌类型,比如 BEARER、MAC 等等 |
expires_in | 推荐 | 访问令牌的生命周期,以秒为单位,表示令牌下发后多久时间过期,如果没有指定该项,则使用默认值 |
scope | 可选 | 权限范围,如果最终下发的访问令牌对应的权限范围与实际应用指定的不一致,则必须在下发访问令牌时用该参数指定说明 |
最后访问令牌以 JSON 格式响应, 并要求指定响应首部
Cache-Control: no-store
和 Pragma: no-cache
成功响应参数示例:
- HTTP/1.1 200 OK
- Content-Type: application/json;charset=UTF-8
- Cache-Control: no-store
- Pragma: no-cache
- {
- "access_token":"2YotnFZFEjr1zCsicMWpAA",
- "token_type":"example",
- "expires_in":3600,
- "example_parameter":"example_value"
- }
错误响应参数说明:
名称 | 是否必须 | 描述信息 |
---|---|---|
error | 必须 | 错误代码 |
error_description | 可选 | 具备可读性的错误描述信息 |
error_uri | 可选 | 错误描述信息页面地址 |
错误响应示例:
- HTTP/1.1 400 Bad Request
- Content-Type: application/json;charset=UTF-8
- Cache-Control: no-store
- Pragma: no-cache
- {
- "error":"invalid_request"
- }
四. 本篇小结
本文介绍了 OAuth 2.0 授权协议的理论知识, OAuth 2.0 被广泛应用于第三方授权登录, 很多其它的协议都可以基于该协议进行改造, 比如前面提到的 SSO, 作为开发人员, 还是建议对该协议或多或少有些了解如果要自己实现一个授权和鉴权服务, 该协议为我们绘制指明了思路, 但是还有很多细节实现需要我们再去查阅各种资料和实践
关于 token 的生成最后再补充一点, OAuth 2.0 协议只是一笔带过的说它是一个字符串, 用于表示特定的权限生命周期等, 却没有明确阐述 token 的生成策略, 以及如何去验证一个 token 协议不去详细阐述, 个人觉得是因为这一块是与具体业务绑定的, 无法完全做到抽象, 并且在这一块去做详细的规定, 意义也不大
Token 本质上就是对用户授权这一操作在时间和权限范围两个维度上的一个表征, 协议可以对 token 的传递和基本验证做相应规定, 但是具体的一个 token 包含哪些元素, 采用什么样的生成算法还是需要由自己去把握一些文档, 比如参考文献 3 和 4 都为 token 的生成进行了扩展说明, 鉴于篇幅, 不再展开
参考文献
- RFC5849 - The OAuth 1.0 Protocol
- RFC6749 - The OAuth 2.0 Authorization Framework
- RFC6750 - The OAuth 2.0 Authorization Framework: Bearer Token Usage
- HTTP Authentication: MAC Authentication (draft-hammer-oauth-v2-mac-token-02)
来源: https://segmentfault.com/a/1190000013659546