JWT(JSON Web Token)通常用于标识经过身份验证的用户。 它们由身份验证服务器签发,并由客户端-服务器(用于保护其API)使用。
如果您想了解有关JSON Web Token(JWT)的详细信息,您来对地方了。
我们将涵盖以下内容:
- JWT是什么?
- JWT的结构
- JWT声明约定
- 它们如何工作(通过示例)
- JWT的优缺点
- 开发过程中常见的问题
- 进一步的阅读材料
JWT是什么?
JSON Web令牌是一种开放的行业标准,用于在两个实体之间共享信息, 通常是客户端(如您的应用的前端)和服务器(您的应用的后端)。
它们包含需要共享的信息的JSON对象。
每个JWT还使用加密技术(哈希)签名,以确保JSON内容(也称为JWT声明)不能被客户端或恶意方篡改。
例如,当您使用Google登录时,Google会发出一个包含以下声明JSON负载的JWT:
{
"iss": "https://accounts.google.com",
"azp": "1234987819200.apps.googleusercontent.com",
"aud": "1234987819200.apps.googleusercontent.com",
"sub": "10769150350006150715113082367",
"at_hash": "HK6E_P6Dh8Y93mRNtsDB1Q",
"email": "jsmith@example.com",
"email_verified": "true",
"iat": 1353601026,
"exp": 1353604926,
"nonce": "0394852-3190485-2490358",
"hd": "example.com"
}
通过上述信息,使用Google登录的客户端应用可以准确知道最终用户是谁。
什么是令牌以及为什么需要它?
您可能会想,为什么身份验证服务器不能直接发送一个纯JSON对象,而需要将其转换为“令牌”。
如果身份验证服 务器将其作为纯JSON发送,客户端应用的API将无法验证他们接收到的内容是否正确。 例如,恶意攻击者可能会更改用户ID(上述JSON中的sub声明),应用程序的API将无法知道这种情况的发生。
由于这个安全问题,身份验证服务器需要以一种可以被客户端应用验证的方式传输此信息,这就是“令牌”概念的由来。
简单地说,令牌是一个包含某些信息的字符串,可以安全地验证。
这可以是指向数据库中ID的一组随机字母数字字符,或者可以是客户端可以自行验证的编码JSON(称为JWT)。
JWT的结构
一个JWT包含三个部分:
- Header(头部):包含两部分信息:
- 使用的签名算法。
- 令牌类型,在这种情况下,通常是“JWT”。
- Payload(负载):包含声明或JSON对象。
- Signature(签名):通过加密算法生成的字符串,可用于验证JSON负载的完整性。
JWT声明约定
您可能已经注意到,在上面Google签发的JWT示例中,JSON负载具有非显而易见的字段名。它们使用sub
、iat
、aud
等:
- iss:令牌 的签发者(在此例中为Google)
- azp和aud:Google为您的应用分配的客户端ID。这样,Google知道哪个网站在尝试使用其登录服务,网站知道JWT是专门为它们签发的。
- sub:终端用户的Google用户ID
- at_hash:访问令牌的哈希值。OAuth访问令牌不同于JWT,因为它是一个不透明的令牌。访问令牌的目的是客户端应用可以向Google查询有关登录用户的更多信息。
- email:终端用户的电子邮件ID
- email_verified:用户是否已验证其电子邮件。
- iat:JWT创建的时间(自纪元以来的毫秒数)。
- exp:JWT将过期的时间(自纪元以来的毫秒数)。
- nonce:客户端应用可以用来防止重放攻击。
- hd:用户的托管G Suite域
使用这些特殊的键名是为了遵循JWT中重要字段名称的行业惯例。遵循这一惯例,使得不同语言的客户端库能够检查由任何身份验证服务器签发的JWT的有效性。例如,如果客户端库需要检查JWT是否已过期,它只需查找iat字段。
它们如何工作(通过示例)
解释JWT如何工作最简单的方法是通过一个示例。
我们将从为特定JSON负载创建JWT开始,然后进行验证:
-
创建JSON 我们采用以下最小的JSON负载:
{
"userId": "abcd123",
"expiry": 1646635611301
} -
创建JWT签名密钥并决定签名算法 首先,我们需要一个签名密钥和一个要使用的算法。我们可以使用任何安全随机源生成签名密钥。为了本文的目的,我们使用:
- 签名密钥:
NTNv7j0TuYARvmNMmWXo6fKvM4o6nv/aUi9ryX38ZH+L1bkrnD1ObOQ8JAUmHCBq7Iy7otZcyAagBLHVKvvYaIpmMuxmARQ97jUVG16Jkpkp1wXOPsrF9zwew6TpczyHkHgX5EuLg2MeBuiT/qJACs1J0apruOOJCg/gOtkjB4c=
- 签名算法:
HMAC + SHA256
,也称为HS256
。
- 签名密钥:
-
创建Header 这包含有关使用哪个签名算法的信息。与负载类似,这也是一个JSON,并将附加到JWT的开头(因此称为
Header
):{
"typ": "JWT",
"alg": "HS256"
} -
创建签名 首先,我们删除负载JSON中的所有空格,然后对其进行base64编码,得到
eyJ1c2VySWQiOiJhYmNkMTIzIiwiZXhwaXJ5IjoxNjQ2NjM1NjExMzAxfQ
。您可以尝试将此字符串粘贴到在线base64解码器中以检索我们的JSON。同样,我们删除Header JSON中的空格并对其进行base64编码,得到
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
。我们将这两个base 64字符串连接起来,中间用
.
分隔,如<header>.<payload>
,得到eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOiJhYmNkMTIzIiwiZXhwaXJ5IjoxNjQ2NjM1NjExMzAxfQ
。这样做没有特别原因,只是为了设置一个行业可以遵循的惯例。现在,我们对上述连接字符串和密钥运行
Base64 + HMACSHA256
函数,得到签名:Base64URLSafe(
HMACSHA256("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOiJhYmNkMTIzIiwiZXhwaXJ5IjoxNjQ2NjM1NjExMzAxfQ", "NTNv7j0TuYARvmNMmWXo6fKvM4o6nv/aUi9ryX38ZH+L1bkrnD1ObOQ8JAUmHCBq7Iy7otZcyAagBLHVKvvYaIpmMuxmARQ97jUVG16Jkpkp1wXOPsrF9zwew6TpczyHkHgX5EuLg2MeBuiT/qJACs1J0apruOOJCg/gOtkjB4c=")
)结果为:
3Thp81rDFrKXr3WrY1MyMnNK8kKoZBX9lg-JwFznR-M
我们仅出于行业惯例对其进行base64编码。 -
创建JWT 最后,我们附加生成的签名,如
<header>.<body>.<signature>
,创建我们的JWT:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOiJhYmNkMTIzIiwiZXhwaXJ5IjoxNjQ2NjM1NjExMzAxfQ.3Thp81rDFrKXr3WrY1MyMnNK8kKoZBX9lg-JwFznR-M
-
验证JWT 身份验证服务器将JWT发送回客户端前端。前端将JWT附加到对客户端API层的网络请求中。API层将执行以下步骤来验证JWT:
- 获取JWT的
Header
部分(eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
)。 - 对其进行base64解码以获得纯文本JSON:
{"typ":"JWT","alg":"HS256"}
- 验证
typ
字段的值是否为JWT
和alg
是否为HS256
。如果不是,将拒绝JWT。 - 获取签名密钥并在传入的
JWT
的Header
和Body
上运行与**步骤(4)相同的Base64URLSafe(HMACSHA256(...))
操作。注意,如果传入的JWT的Body不同,此步骤将生成与步骤(4)**不同的签名。 - 检查生成的签名是否与传入
JWT
的签名相同。如果不相同,则拒绝JWT。 - 我们对
JWT
的Body
部分(eyJ1c2VySWQiOiJhYmNkMTIzIiwiZXhwaXJ5IjoxNjQ2NjM1NjExMzAxfQ
)进行base64解码,得到{"userId":"abcd123","expiry":1646635611301}
。 - 如果当前时间(以毫秒为单位)大于JSON的到期时间(因为JWT已过期),我们将拒绝JWT。
只有在通过上述所有检查后,我们才可以信任传入的JWT。
- 获取JWT的
JWT的优缺点
使用JWT有很多优点:
- 安全:JWT使用秘密(
HMAC
)或公钥/私钥对(RSA
或ECDSA
)进行数字签名,保护它们不被客户端或攻击者修改。 - 仅存储在客户端:您在服务器上生成JWT并将其发送到客户端。客户端然后在每个请求中提交JWT。这节省了数据库空间。
- 高效/无状态:验证JWT非常快速,因为它不需要数据库查找。这在大型分布式系统中尤其有用。
但是,也有一些缺点:
- 不可撤销:由于其自包含性质和无状态验证过程,在JWT自然过期前很难撤销。 因此,立即禁止用户的操作不能轻易实现。尽管如此,可以维护JWT拒绝/黑名单,通过这种方式可以立即撤销它们。
- 依赖一个密钥:创建JWT依赖一个密钥。如果该密钥被泄露,攻击者可以伪造自己的JWT,API层将接受它们。 这意味着,如果密钥被泄露,攻击者可以冒充任何用户的身份。我们可以通过定期更改密钥来降低这种风险。
总而言之,JWT对于不需要立即禁止用户操作的大型应用最为有用。
开发过程中常见的问题
JWT被拒绝:此错误表示JWT的验证过程失败。这可能是因为:
- JWT已过期
- 签名不匹配——这意味着签名密钥已更改,或者JSON主体已被篡改。
- 其他声明不符合。例如,如果Google JWT示例中的JWT是为App1生成的,但发送到App2,App2会拒绝它(因为aud声明指向App1的ID)。
JWT令牌不支持所需的范围
JWT中的声明可以表示用户授予的范围或权限。例如,终端用户可能只同意应用读取其数据,但不同意修改数据。然而,应用可能期望用户同意修改数据。在这种情况下,应用所需的范围与JWT中的范围不一致。
JWT解码失败
如果JWT格式错误,则会出现此错误。例如,客户端可能期望JWT是base64编 码的,但身份验证服务器没有进行base64编码。