术→技巧, 研发

OAuth 2.0 授权认证详解

钱魏Way · · 0 次浏览

Auth2.0 协议简介

关于应用系统用户身份管理需求,包括身份认证、权限授权、单点登录、联合身份认证等业务场景,业界有一堆的标准和规范,比如单点登录的CAS、Kerberos,第三方身份认证OpenID,第三方用户授权OAuth,联合身份认证和授权数据标准SAML等。每种技术有各自的应用场景,也存在交叉场景,想要把他们搞清楚,需要了解各种技术的工作原理和应用场景。今天就从其中一个技术开始,对OAuth2.0用户授权框架做一个简单介绍,想对框架全面了解的可以参考框架的标准RFC6749。

Oauth使用场景:

  • 第三方登录(确定登录的身份等信息)
  • API鉴权(确定请求方是否被许可)

在OAuth之前,HTTP Basic Authentication, 即用户输入用户名&密码的形式进行验证, 这种形式是不安全的。OAuth的出现就是为了解决访问资源的安全性以及灵活性。举一个通俗的例子,用户把照片、视频、联系人数据存储在内容托管云服务R(Resource)中的Picture、Video、Contact三个模块中;用户使用在线照片打印服务P(Printer),用户需要让P服务读取R服务中的照片进行打印,但不想让P服务读取R服务中的其他数据。

传统方式下,用户只能向P服务提供R服务的用户名密码,P服务通过用户名密码登录R服务,读取照片,并且不能限制P服务读取的范围。

使用OAuth框架,通过以下授权流程,在不暴露用户密码的情况下,向P服务授予有限的操作S服务的权限,整体流程如下:

  • 用户登录P服务,点击获取R服务权限的链接。
  • 浏览器跳转到R服务,用户登录R服务后,跳出向P服务授予权限的界面。
  • 用户选择授予Picture模块、只读、有效期1小时三个权限的授权选项,并提交。
  • 页面跳转回P服务,并携带R服务生产的授权码(Picture模块只读权限)。
  • P服务获得授权码,通过授权码(附加clint_id和client_secret)向R服务发起读取Picture模块请求。
  • R服务验证用户信息和授权码后,向P服务提供Picture的读取权限。

OAuth发展至今,共有三个版本,分别为:初始化版本OAuth1.0;漏洞修复版本OAuth 1.0a;不向后兼容的OAuth2.0版本。2.0版本主要是修复了前面版本的安全漏洞,对授权的流程进行了优化,提供了更丰富的使用场景,由于优化精简了授权的步骤,所以不能向后兼容。

OAuth 2.0的授权认证流程

OAuth 2.0 的核心概念

根据RFC描述,OAuth 2.0定义了4种服务角色,分别描述如下:

  • 资源所有者 Resource Owner,能够授予对受保护资源的访问权限的实体,当资源所有者是人员时,资源所有者就是最终用户。
  • 资源服务器 Resource Server,托管受保护资源的服务器,能够使用访问令牌(Access Token)接受和响应受保护的资源请求。
  • 客户端 Client,代表资源所有者,经其授权后向受保护资源发起请求的应用程序。
  • 授权服务器 Authorization Server,授权服务器对资源所有者进行认证并获取授权后,向客户端颁发访问令牌(Access Token)

在认证和授权的过程中涉及的一些概念:

访问令牌(access token)

访问令牌是在用户授权许可下,授权服务器下发给客户端的一个授权凭证,该令牌所要表达的意思是“用户授予该APP在多少时间范围内允许访问哪些与自己相关的服务”,所以访问令牌主要在 时间范围 和 权限范围 两个维度进行控制,此外访问令牌对于客户端来说是非透明的,外在表现就是一个字符串,客户端无法知晓字符串背后所隐藏的用户信息,因此不用担心用户的登录凭证会因此而泄露。

刷新令牌(refresh token)

刷新令牌的作用在于更新访问令牌,访问令牌的有效期一般较短,这样可以保证在发生访问令牌泄露时,不至于造成太坏的影响,但是访问令牌有效期设置太短存在的副作用就是用户需要频繁授权,虽然可以通过一定的机制进行静默授权,但是频繁的调用授权接口,之于授权服务器也是一种压力,这种情况下就可以在下发访问令牌的同时下发一个刷新令牌,刷新令牌的有效期明显长于访问令牌,这样在访问令牌失效时,可以利用刷新令牌去授权服务器换取新的访问令牌,不过协议对于刷新令牌没有强制规定,是否需要该令牌是客户端可以自行选择。

回调地址(redirect uri)

OAuth2.0 是一类基于回调的授权协议,在授权码模式中,整个授权需要分为两步进行,第一步下发授权码,第二步根据第一步拿到的授权码请求授权服务器下发访问令牌。

OAuth 在第一步下发授权码时,是将授权码以参数的形式添加到回调地址后面,并以 302 跳转的形式进行下发,这样简化了客户端的操作,不需要再主动去触发一次请求,即可进入下一步流程,但若在客户端请求过程中修改了对应的回调地址,并指向其他的服务器,使客户端的授权码被盗用,或使用户被引导至恶意站点而被攻击,此外,还会使授权服务器变成“请求发送器”,以授权服务器为代理请求目标地址,消耗授权服务器性能的同时,对目标地址服务器产生 DDOS 攻击。

为了避免上述安全隐患,OAuth 协议强制要求客户端在注册时填写自己的回调地址,这个回调地址的目的是为了让回调请求能够到达客户端自己的服务器,从而可以走获取访问令牌的流程。客户端可以同时配置多个回调地址,并在请求授权时携带一个地址,服务器会验证客户端传递上来的回调地址是否与之前注册的回调地址相同,或者前者是后者集合的一个元素,只有在满足这一条件下才允许下发授权码,同时协议还要求两步请求客户端携带的回调地址必须一致,通过这些措施来保证回调过程能够正常达到客户端自己的服务器,并继续后面拿授权码换取访问令牌的流程。

权限范围(scope)

访问令牌自带过期时间,可以在时间维度上对授权进行控制,而在范围维度上,OAuth 引入了一个 scope 的概念。scope 可以看做是一个对象,包含一个权限的 ID,名称,以及描述信息等,比如“获取您的基本资料(头像、昵称)”。应该在接入账号服务时必须向第三方登录服务提供方申请响应的 scope,并在请求授权时指明该参数(否则表明获取该应用所允许的所有权限),这些权限在用户确认授权时,必须毫无保留的展示给用户,以让用户知道该APP需要获取用户的哪些数据或服务。

认证思路与流程

OAuth 在”客户端”与”服务提供商”之间,设置了一个 授权层(authorization layer)。”客户端”不能直接登录”服务提供商”,只能登录授权层,以此将用户与客户端区分开来。”客户端”登录授权层所用的令牌(token),与用户的密码不同,用户可以在登录的时候,指定授权层令牌的权限范围和有效期。”客户端”登录授权层以后,”服务提供商”根据令牌的权限范围和有效期,向”客户端”开放用户储存的资料。

官方 RFC 6749 文件中的 OAuth 2.0 流程图有点晦涩,优化了 一下:

  1. 用户访问第三方应用程序(简称:客户端)以后,客户端要求用户给予授权。
  2. 用户同意给予客户端授权。
  3. 客户端使用第 2 步获得的授权,向认证服务器申请令牌。
  4. 认证服务器对客户端进行认证以后,确认无误,同意发放令牌。
  5. 客户端使用令牌,向资源服务器申请获取资源。
  6. 资源服务器确认令牌无误,同意向客户端开放资源。

上述中的第 2 步 是关键,即用户怎样才能给于客户端授权。有了这个授权以后,客户端就可以获取令牌,进而凭令牌获取资源。

OAuth2.0 的四种模式

OAuth2.0 相对于 1.0 版本在授权模式上做了更多的细化,已定义的授权模式分为四种:授权码模式(Authorization Code Grant),隐式授权模式(Implicit Grant),资源所有者密码凭证模式(Resource Owner Password Credentials Grant),以及客户端凭证模式(Client Credentials Grant)。

授权码模式(authorization code)

授权码模式在整个授权流程上与 1.0 版本最贴近,但是整个流程还是要简化了许多,也是 OAuth2.0 中最标准,应用最广泛的授权模式。这类授权模式非常适合于具备服务端的应用,当然现在大多数 APP 都有自己的服务端,所以大部分 APP 的 OAuth 授权都可以采取授权码模式,下图为授权码各个角色之间的交互时序(这里让用户直接参与其中,省略了用户代理)。

整个授权流程说明如下:

  • 客户端携带 client_id, scope, redirect_uri, state 等信息引导用户请求授权服务器的授权端点下发 code
  • 授权服务器验证客户端身份,验证通过则询问用户是否同意授权(此时会跳转到用户能够直观看到的授权页面,等待用户点击确认授权)
  • 假设用户同意授权,此时授权服务器会将 code 和 state(如果客户端传递了该参数)拼接在 redirect_uri 后面,以302形式下发 code
  • 客户端携带 code, redirect_uri, 以及 client_secret 请求授权服务器的令牌端点下发 access_token (这一步实际上中间经过了客户端的服务器,除了 code,其它参数都是在应用服务器端添加)
  • 授权服务器验证客户端身份,同时验证 code,以及 redirect_uri 是否与请求 code 时相同,验证通过后下发 access_token,并选择性下发 refresh_token

获取授权码

授权码是授权流程的一个中间临时凭证,是对用户确认授权这一操作的一个暂时性的证书,其生命周期一般较短,协议建议最大不要超过10分钟,在这一有效时间周期内,客户端可以凭借该暂时性证书去授权服务器换取访问令牌。

请求参数说明:

名称 是否必须 描述信息
response_type 必须 对于授权码模式 response_type=code
client_id 必须 客户端ID,用于标识一个客户端,等同于appId,在注册应用时生成
redirect_uri 可选 授权回调地址,具体参见 2.2.3 小节
scope 可选 权限范围,用于对客户端的权限进行控制,如果客户端没有传递该参数,那么服务器则以该应用的所有权限代替
state 推荐 用于维持请求和回调过程中的状态,防止CSRF攻击,服务器不对该参数做任何处理,如果客户端携带了该参数,则服务器在响应时原封不动的返回

请求参数示例:

GET /authorize?response_type=code&client_id=s6BhdRkqt3&state=xyz&redirect_uri=https://client.example.com/cb 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

下发访问令牌

授权服务器的授权端点在以 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,用于标识一个客户端,等同于appId,在注册应用时生成
  • 如果在注册应用时有下发客户端凭证信息(client_secret),那么客户端必须携带该参数以让授权服务器验证客户端的有效性。
  • 针对客户端凭证需要多说的一点就是,不能将其传递到客户端,客户端无法保证凭证的安全,凭证应该始终留在应用的服务器端,当下发code回调请求到应用服务器时,在服务器端携带上凭证再次请求下发令牌。

请求参数示例:

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://client.example.com/cb

授权服务器需要验证客户端的有效性,以及是否与之前请求授权码的客户端是同一个(请求授权时的信息可以记录在 code,或以 code 为 key 建立缓存),授权服务器还要保证code 处于生命周期内(推荐10分钟内有效),且只能被使用一次。授权服务器验证通过之后,生成 access_token,并选择性下发 refresh_token,OAuth2.0 协议明确了 token 的下发策略,对于生成策略没有做太多说明。

成功响应参数说明:

名称 是否必须 描述信息
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"
}

授权码授权流程分为两步走,将用户授权与下发 token 分开,这给授权带来了更多的灵活性,正常授权过程中必须经过用户登录这一步骤,在用户已登录的前提下,可以直接询问用户是否同意授权,但是在一些场景下,比如内部走 SSO 登录的应用集成了基于 OAuth 登录的第三方应用,这个时候在 OAuth 授权登录第三方应用时用户体验较好的流程是不需要用户再一次输入用户名和密码登录的,这就需要将外围APP 的登录态传递给该应用,但是这样是存在安全问题的,用户的登录态必须把握在走SSO 登录流程的应用中,这样的场景下授权码授权模式的两步走流程就可以满足在不交出用户登录态的情况下,无需再次登录即可授权。

内部应用可以拿着第三方应用的client_id 等信息代替第三方应用去请求获取 code,因为自己持有用户的登录态,所以过程中无需用户再次输入用户名和密码,拿到 code 之后将其交给第三方应用,第三方应用利用 code 和自己的 client_secret 信息去请求授权服务器下发 token,整个流程内部应用不需要交出自己持有的用户登录态,第三方应用也无需交出自己的 client_secret 信息,最终却能够实现在保护用户登录凭证的前提下无需再次登录即可完成整个授权流程。

令牌的刷新

为了防止客户端使用一个令牌无限次数使用,令牌一般会有过期时间限制,当快要到期时,需要重新获取令牌,如果再重新走授权码的授权流程,对用户体验非常不好,于是 OAuth 2.0 允许用户自动更新令牌。

具体方法是,B 网站颁发令牌的时候,一次性颁发两个令牌,一个用于获取数据,另一个用于获取新的令牌(refresh token 字段)。令牌到期前,用户使用 refresh token 发一个请求,去更新令牌。

https://b.com/oauth/token?grant_type=refresh_token&client_id=CLIENT_ID&client_secret=CLIENT_SECRET&refresh_token=REFRESH_TOKEN

上面 URL 中:

  • grant_type参数为refresh_token表示要求更新令牌,此处的值固定为refresh_token,必选项;
  • client_id参数和client_secret参数用于确认身份;
  • refresh_token参数就是用于更新令牌的令牌。

B 网站验证通过以后,就会颁发新的令牌。

注意: 第三方应用服务器拿到刷新令牌必须存于服务器,通过后台进行重新获取新的令牌,以保障刷新令牌的保密性。

隐式授权模式(Implicit Grant)

有些 Web 应用是纯前端应用,没有后端。这时就不能用上面的方式了,必须将令牌储存在前端。RFC 6749 就规定了第二种方式,允许直接向前端颁发令牌。这种方式没有授权码这个中间步骤,所以称为(授权码)”隐藏式”(implicit)。

+----------+
| Resource |
|  Owner   |
|          |
+----------+
     ^
     |
    (B)
+----|-----+          Client Identifier     +---------------+
|         -+----(A)-- & Redirection URI --->|               |
|  User-   |                                | Authorization |
|  Agent  -|----(B)-- User authenticates -->|     Server    |
|          |                                |               |
|          |<---(C)--- Redirection URI ----<|               |
|          |          with Access Token     +---------------+
|          |            in Fragment
|          |                                +---------------+
|          |----(D)--- Redirection URI ---->|   Web-Hosted  |
|          |          without Fragment      |     Client    |
|          |                                |    Resource   |
|     (F)  |<---(E)------- Script ---------<|               |
|          |                                +---------------+
+-|--------+
  |    |
 (A)  (G) Access Token
  |    |
  ^    v
+---------+
|         |
|  Client |
|         |
+---------+

它的步骤如下:

  • (A)客户端将用户导向认证服务器。
  • (B)用户决定是否给于客户端授权。
  • (C)假设用户给予授权,认证服务器将用户导向客户端指定的”重定向URI”,并在URI的Hash部分包含了访问令牌。
  • (D)浏览器向资源服务器发出请求,其中不包括上一步收到的Hash值。
  • (E)资源服务器返回一个网页,其中包含的代码可以获取Hash值中的令牌。
  • (F)浏览器执行上一步获得的脚本,提取出令牌。
  • (G)浏览器将令牌发给客户端。

下面是上面这些步骤所需要的参数。

A步骤中,客户端发出的HTTP请求,包含以下参数:

  • response_type:表示授权类型,此处的值固定为”token”,必选项。
  • client_id:表示客户端的ID,必选项。
  • redirect_uri:表示重定向的URI,可选项。
  • scope:表示权限范围,可选项。
  • state:表示客户端的当前状态,可以指定任意值,认证服务器会原封不动地返回这个值。
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

C步骤中,认证服务器回应客户端的URI,包含以下参数:

  • access_token:表示访问令牌,必选项。
  • token_type:表示令牌类型,该值大小写不敏感,必选项。
  • expires_in:表示过期时间,单位为秒。如果省略该参数,必须其他方式设置过期时间。
  • scope:表示权限范围,如果与客户端申请的范围一致,此项可省略。
  • state:如果客户端的请求中包含这个参数,认证服务器的回应也必须一模一样包含这个参数。
HTTP/1.1 302 Found
Location: http://example.com/cb#access_token=2YotnFZFEjr1zCsicMWpAA&state=xyz&token_type=example&expires_in=3600

在上面的例子中,认证服务器用HTTP头信息的Location栏,指定浏览器重定向的网址。注意,在这个网址的Hash部分包含了令牌。

根据上面的D步骤,下一步浏览器会访问Location指定的网址,但是Hash部分不会发送。接下来的E步骤,服务提供商的资源服务器发送过来的代码,会提取出Hash中的令牌。

资源所有者密码凭证模式(Resource Owner Password Credentials Grant)

如果你高度信任某个应用,RFC 6749 也允许用户把用户名和密码,直接告诉该应用。这通常用在用户对客户端高度信任的情况下,比如客户端是操作系统的一部分,或者由一个著名公司出品。而认证服务器只有在其他授权模式无法执行的情况下,才能考虑使用这种模式。该应用就使用你的密码,申请令牌,这种方式称为”密码式”(password)。

+----------+
| Resource |
|  Owner   |
|          |
+----------+
     v
     |    Resource Owner
    (A) Password Credentials
     |
     v
+---------+                                  +---------------+
|         |>--(B)---- Resource Owner ------->|               |
|         |         Password Credentials     | Authorization |
| Client  |                                  |     Server    |
|         |<--(C)---- Access Token ---------<|               |
|         |    (w/ Optional Refresh Token)   |               |
+---------+                                  +---------------+

它的步骤如下:

  • (A)用户向客户端提供用户名和密码。
  • (B)客户端将用户名和密码发给认证服务器,向后者请求令牌。
  • (C)认证服务器确认无误后,向客户端提供访问令牌。

B步骤中,客户端发出的HTTP请求,包含以下参数:

  • grant_type:表示授权类型,此处的值固定为”password”,必选项。
  • username:表示用户名,必选项。
  • password:表示用户的密码,必选项。
  • scope:表示权限范围,可选项。
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

C步骤中,认证服务器向客户端发送访问令牌,下面是一个例子。

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"
}

整个过程中,客户端不得保存用户的密码。

客户端凭证模式(Client Credentials Grant)

最后一种方式是凭证式(client credentials),适用于没有前端的命令行应用,即在命令行下请求令牌。

当客户端请求访问其控制下的受保护资源时,客户端只能使用其客户端凭据(或其他支持的身份验证方法)来请求访问令牌。或者其他资源拥有者之前与授权服务器安排好的资源拥有者(其方法不在本文的范围之内)。客户端凭据授予类型必须仅供机密客户端使用。

+---------+                                  +---------------+
|         |                                  |               |
|         |>--(A)- Client Authentication --->| Authorization |
| Client  |                                  |     Server    |
|         |<--(B)---- Access Token ---------<|               |
|         |                                  |               |
+---------+                                  +---------------+

它的步骤如下:

  • (A)客户端向认证服务器进行身份认证,并要求一个访问令牌。
  • (B)认证服务器确认无误后,向客户端提供访问令牌。

A步骤中,客户端发出的HTTP请求,包含以下参数:

  • granttype:表示授权类型,此处的值固定为”clientcredentials”,必选项。
  • scope:表示权限范围,可选项。
POST /token HTTP/1.1
Host: server.example.com
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials

认证服务器必须以某种方式,验证客户端身份。

B步骤中,认证服务器向客户端发送访问令牌。

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"
}

参考链接:

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注