原文链接:https://uriotnews.com/?s=oauth2.0+and+jwt
从基于计算机的应用出现伊始,几乎每个开发者在其职业生涯内都会面对的一个最常见也是最复杂的问题,就是安全性(security)。这类问题意味着要考虑理解由谁提供什么数据/信息,此外还有关乎时间、校验、再校验等诸如此类的很多其他方面的事情。
而和安全性相关的所有关注点都可以被分解成两类问题:认证(Authentication) 和 授权(Authorization)。
虽然这两个术语常常交替着使用,但它们本质上表示了不同的功用。让我们试着擦亮记忆,再一次来定义这些概念。
认证是这样一种验证过程:通过让用户、网站、应用程序通过提供合法证书或验证方式,以证明他们符合自己所宣称的身份。认证经常通过用户名和密码证实,有时也会辅以一些其他的只为用户所知的信息。这类信息或元素称为因子(factors)。基于这些因子,任何认证机制都可以划分为以下三类:
授权指的是一个验证某用户能访问什么的过程。在授权过程中,某用户/应用程序的权限级别被确定后,才被允许访问特定的 APIs/模块。通常,授权发生在用户身份被 认证 之后。
授权是通过使用“策略(policies)”和“规则(rules)”来实现的。
虽然认证和授权常常交替着使用,但可以试着用一个“苏打水和鸡尾酒”的比喻来理解:二者殊为不同 -- 苏打水作为一种原材料,可以被用来制作多种不同的饮料,也可以单独饮用;而鸡尾酒则是一种由多种成分构成的混合品,苏打水也可能是其中之一,但不会只包含这一种。
如此说来,说苏打水等同于鸡尾酒或鸡尾酒就是冒泡的苏打水都是不正确的。相似的是,认证和授权也不是同样的术语;实现得好的话它们可以相得益彰,但本质上是不同的。
认证 | 授权 | |
---|---|---|
1 | 确定用户所宣称的身份 | 确定用户可访问的权限 |
2 | 通过合法凭证校验用户 | 通过规则和策略校验访问 |
3 | 早于授权 | 在认证成功后执行 |
4 | 通过 ID tokens 实现 | 用 Access Tokens 实现 |
在真实场景中,要结合使用认证和授权以保护资源。当你能证明自己的身份之前,不应该被允许访问资源;而即使证明了身份,若无访问权限,依然应被拒绝。
要实现认证和授权有多种途径,但时下最流行的是 “基于令牌(token-based)” 的方法。
基于令牌的认证和授权(Token-based authentication/authorization)是这样一种技术:当用户在某处输入一次其用户名和密码后,作为交换会得到一个唯一生成的已加密令牌。该令牌随后会替代登陆凭证,用以访问受保护的页面或资源。
这种方式的要点在于确保每个发往服务器的请求都伴随着一个已签名的令牌,服务器利用该令牌核验真实性之后才对当次请求做出响应。
一个“令牌”就是服务器生成的一段数据,包含了唯一性识别一个用户的信息,一般被生成为一长串随机字符和数字。
比如看起来可能像这样:cc7112734bbde748b7708b0284233419
,或更复杂些比如: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJtZXNzYWdlIjoiSldUIFJ1bGVzISIsImlhdCI6MTQ1OTQ0ODExOSwiZXhwIjoxNDU5NDU0NTE5fQ.\-yIVBD5b73C75osbmwwshQNRC7frWUYrqa TjTpza2y4
。
这个令牌本身是无意义和无用的,但结合适当的令牌化系统,就会变成保证应用安全性的重要一环。
比之于传统的 cookies 等手段,使用令牌有如下好处:
尽管具体实现各有不同,但基本上都涉及以下步骤:
虽说并无关于该如何实现你的应用的限制,但 IETF(Internet Engineering Task Force,互联网工程任务组) 还是定义了一些标准。其中最流行的有两个:
我们已经刷新了关于认证和授权的认知,并将了解基于令牌认证的常识。在本章节中,来看看最常用的一种实现:OAuth 2.0
。
在传统 C/S 模型中,客户端(client)通过让服务器认证资源拥有者(resource owner)的凭证来请求服务端受保护的资源。资源拥有者会将自己的凭证分享给第三方应用(third-party applications),让后者得以访问受限资源。这种凭证分享行为会造成若干问题和限制,其中的一些如下所列:
OAuth 针对这些问题提出了引入一个认证层,并把客户端(client)的角色与资源拥有者的角色分离开来。所以:
OAuth 是一种授权协议,以允许用户将对其在一个站点上的资源的受限访问许可给另一个站点,而不必公开其凭据
OAuth 为客户端提供一种“安全代理访问”能力,用以代表资源拥有者访问服务器资源。OAuth 指定了这样一个过程:资源拥有者在不分享其凭证的前提下授权第三方访问其服务器资源。
下面举个例子来说明:
你知道有些小轿车的“泊车钥匙”吧?如果还未曾耳闻的话,那就是有些车型(没错,就是特别奢侈的那些!)附有一种特别的钥匙,可以在泊车时交给服务员。和你的正常钥匙不同的是,这种钥匙不允许汽车开出去一两英里那么远。
某些泊车钥匙打不开后备箱,另一些则访问不了车载电话的通讯录。无论此类限制是什么,思路都是一样清晰的:你让某人通过特殊的钥匙有限访问你的车,但你的正常钥匙能解锁一切。
类似的,OAuth 中的“泊车钥匙”就是 访问令牌(Access Tokens),通过其允许对资源的不同级别的访问。
角色(Roles): OAuth2.0 规范定义了四种角色。
令牌(Tokens): 令牌有两种类型。
不同于请求令牌,更新令牌专为授权服务器设计,不会发送给资源服务器。
授权许可(Authorization Grant): 授权许可是一种表示资源拥有者之认可(访问其受保护资源)的凭证,被客户端用于获取访问令牌。OAuth 2.0 规范定义了四种许可类型:
处理 OAuth 2.0 时理解这些术语是至关重要的。所以,也试着用一个例子来说明。
想象一个地铁运输系统。典型的引导流程如下:一位乘客(commuter)从售票机或售票窗口购买车票,制票系统许可这张车票在有限的时间或站点数量之间是合法的。而后,乘客在闸机验票,车票合法则准许进入,即可乘坐列车。
以上场景可以和下面的 OAuth 2.0 中的角色对应起来:
乘客 (客户端) 打算利用地铁 (受保护的资源),所以他/她得先向售票机或售票窗口 (资源服务器) 买票。制票系统 (授权服务器) 代表地铁部门 (资源拥有者) 以车票 (令牌) 为依据许可访问。
一次 OAuth 2.0 的流程可用下图表示:
OAuth 2.0 把认证从授权决策中解耦。恰当设计的 OAuth 2.0 令牌既可以支持细粒度授权,也可以支持粗粒度授权。对于任何从另一处(服务器/应用)访问存储在某处的资源/数据的场景,OAuth 2.0 可说是最适用的方法之一了。
以下列出一些场景,我们将尝试通过一个可穿戴设备的例子理解 OAuth 2.0 用例。
就拿运动手环来说吧,假设 Alice 买了一个,并用移动端上配套的手环 app 跟踪并分析运动过程。那么流程会是什么样呢?
客户端密码: (尽管使用了 OAuth 2.0 的认证应该被避免). Alice 不必创建一个新密码;取而代之的是,她使用自己在 FriendBook 服务器上已经创建的密码。
Web 服务器: 可穿戴设备的 app 不必每次操作都发起登录。Alice 要从 FriendBook 上分享或拉取数据,手环 app 将能够以服务器对服务器的方式访问那些数据。
用户代理: 手环 app 扮演了其应用服务器的代理人的角色,用来从主服务器上同步数据。由于使用了 OAuth 2.0 对此授权,该代理可以准确访问服务器上的资源(数据)。
下面来看看 JWT。
JSON Web Token (JWT),通常读作 “jot”,是一个定义了以 JSON 对象紧凑而自包含的在各方之间安全传输信息的标准。其包含了声明方面的信息,特别的被用于如 HTTP 等空间受约束的环境;该信息可被验证,也是可信的,因为经过了数字化签名。JWT 可以用 密钥(如 HMAC) 或 公钥私钥对(RSA 或 ECDSA) 签名。
JWT 的两个特性是:
这些令牌可以是被签名的、被加密的,或两者皆有。签名过的令牌被用来验证令牌完整性,而加密过的令牌用来隐藏声明。
注意:正如名称所暗示的,JWT 是 JSON 形式的,也就意味着其包含键值对。虽说在 JSON 合法和有关方一致性方面,对键和值有多长并无限制,但大多数标准都遵循了 3个字母 的键格式。
JWT 表现为由点(.
)分割的三个字符串组成的一个序列,典型的格式看起来如下:
AAAAA.BBBBB.CCCCC
三个子串分别称作 头部(Header) 、 负载(Payload) 和 签名(Signature),下面逐一讲解:
虽说只要相关几方之间有共识,则在头部中放什么是没有限制的,但通常由两部分组成:
JWT
HMAC
、RSA
、SHA
JWT 的第二部分表示负载,这部分由声明(claims)组成。
所谓声明就是关于实体和任意附加数据的信息。在一段 JWT 中,声明由键表示。这些声明是依赖上下文的,且应该相应的被处理和被理解,但依每种规范会有若干标准规则应用于声明:
一个可能的负载例子:
{
"sub": "1234567890", "name": "Alice", "admin": true
}
负载中的声明又可以细分为以下三种类型:
有一些声明注册在 IANA(https://dzone.com/refcardz/core-json) 的 “JSON Web 令牌声明” 注册表中。这些声明并非是在所有情况下都要求强制使用或实现的,准确的说它们是作为提供一个有用的集合的起始点而被注册的。
其中一些有必要了解的是:
此类声明的名字可被 JWT 使用者任意定义。但为了预防冲突,任何新名字都应该注册在 IANA “JSON Web Token Claims”
注册表中,或将其定义为包含防冲突命名空间的 URI 等。
在任何情况下,对名字和值的定义都要考虑到合理的预防措施,以确保它们在其定义的命名空间中受控。
这可以理解为是创建自定义声明以在应用内共享信息规格,可以是除以上两种外的任意声明名字。与公有声明不同,私有声明受制于冲突问题,要小心使用。
签名先是通过对头部和负载 Base64 编码而生成,其后会与一个密钥联合,最好被头部中指定的算法签名。
签名被用于校验 JWT 的发送者是否名实相符,以及信息在传送过程中是否被更改。比如,如果创建了一个使用 HMAC SHA256 算法之令牌的签名,你会像下面这样做:
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
观察如下 JWT 签名:
eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzYXRpc2giLCJhdWQiOiJteWFwcCIsIkNVU1QiOiIxIiwiZXhwIjoxNTY2MjE0NTg1LCJpc3MiOiJhdXRoLWFwcCJ9.WknG6jiM_vAaflLnKyjlXh5BrM4MUJR9dFrVx-XE3zRVWiyXeIVzI-OomFh0vVHRwrK3-Tttg0HyKBTnCA3mSg
该签名使用了 HS512 算法编码,并包含了如下信息:
Header: {
"alg": "HS512",
"typ": "JWT"
}
Payload: {
"sub": "satish",
"aud": "myapp",
"CUST": "1",
"exp": 1566214585,
"iss": "my-auth-app"
}
当用户使用其凭证成功登录后,一个 ID 令牌会被返回。按照 OpenID Connect (OIDC) 规范,该 ID 令牌就是一个 JWT。
一旦用户登录成功,应用就可能会代表用户请求访问路由、服务、资源等。为此,将使用一个访问令牌,形式上可能就是 JWT。每个后续的请求也都包含该访问令牌。由于 JWT 开销很小,也能轻易用于跨域名访问,单点登录(SSO,Single Sign-on)广泛使用这项技术。
由于可被签名,JWT 是一种在多方间安全传递信息的良好方式,这意味着你能确定发送者名实相符。另外,一个 JWT 结构允许你验证内容没有被篡改过。
JWT 最大的优势(比之于使用内存内随机令牌的用户 session 管理)就是其使得对第三方服务器认证逻辑的代理可以:
认证逻辑/服务器可以从应用服务器完全分离,无需在应用间再分享密码摘要。
由于 JWT 是自包含的,且无需在内存中保持请求之间的令牌,所以应用服务器可以做到完全无状态(stateless)。认证服务器可以颁发令牌,将其发回后就立即丢弃掉。
JSON 比 XML 简介,所以当其被编码后,一个 JWT 比 SAML 令牌更小。这使得 JWT 成为一个在 HTML 和 HTTP 环境中传送的好选择。
为了签名,JWT 可以使用一个公钥/私钥对,表现为 X.509 证书的形式。一个 JWT 也可以通过分享使用了 HMAC 算法的密钥而被对称签名。同时虽然 SAML 令牌也可以使用 JWT 这样的公钥/私钥对,但相比于签名 JSON 的简单性,想用 XML 数字签名算法签名 XML 却不会引入未知的安全漏洞是非常困难的。
因为直接映射到对象,JSON 处理器在大多数编程语言中都更常见。相反,XML 没有自然的 文档到对象 的映射。这意味着 JWT 比 SAML 更易用。
JWT 为互联网规模而设计,意思就是其在用户设备上更易处理,特别是移动端。
除去以上说过的优缺点,JWT 标准也有其自身的问题:
为了应对这些调整,一些 JWT 库在标准实现之上增加了一个层,并允许更新令牌机制,同时也包含一些特性如在必要情况下强制用户重新认证等。
在动手实现 JWT 之前,让我们了解一些最佳实践,以确保基于令牌的认证恰当地用于你的应用中。
用 Spring Boot 2 和 JWT 实现基于角色的访问控制