JWT 基础知识简介
简介——什么是JWT?
无国籍?这是什么意思?
进一步细分——JWT 的组成部分
它如何保证安全?
JWT 的缺点:注销用户/使令牌无效
关于复杂性、漏洞等的注意事项:
其他资源:
如果你因为 JWT(JSON Web Tokens)听起来吓人或复杂而犹豫不决,那么我有个好消息要告诉你:它其实并不复杂。在这篇简短易懂的指南中,我将带你了解 JWT 的基础知识、它的独特之处,以及它与其他身份验证形式相比的优缺点。
简介——什么是JWT?
JSON Web Tokens,或称 JWT,是一种“无状态”(稍后会详细介绍)的身份验证和会话管理方法,其中有关身份验证或会话的信息(用户是否是管理员?权限?等等)可以存储在令牌本身内,而不是存储在服务器上。
本质上,您可以将 JWT 视为一个容器,其中装满了服务器发送的事实(在 JWT 术语中称为声明),以及有关如何存储这些“事实”的元数据(算法和类型)。
从最实际的角度来看,JWT 可以表示为单个字符串,您稍后会看到。
无国籍?这是什么意思?
JWT 并不总是无状态系统的一部分,但它们的组成方式使它们适合这种设置。
在典型的非 JWT 系统中,如果要处理身份验证并为不同的用户设置不同的权限,通常会结合使用会话和服务端存储——这就是状态。当用户登录时,您会session
为其创建一个新的状态,并赋予其唯一的 ID。为了检查权限,您需要根据权限表和/或角色表查找他们的用户 ID。您加载的每个页面都可能需要多次数据库查找,以检查会话是否有效,然后获取角色,再获取权限等等。
相比之下,使用 JWT,所有关于用户有效性(已登录)及其角色和/或权限的信息都直接包含在 JWT 字符串本身中。服务器在接收到传入的 JWT 时,只需验证其有效性,而不必进行任何数据库查找。这可以使 JWT 成为无状态的。
然而这种方法也有缺点,稍后将会讨论。
进一步细分——JWT 的组成部分
JWT 看起来非常复杂,所以让我们逐个分解一下:
它们究竟长什么样?
首先,你可能想知道 JWT 到底是什么样子的。好吧,服务器最终发送到客户端的字符串,存储在localStorage
、sessionStorage
或 cookie 中,可能如下所示(真实示例):
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiQm9iYnkgVGFibGVzIiwiaWF0IjoxNTE2MjM5MDIyLCJpc0FkbWluIjp0cnVlLCJwZXJtaXNzaW9ucyI6eyJ1c2Vyc01ha2UiOnRydWUsInVzZXJzQmFuIjp0cnVlLCJ1c2Vyc0RlbGV0ZSI6ZmFsc2V9fQ.HFRcI4qU2lyvDnXhO-cSTkhvhrTCyXv6f6wXSJKGbFk
很有启发吧?好吧,一旦你明白了它的真正含义,就没那么可怕了:
base64Url(header) + '.' + base64Url(payload) + '.' + base64Url(signature)
让我们进一步分解一下……
碎片
如上所示,JWT 由三个主要部分组成,每个部分在连接在一起之前都经过 base64 编码(使用网络安全变体,以避免出现可能破坏 URL 的字符)。除签名外,每个部分在编码前也是一个 JSON 对象,更多内容如下:
标头:
标头是包含有关 JWT 本身的元数据的部分。它至少包含用于加密签名的算法类型,但有时还会包含其他元数据,例如令牌本身的类型(JWT
)。
例子:
{
"alg": "HS256",
"typ": "JWT"
}
在这个例子中,标题说“嘿,我是 JWT 类型的令牌,加密算法为 HS256!”
有效载荷:
就功能而言,而非安全性而言,有效载荷才是最重要的部分。有效载荷包含claims
(或者我之前称之为“真相”),服务器表示该信息属于持有 JWT 的人,并且可以将其发送回服务器。
当用户通过标头、URL、cookie 等将 JWT 发送回服务器时,服务器会解码有效负载并以数千种方式使用它;例如,允许用户执行“删除”操作,因为其中一项声明是他们是管理员。
有效载荷示例:
{
"name": "Bobby Tables",
"iat": 1516239022,
"isAdmin": true,
"permissions": {
"usersMake": true,
"usersBan": true,
"usersDelete": false
}
}
您的有效负载没有强制必须包含的密钥对,但有一些标准化且通用的密钥对。例如,iat
是 的注册声明issued at
,如果包含,则必须是一个代表 JWT 签发时间戳的数字。您应该避免与保留/注册的声明名称冲突。您可以在原始规范中阅读更多相关信息,点击此处。
提醒:您应该尽量保持键和值简短,因为 JWT 的设计很小。
签名:
JWT 的签名是安全性方面最重要的部分。本质上,它是标头 + 有效负载的值,经过单向加密哈希函数处理,该函数使用只有服务器或其他受信任实体知道的密钥。
如果使用 base64 解码该值,它看起来像乱码,因为它是一个安全的哈希:
.T\#..Ú\¯.uá;ç.NHo.´ÂÉ{ú.¬.H..lY
在服务器上生成此哈希的伪代码如下所示:
HMACSHA256(
base64Url(header) + "." +
base64Url(payload),
SECRET_KEY
);
签名是服务器使用其密钥**验证用户发送的 JWT 是否有效以及是否由其自身或受信任的创建者创建的一种方式。
(PS:我在此处的示例中使用的密钥是krusty-krab-krabby-patty-secret-formula
)。
** = 如果使用非对称加密,则可以使用公钥进行验证。请参阅“如何保证安全?”部分下的“签名算法”小节。
故障总结——重新组合
因此,为了总结并再次展示这些部分如何组合在一起,我们采取以下措施:
const header = {
"alg": "HS256",
"typ": "JWT"
};
const payload = {
"name": "Bobby Tables",
"iat": 1516239022,
"isAdmin": true,
"permissions": {
"usersMake": true,
"usersBan": true,
"usersDelete": false
}
};
...使用单向加密算法进行签名...
// Pseudo code
const signature = hashHS256(header, payload).withSecret(SECRET_KEY);
...最后,对每个部分进行 base64 转换后,将各部分连接成一个字符串:
// Pseudo code
const jwtString = base64Url(header) + '.' + base64Url(payload) + '.' + base64Url(signature);
它如何保证安全?
JWT 的安全机制在于其签名组件(如上所述)。进一步细分,当用户将 JWT 发送回服务器时,服务器会通过以下方式验证签名,以确保声明可信:
- 接收传入的标头和有效载荷
- 添加
SECRET_KEY
只有服务器和验证器知道** - 将标头、有效载荷和秘密通过单向加密哈希
- 这将创建一个服务器生成的签名
- 检查新创建的签名是否与用户在 JWT 中发送的签名匹配
- 如果它们匹配,则证明用户发送的 JWT 确实是使用服务器拥有的相同秘密创建的!
请注意,安全性的强度与您的密钥密切相关 - 使用适当长的密钥(以避免暴力破解尝试),并且永远不会共享。
附注:除了验证签名之外,服务器通常还会检查 JWT 是否符合预期的结构和标准。详情请参阅auth0 指南。
** = 或公钥,如果使用非对称加密,请参见下文。
签名算法
在这些示例中,我使用的是 HMAC,它是一种对称算法,这意味着只有一个私钥,没有公钥。为了让多方能够创建或验证 JWT,他们必须拥有私钥。
使用非对称算法,虽然密钥仍然用于创建令牌,但公钥也可用于验证其有效性。这意味着其他服务器可以创建 JWT,而您的服务器仍然可以通过与公钥进行比对来验证它,而无需共享私钥/密钥。这种方法通常是首选,因为它允许多方进行验证,而只有拥有私钥的人才可以真正创建令牌。
我不会在这里进一步讨论细节,因为这应该是一个介绍,但你可以在这里阅读更多内容。
JWT 的缺点:注销用户/使令牌无效
到目前为止,JWT 似乎比有状态身份验证方法具有许多优势,但我们尚未讨论的是注销用户、删除用户或以其他方式使令牌无效或撤销。
事实上,这正是 JTW 的不足之处。为了使 JWT 失效,你需要某种数据库/状态系统,因为你最终要做的就是维护黑名单或白名单。
有了黑名单,每当您想要使令牌无效时,您都会将其添加到黑名单表中,并且每当用户尝试使用 JWT 时,您总是需要检查它不在黑名单上。
使用白名单,基本原理相同,但每个 token 在创建时都会添加到白名单中,并在失效时从白名单中移除。并且,每个传入的 JWT 只有在白名单中才会被接受。
如果您必须使用其中任何一种方法,在我看来,这表明您的需求与 JWT 所能提供的不一致,并且可能是时候重新考虑您的网站架构,看看是否有更好的选择。
避免状态:自动过期会话
对于避免必须实施用于注销用户的有状态 JWT 系统的网站来说,一个相当常见的解决方法是快速自动使令牌过期。
例如,您可以将 JWT 的创建时间或计划的过期时间放入其自身的有效负载中。然后,在验证传入的 JWT 时,只需检查过期时间是否早于或等于当前时间 - 如果是,则拒绝该 JWT。
问题在于,它仍然只是权宜之计,最终可能比一开始就使用会话更复杂。为了避免用户在使用网站时不断被注销(真烦人!),你需要使用自动刷新的 JWT,可能通过Auth0 的 Refresh Tokens之类的服务来实现。这仍然不支持手动注销流程,除非你同时引入撤销功能作为刷新模式的一部分。
避免状态:惰性失效
想象一下,你需要大幅升级你的用户管理系统,并强制所有人切换。不幸的是,网上很多开发者都建议一种不太理想的解决方法,那就是……更改你的私钥。如果你这样做,所有流通的 JWT 都会立即失效,并强制所有人重新登录,因为每个人的 JWT 的签名部分将不再匹配。
关于复杂性、漏洞等的注意事项:
关于 JWT,我还有很多内容没有在这篇文章中完整讲解(我找个借口,这篇文章应该只是个介绍)。事实上,许多开发者使用第三方服务(又称联合身份管理服务),例如Auth0,来管理 JWT 的大部分功能。这并不能神奇地解决我之前提到的许多 JWT 问题,但它可以让使用起来更容易,并抽象出一些复杂性和安全隐患。
其他资源:
附言:如果您喜欢本指南的写作风格,您可能会喜欢我的其他一些速查表和快速参考指南。我一直在收集它们并在这里展示。
文章来源:https://dev.to/joshuatz/simple-intro-to-jwt-basics-2kd8