关于 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 的流程是怎样的:
首先你需要向 Google API 注册你的应用程序,注册完毕之后你会拿到认证信息(credentials)包括
ID 和 secret。不是所有的程序类型都有 secret。
接下来就要向 Google 请求 access token。这里我们先忽略一些细节,例如请求参数(当然需要上面申请到的 secret)以及不同类型的程序的请求方式等。重要的是,如果你想访问的是用户资源,这里就会提醒用户进行授权。
如果用户授权完毕。Google 就会返回 access token。又或者是返回授权代码(authorization code),你再通过代码取得 access token
token 获取到之后,就能够带上 token 访问 API 了
OAuth
SSO (Single sign-on)
通常公司内部会有非常多的工具平台供大家使用,比如人力资源,代码管理,日志监控,预算申请等等。如果每一个平台都实现自己的用户体系的话无疑是巨大的浪费,所以公司内部会有一套公用的用户体系,用户只要登陆之后,就能够访问所有的系统。这就是单点登录(SSO: Single Sign-On)
SSO 是一类解决方案的统称,而在具体的实施方面,我们有两种策略可供选择:1) SAML 2.0 ; 2) OAuth 2.0。接下来我们区别这两种授权方式有什么不同。
但是在描述不同的策略之前,我们先叙述几个共有的,并且相当重要的概念。
Authentication VS Authorisation
Authentication: 身份鉴别,以下简称认证
Authorisation: 授权
认证的作用在于认可你有权限访问系统,用于鉴别访问者是否是合法用户;而授权用于决定你有访问哪些资源的权限。大多数人不会区分这两者的区别,因为站在用户的立场上。而作为系统的设计者来说,这两者是有差别的,这是不同的两个工作职责,我们可以只需要认证功能,而不需要授权功能,甚至不需要自己实现认证功能,而借助 Google 的认证系统,即用户可以用 Google 的账号进行登陆。
OAuth 2.0
我们先简单了解 SSO 下的 OAuth 2.0 的流程。
用户通过客户端(可以是浏览器也可以是手机应用)想要访问 SP 上的资源,但是 SP 告诉用户需要进行认证,将用户重定向至 IdP
IdP 向用户询问 SP 是否可以访问用户信息,如果用户同意,IdP 向客户端返回 access code
客户端拿 code 向 IdP 换 access token,并拿着 access token 向 SP 请求资源
SP 接受到请求之后拿着附带 token 向 IdP 验证用户的身份
那么 OAuth 是如何避免 SAML 流程下无法解析 POST 内容的信息的呢?用户从 IdP 返回客户端的方式是通过 URL 重定向,这里的 URL 允许自定义schema,所以即使在手机上也能拉起应用;另一方面因为 IdP 向客户端传递的是 code,而不是 XML 信息,所以 code 可以很轻易的附着在重定向 URL 上进行传递。
JWT
首先我们需要从感性上认识 JWT。本质上来说 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
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 译为「签名」
创建签名要分以下几个步骤:
- 你需要从接口服务端拿到密钥,假设为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 究竟带来了什么
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 做一次合法性的验证。这样也间接达到了认证用户的目的。