一般为了弄清楚一个概念,咱们须要掌握十个概念。在判断 JWT(JsonWebToken)
是否能代替 session
管理以前,咱们要了解什么是 token
,以及 access token
和 refresh token
的区别。python
了解什么是 OAuth
,什么是 SSO
,SSO
下不一样策略 OAuth
和 SAML
的不一样,以及 OAuth
与 OpenID
的不一样,更重要的是区分 authorisation
和 authentication
。算法
最后咱们引出 JSON WEB TOKEN
,聊聊 JWT
在 Session
管理方面的优点和劣势,同时尝试解决这些劣势,看当作本和代价有多少。编程
本文关于 OAuth
受权 和 API
调用实例都来自 Google API
。json
Token
即便是在计算机领域中也有不一样的定义,这里咱们说的 token
,是指 访问资源 的凭据。例如当你调用 Google API
时,须要带上有效 token
来代表你请求的 合法性。这个 Token
是 Google
给你的,这表明 Google
给你的 受权 使得你有能力访问 API
背后的 资源。后端
请求 API
时携带 token
的方式也有不少种,经过 HTTP Header
或者 url
参数或者 google
提供的类库均可以:api
GET /drive/v2/files HTTP/1.1
Authorization: Bearer <token>
Host: www.googleapis.com/
复制代码
GET https://www.googleapis.com/drive/v2/files?token=<token>
复制代码
from googleapiclient.discovery import build
drive = build('drive', 'v2', credentials=credentials)
复制代码
更具体的说,上面用于调用 API
的 token
,咱们称为细分为 access token
。一般 access token
是有 有效期限 的,若是 过时 就须要 从新获取。那么如何从新获取?先看看第一次获取 token
的流程是怎样的:浏览器
首先须要向 Google API
注册一个应用程序,注册完毕以后就会拿到 认证信息(credentials
)包括 ID
和 secret
。不是全部的程序类型都有 secret
。缓存
接下来就要向 Google
请求 access token
。这里先忽略一些细节,例如请求参数(固然须要上面申请到的 secret
)。重要的是,若是你想访问的是 用户资源,这里就会提醒用户进行 受权。bash
若是 用户受权 完毕。Google
就会返回 access token
。又或者是返回 受权代码(authorization code
),再经过代码取得 access token
。服务器
token
获取到以后,就可以带上 token
访问 API
了。
流程以下图所示:
注意:在第三步经过
authorization code
兑换access token
的过程当中,access token
,还会返回额外的信息,这其中和以后更新相关的就是refresh token
。
一旦 access token
过时,你就能够经过 refresh token
再次请求 access token
。
以上只是大体的流程,而且故意省略了一些额外的概念。好比更新 access token
固然也能够不须要 refresh token
,这要根据你的 请求方式 和访问的 资源类型 而定。
这里又会引发另外的两个问题:
若是 refesh token
也过时了怎么办?这时就须要用户 从新登录受权。
为何要区分 refresh token
和 access token
?若是合并成一个 token
而后把 过时时间 调整的 更长,而且每次 失效 以后用户 从新登录受权 就行了?这个问题会和后面谈的相关概念有关,后面会给予解释说明。
从获取 token
到使用 token
访问接口。这实际上是标准的 OAuth2.0
机制下访问 API
的流程。这里介绍一下 OAuth
里外相关的概念,更深刻的理解 token
的做用。
一般公司内部会有很是多的平台供你们使用,好比人力资源,代码管理,日志监控,预算申请等等。若是每个平台都实现本身的用户体系的话无疑是巨大的浪费,因此公司内部会有一套 公用的用户体系,用户只要登录以后,就可以 访问全部的系统。这就是 单点登陆。
SSO
是一类 解决方案 的统称,而在具体的实施方面,咱们有两种策略可供选择:
SAML 2.0
OAuth 2.0
接下来咱们区别这 两种受权方式 有什么不一样。可是在描述 不一样的策略 以前,咱们先叙述几个 共有的特性,而且至关重要的概念。
Authentication: 身份鉴别,如下简称 认证;
Authorisation: 资源访问 受权。
认证 的做用在于 承认 你可以访问系统,用于 鉴别访问者 是不是 合法用户;而 受权 用于决定你有访问 哪些资源的权限。
大多数人不会区分这二者的区别,由于站在用户的立场上。而做为系统的设计者来讲,这二者是有差异的,这是不一样的两个工做职责。咱们能够只须要 认证功能,而不须要 受权功能,甚至不须要本身实现 认证功能。而借助 Google
的认证系统,即用户能够用 Google
的帐号进行登录。
把负责 认证的服务 称为 AuthorizationServer
或者 IdentityProvider
,如下简称 IDP
。
把负责 提供资源(API
调用)的服务称为 ResourceServer
或者 ServiceProvider
,如下简称 SP
。
下图是 SAML2.0
的流程图,看图说话:
还 未登录 的用户 打开浏览器 访问你的网站(SP
),网站 提供服务 可是并 不负责用户认证。
因而 SP
向 IDP
发送了一个 SAML
认证请求,同时 SP
将 用户浏览器 重定向到 IDP
。
IDP
在验证完来自 SP
的 请求无误 以后,在浏览器中呈现 登录表单 让用户填写 用户名 和 密码 进行登录。
一旦用户登录成功, IDP
会生成一个包含 用户信息(用户名 或者 密码)的 SAML token
(SAML token
又称为 SAML Assertion
,本质上是 XML
节点)。IDP
向 SP
返回 token
,而且将 用户重定向 到 SP
(token
的返回是在 重定向步骤 中实现的,下面会详细说明)。
SP
对拿到的 token
进行验证,并从中解析出 用户信息,例如 用户是谁 以及 用户的权限 有哪些。此时就可以根据这些信息容许用户访问咱们网站的内容。
当用户在 IDP
登录成功以后,IDP
须要将用户 再次重定向 到 SP
站点,这一步一般有两个办法:
HTTP
重定向:这并不推荐,由于 重定向 的 URL
长度 有限制,没法携带更长的信息,好比 SAML Token
。
HTTP POST
请求:这个是更常规的作法,当用户登录完毕以后渲染出一个表单,用户点击后向 SP
提交 POST
请求。又或者可使用 JavaScript
向 SP
发出一个 POST
请求。
若是你的应用是基于 Web
,那么以上的方案没有任何问题。但若是你开发的是一个 iOS
或者 Android
的手机应用,那么问题就来了:
用户在 iPhone
上打开应用,此时用户须要经过 IDP
进行认证。
应用跳转至 Safari
浏览器,在登录认证完毕以后,须要经过 HTTP POST
的形式将 token
返回至 手机应用。
虽然 POST
的 url
能够 拉起应用,可是 手机应用 没法解析 POST
的内容,咱们也就没法读取 SAML Token
。
固然仍是有办法的,好比在
IDP
受权阶段 不跳转至系统的Safari
浏览器,在 内嵌 的Webview
中解决,在千方百计从Webview
中提取token
,或者利用 代理服务器。
不管如何,SAML 2.0
并 不适用 于当下 跨平台 的场景,这也许与它产生的年代也有关系,它诞生于 2005
年,在那个时刻 HTTP POST
确实是最好的选择方案。
咱们先简单了解 SSO
下的 OAuth2.0
的流程。
用户经过 客户端(能够是 浏览器 也能够是 手机应用)想要访问 SP
上的资源,可是 SP
告诉用户须要进行 认证,将用户 重定向 至 IDP
。
IDP
向 用户 询问 SP
是否能够访问 用户信息。若是用户赞成,IDP
向 客户端 返回 authorization code
。
客户端拿到 authorization code
向 IDP
交换 access token
,并拿着 access token
向 SP
请求资源。
SP
接受到请求以后,拿着附带的 token
向 IDP
验证 用户的身份。确认身份无误后,SP
向 客户端 发放相关资源。
那么 OAuth
是如何避免 SAML
流程下 没法解析 POST
内容的信息的呢?
一方面是用户从 IDP
返回 客户端 的方式,也是经过 URL
重定向,这里的 URL
容许 自定义 schema
,因此即便在 手机 上也能 拉起应用;
另外一方面由于 IDP
向 客户端 传递的是 authorization code
,而不是 XML
信息,因此 code
能够很轻易的附着在 重定向 URL
上进行传递。
但以上的 SSO
流程体现不出 OAuth
的本意。OAuth
的本意是 一个应用 容许 另外一个应用 在 用户受权 的状况下 访问本身的数据。
OAuth
的设计本意更倾向于 受权而非认证(固然受权用户信息就间接实现了认证),虽然 Google
的 OAuth 2.0 API
同时支持 受权 和 认证。因此你在使用 Facebook
或者 Gmail
帐号登录第三方站点时,会出现 受权对话框,告诉你 第三方站点 能够访问你的哪些信息,须要征得你的赞成。
在上面 SSO
的 OAuth
流程中涉及三方角色: SP
, IDP
以及 Client
。但在实际工做中 Client
能够是不存在的,例如你编写了一个 后端程序 定时的经过 Google API
从 Youtube
拉取最新的节目数据,那么你的 后端程序 须要获得 Youtube
的 OAuth
受权 便可。
若是你有留心的话,你会在某些站点看到容许以 OpenID
的方式登录,其实也就是以 Facebook
帐号或者 Google
帐号登录站点:
OpenID
和 OAuth
很像。但本质上来讲它们是大相径庭的两个东西:
OpenID: 只用于 身份认证(Authentication
),容许你以 同一个帐户 在 多个网站登录。它仅仅是为你的 合法身份 背书,当你以 Facebook
帐号登录某个站点以后,该站点 无权访问 你的在 Facebook
上的 数据。
OAuth: 用于 受权(Authorisation
),容许 被受权方 访问 受权方 的 用户数据。
如今能够回答上面的问题了,为何咱们须要 refresh token
?
这样的处理是为了 职责的分离:
refresh token: 负责 身份认证;
access token: 负责 请求资源。
虽然 refresh token
和 access token
都由 IDP
发出,可是 access token
还要和 SP
进行 数据交换,若是 公用的话 这样就会有 身份泄露 的可能。而且 IDP
和 SP
多是 彻底不一样 的 服务提供 的。而在上文,咱们之因此没有这样的顾虑是由于 IDP
和 SP
都是 Google
。
本质上来讲 JWT
也是 token
,正如咱们在上文提到的,它是 访问资源 的 凭证。
Google
的一些 API
诸如 Prediction API
或者 Google Cloud Storage
,是不须要 访问 用户的 我的数据 的。于是不须要通过 用户的受权 这一步骤,应用程序能够直接访问。就像上面 OAuth
中没有 Client
没有参与的流程相似。这就要借助 JWT
完成访问了, 具体流程以下:
首先须要在 Google API
上建立一个服务帐号(service account
)。
获取 服务帐号 的 认证信息(credential
),包括 邮箱地址,client ID
,以及一对 公钥/私钥。
使用 Client ID
和 私钥 创一个 签名 的 JWT
,而后将这个 JWT
发送给 Google
交换 access token
。
Google
返回 access token
。
程序经过 access token
访问 API
。
甚至你能够不须要向 Google
索要 access token
,而是携带 JWT
做为 HTTP header
里的 bearer token
直接访问 API
也是能够的。这才是 JWT
的最大魅力。
JWT
顾名思义,它是 JSON
结构的 token
,由三部分组成:
header
payload
signature
header
用于描述 元信息,例如产生 signature
的算法:
{
"typ": "JWT",
"alg": "HS256"
}
复制代码
其中 alg
关键字就指定了使用哪种 哈希算法 来建立 signature
。
payload
用于携带你但愿 向服务端传递 的信息。你既能够往里添加 官方字段,例如:iss(Issuer)
, sub(Subject)
, exp(Expirationtime)
,也能够塞入 自定义的字段,好比 userId
:
{
"userId": "b08f86af-35da-48f2-8fab-cef3904660bd"
}
复制代码
signature
译为 签名,建立签名要分如下几个步骤:
从 接口服务端 拿到 密钥,假设为 secret
。
对 header
进行 base64
编码,假设结果为 headerStr
。
将 payload
进行 base64
编码,假设结果为 payloadStr
。
将 headerStr
和 payloadStr
用 .
字符 拼装起来成为字符 data
。
以 data
和 secret
做为参数,使用 哈希算法 计算出 签名。
若是上述描述还不直观,用 伪代码 表示就是:
// 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
时候,你彻底能够在没有 secret
的状况下解码出 header
和 payload
,由于 header
和 payload
只是通过了 base64
编码(encode
)而已,编码的目的在于 利于数据结构的传输。
虽然建立 signature
的过程近似于 加密 (encrypt
),但本质实际上是一种 签名 (sign
) 的行为,用于保证 数据的完整性,实际上也而且并 没有加密任何数据。
接下来在 API
调用中就能够附上 JWT
(一般是在 HTTP Header
中)。又由于 SP
会与程序 共享 一个 secret
,因此 程序 能够经过 header
提供的相同的 hash
算法来 验证签名 是否正确,从而判断应用是否有权力调用 API
。
由于 HTTP
是 无状态 的,因此 客户端 和 服务端 须要解决的问题是,如何让它们之间的对话变得有状态。例如只有是 登录状态 的 用户 才有权限调用某些接口,那么在 用户登录 以后,须要记住该用户是 已经登录 的状态。常见的方法是使用 session
机制。
常见的 session
模型是这样工做的:
用户在浏览器 登录 以后,服务端为用户生成 惟一 的 session id
,存储在 服务端 的 存储服务(例如 MySQL
, Redis
)中。
该 session id
也同时 返回给浏览器,以 SESSION_ID
为 KEY
存储在浏览器的 cookie
中。
若是用户再次访问该网站,cookie
里的 SESSION_ID
会随着 请求 一同发往 服务端。
服务端经过判断 SESSION_ID
是否已经在 Redis
中判断用户是否处于 登录状态。
相信你已经察觉了,理论上来讲,JWT
机制能够取代 session
机制。用户不须要提早进行登录,后端也不须要 Redis
记录用户的登录信息。客户端的本地保存一份合法的 JWT
,当用户须要调用接口时,附带上该合法的 JWT
,每一次调用接口,后端都使用请求中附带的 JWT
作一次 合法性的验证。这样也间接达到了 认证用户 的目的。
然而 JWT
真的能取代 session
机制吗?这么作有哪些好处和坏处?这些问题将留在下一篇再讨论。
欢迎关注技术公众号: 零壹技术栈
本账号将持续分享后端技术干货,包括虚拟机基础,多线程编程,高性能框架,异步、缓存和消息中间件,分布式和微服务,架构学习和进阶等学习资料和文章。