仓库源文站点原文


layout: post title: 不要用JWT替代session管理(上):全面了解Token,JWT,OAuth,SAML,SSO modified: 2018-06-24 tags: [back-end] image: feature: abstract-3.jpg credit: dargadgetz creditlink: http://www.dargadgetz.com/ios-7-abstract-wallpaper-pack-for-iphone-5-and-ipod-touch-retina/ comments: true

share: true

通常为了弄清楚一个概念,我们需要掌握十个概念。在判断 JWT (Json Web Token) 是否能代替 session 管理之前,我们要了解什么是 token,以及 access token 和 refresh token 的区别;了解什么是 OAuth,什么是 SSO,SSO 下不同策略 OAuth 和 SAML 的不同,以及 OAuth 与 OpenID 的不同,更重要的是区分 authorisation 和 authentication;最后我们引出 JSON WEB TOKEN,聊聊 JWT 在 session 管理方面的优势和劣势,同时尝试解决这些劣势,看看成本和代价有多少

本文关于 OAuth 授权和 API 调用实例都来自 Google API。

关于 Token

token 即使是在计算机领域中也有不同的定义,这里我们说的token,是指访问资源的凭据。例如当你调用Google API,需要带上有效 token 来表明你请求的合法性。这个 token 是 Google 给你的,这代表 Google 给你的授权使得你有能力访问 API 背后的资源。

请求 API 时携带 token 的方式也有很多种,通过 HTTP Header 或者 url 参数 或者 google 提供的类库都可以:

// HTTP Header:
GET /drive/v2/files HTTP/1.1
Authorization: Bearer <token>
Host: www.googleapis.com/

// URL query string parameter
GET https://www.googleapis.com/drive/v2/files?token=<token>

// Python:
from googleapiclient.discovery import build
drive = build('drive', 'v2', credentials=credentials)

更具体的说,上面用于调用 API 的 token 我们称为细分为 access token。通常 access token 是有有效期限的,如果过期就需要重新获取。那么如何重新获取?现在我们要让时光倒流一会,回顾第一次获取 token 的流程是怎样的:

  1. 首先你需要向 Google API 注册你的应用程序,注册完毕之后你会拿到认证信息(credentials)包括 ID 和 secret。不是所有的程序类型都有 secret。
  2. 接下来就要向 Google 请求 access token。这里我们先忽略一些细节,例如请求参数(当然需要上面申请到的 secret)以及不同类型的程序的请求方式等。重要的是,如果你想访问的是用户资源,这里就会提醒用户进行授权。
  3. 如果用户授权完毕。Google 就会返回 access token。又或者是返回授权代码(authorization code),你再通过代码取得 access token
  4. token 获取到之后,就能够带上 token 访问 API 了

流程如下图所示: google token flow

注意在第三步通过 code 兑换 access token 的过程中,Google 并不会仅仅返回 access token,还会返回额外的信息,这其中和之后更新相关的就是 refresh token

一旦 access token 过期,你就可以通过 refresh token 再次请求 access token。

以上只是大致的流程,并且故意省略了一些额外的概念。比如更新 access token 当然也可以不需要 refresh token,这要根据你的请求方式和访问的资源类型而定。

这里又会引起另外的两个问题:

  1. 如果 refesh token 也过期了怎么办?这就需要用户重新登陆授权了
  2. 为什么要区分 refresh token 和 access token ?如果合并成一个 token 然后把过期时间调整的更长,并且每次失效之后用户重新登陆授权就好了?这个问题会和后面谈的相关概念有关,稍后再回答

OAuth

从获取 token 到使用 token 访问接口。这其实是标准的 OAuth 2.0 机制下访问 API 的流程。这一节我们聊一聊 OAuth 里外相关的概念,更深入的理解 token 的作用。

SSO (Single sign-on)

通常公司内部会有非常多的工具平台供大家使用,比如人力资源,代码管理,日志监控,预算申请等等。如果每一个平台都实现自己的用户体系的话无疑是巨大的浪费,所以公司内部会有一套公用的用户体系,用户只要登陆之后,就能够访问所有的系统。这就是单点登录(SSO: Single Sign-On)

SSO 是一类解决方案的统称,而在具体的实施方面,我们有两种策略可供选择:1) SAML 2.0 ; 2) OAuth 2.0。接下来我们区别这两种授权方式有什么不同。

但是在描述不同的策略之前,我们先叙述几个共有的,并且相当重要的概念。

Authentication VS Authorisation

认证的作用在于认可你有权限访问系统,用于鉴别访问者是否是合法用户;而授权用于决定你有访问哪些资源的权限。大多数人不会区分这两者的区别,因为站在用户的立场上。而作为系统的设计者来说,这两者是有差别的,这是不同的两个工作职责,我们可以只需要认证功能,而不需要授权功能,甚至不需要自己实现认证功能,而借助 Google 的认证系统,即用户可以用 Google 的账号进行登陆。

Authorization Server/Identity Provider(IdP) VS Service Provider(SP)/Resource Server

把负责认证的服务称为 Authorization Server 或者 Identity Provider,以下简称 IdP;而负责提供资源(API调用)的服务称为 Resource Server 或者 Service Provider,以下简称 SP

SMAL 2.0

下图是 SMAL 2.0 的流程图,看图说话

smal flow

当用户在 IdP 登陆成功之后,IdP 需要将用户再次重定向至 SP 站点,这一步通常有两个办法:

如果你的应用是基于 web,那么以上的方案没有任何问题。但如果你开发的是一个 iOS 或者 Android 的手机应用,那么问题就来了:

虽然 POST 的 url 可以拉起应用,但是手机应用无法解析 POST 的内容,我们也就无法读取 SAML Token

当然还是有办法的,比如在 IdP 授权阶段不跳转至系统的 Safari 浏览器,在内嵌的 webview 中解决,在想方设法从 webview 中提取 token,或者利用代理服务器。但无论如何,SAML 2.0 并不适用于当下跨平台的场景,这也许与它产生的年代也有关系,它诞生于 2005 年,在那个时刻 HTTP POST 确实是最好的选择方案

OAuth 2.0

我们先简单了解 SSO 下的 OAuth 2.0 的流程。

oauth flow

那么 OAuth 是如何避免 SAML 流程下无法解析 POST 内容的信息的呢?用户从 IdP 返回客户端的方式是通过 URL 重定向,这里的 URL 允许自定义schema,所以即使在手机上也能拉起应用;另一方面因为 IdP 向客户端传递的是 code,而不是 XML 信息,所以 code 可以很轻易的附着在重定向 URL 上进行传递

但以上的 SSO 流程体现不出 OAuth 的本意。OAuth 的本意是一个应用允许另一个应用在用户授权的情况下访问自己的数据,OAuth 的设计本意更倾向于授权而非认证(当然授权用户信息就间接实现了认证), 虽然 Google 的 OAuth 2.0 API 同时支持授权和认证。所以你在使用 Facebook 或者 Gmail 账号登陆第三方站点时,会出授权对话框告诉你第三方站点可以访问你的哪些信息,需要征得你的同意:

OAuth2Consent

在上面 SSO 的 OAuth 流程中涉及三方角色: SP, IdP 以及 Client。但在实际工作中 Client 可以是不存在的,例如你编写了一个后端程序定时的通过 Google API 从 Youtube 拉取最新的节目数据,那么你的后端程序需要得到 Youtube 的 OAuth 授权即可。

OAuth VS OpenId

如果你有留心的话,你会在某些站点看到允许以 OpenID 的方式登陆,其实也就是以 Facebook 账号或者 Google 账号登陆站点:

openid

这听上去似乎和 OAuth 很像。但本质上来说它们是截然不同用户的两个东西:

Refresh Token

现在我们可以回答本篇第一小节的那个问题了:为什么我们需要 refresh token?

这样的处理是为了职责的分离:refresh token 负责身份认证,access token 负责请求资源。虽然 refresh token 和 access token 都由 IdP 发出,但是 access token 还要和 SP 进行数据交换,如果公用的话这样就会有身份泄露的可能。并且 IdP 和 SP 可能是完全不同的服务提供的。而在第一小节中我们之所以没有这样的顾虑是因为 IdP 和 SP 都是 Google

refresh token

总结

这一小节我们重点了解了 OAuth,以及关于身份认证和授权的区别。现在我们可以把上一小节的知识关联起来,也更加能理解 token:token 其实是为 OAuth 服务的,它是访问数据的一把钥匙。接下来我们看看这把钥匙的另一种形态:Json Web Token, 简称 JWT

JWT

感性认识

首先我们需要从感性上认识 JWT。本质上来说 JWT 也是 token,正如我们第一小节学习到的,它是访问资源的凭证

Google 的一些 API 诸如 Prediction API 或者 Google Cloud Storage,是不需要访问用户的个人数据的,因而不需要经过用户的授权这一步骤,应用程序可以直接访问。就像上一节 OAuth 中没有 Client 没有参与的流程类似。这就要借助 JWT 完成访问了, 具体流程如下

serviceaccount

甚至你可以不需要向 Google 索要 access token,而是携带 JWT 作为 HTTP header 里的 bearer token 直接访问 API 也是可以的。我认为这才是 JWT 的最大魅力

理性认识

JWT 顾名思义,它是 JSON 结构的 token,由三部分组成:1) header 2) payload 3) signature

header

header 用于描述元信息,例如产生 signature 的算法:

{
    "typ": "JWT",
    "alg": "HS256"
}

其中alg关键字就指定了使用哪一种哈希算法来创建 signature

payload

payload 用于携带你希望向服务端传递的信息。你既可以往里添加官方字段(这里的“字段” (field) 也可以被称作“声明” claims),例如iss(Issuer), sub(Subject), exp(Expiration time),也可以塞入自定义的字段,比如 userId:

{
    "userId": "b08f86af-35da-48f2-8fab-cef3904660bd"
}

signature

signature 译为「签名」

创建签名要分以下几个步骤:

如果上述描述还不直观,用伪代码表示就是:

// signature algorithm
data = base64urlEncode( header ) + “.” + base64urlEncode( payload )
signature = Hash( data, secret );

假设我们的原始 JSON 结构是这样的:

// Header
{
  "typ": "JWT",
  "alg": "HS256"
}
// Payload:
{
  "userId": "b08f86af-35da-48f2-8fab-cef3904660bd"
}

如果密钥是字符串secret的话,那么最终 JWT 的结果就是这样的

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOiJiMDhmODZhZi0zNWRhLTQ4ZjItOGZhYi1jZWYzOTA0NjYwYmQifQ.-xN_h82PHVTCMA9vdoHrcZxH-x5mb11y1537t3rGzcM

你可以在 jwt.io 上验证这个结果

JWT 究竟带来了什么

JWT 的目的不是为了隐藏或者保密数据,而是为了确保数据确实来自被授权的人创建的(不被篡改)

回想一下,当你拿到 JWT 时候,你完全可以在没有 secret 的情况下解码出 header 和 payload,因为 header 和 payload 只是经过了 base64 编码(encode)而已,编码的目的在于利于数据结构的传输。虽然创建 signature 的过程近似于加密 (encrypt),但本质其实是一种签名 (sign) 的行为,用于保证数据的完整性,实际上也并且并没有加密任何数据

关于 Encoding, Encryption, Hashing之间的差异,可以参考这篇文章:Encoding vs. Encryption vs. Hashing vs. Obfuscation

用于接口调用

接下来在 API 调用中就可以附上 JWT (通常是在 HTTP Header 中)。又因为 SP 会与程序共享一个 secret,所以后端可以通过 header 提供的相同的 hash 算法来验证签名是否正确,从而判断应用是否有权力调用 API

有状态的对话

因为 HTTP 是无状态的,所以客户端和服务端需要解决的如何让之间的对话变得有状态。例如只有是登陆状态的用户才有权限调用某些接口,那么在用户登陆之后,需要记住该用户是已经登陆的状态。常见的方法是使用 session 机制

常见的 session 模型是这样工作的:

how session work

相信你已经察觉了,理论上来说,JWT 机制可以取代 session 机制。用户不需要提前进行登陆,后端也不需要 Redis 记录用户的登陆信息。客户端的本地保存一份合法的 JWT, 当用户需要调用接口时,附带上该合法的 JWT,每一次调用接口,后端都使用请求中附带的 JWT 做一次合法性的验证。这样也间接达到了认证用户的目的

然而 JWT 真的能取代 session 机制吗?这么做有哪些好处和坏处?这些问题我们留在下一篇再讨论

参考资料集合

https://www.site2share.com/folder/20020530

Google API

JWT

Refresh Token

Token VS Cookie

Oauth

OpenID VS Oauth

SAML VS Oauth

你可能会喜欢