在Web应用的黎明时期,身份验证的范式几乎完全由**基于服务器端会话(Session-Based Authentication)**的机制所主导。这是一个直观且在单体应用时代极其有效的模型,其工作流程如同一场精密的双人舞:
凭证交换与“储物柜钥匙”的签发:用户在登录页面输入用户名和密码。这些凭证被发送到服务器。服务器验证其有效性后,会在自己的“储物间”(内存、数据库或缓存系统)里,为这位用户开辟一个专属的“储物柜”(Session对象)。这个储物柜里存放着用户的关键信息,比如用户ID、角色、权限等级等。为了让用户能够再次找到这个储物柜,服务器会生成一把独一无二的“钥匙”(Session ID),通常是一个长而无规律的字符串。
钥匙的传递与保管:服务器通过HTTP响应,将这把“钥匙”(Session ID)发送回用户的浏览器,并指示浏览器通过Cookie机制将其妥善保管。浏览器就像一位忠实的管家,会将这把钥匙存放在一个安全的地方。
后续访问的“凭钥匙开门”:当用户浏览网站的其他页面或发起新的API请求时,浏览器会自动地、在每一个后续的HTTP请求头中,都附上这枚存储在Cookie中的“钥匙”(Session ID)。
服务器的验证与识别:服务器接收到请求后,会从请求头中取出这把“钥匙”。然后,它会拿着这把钥匙,回到自己的“储物间”,去寻找与之匹配的那个“储物柜”。如果找到了,服务器就打开储物柜,取出用户的身份信息,并确认“哦,原来是张三来了,他有管理员权限”。基于这些信息,服务器继续处理请求。如果找不到匹配的储物柜,或者钥匙已失效,服务器则认为该用户未登录或身份不明,拒绝服务。
这个模型在单台服务器主导的“单体应用(Monolithic Application)”时代,运行得非常完美。它简单、易于理解,并且安全性相对可控。然而,随着互联网技术的发展,应用架构开始向分布式、微服务化演进,传统会话机制的“阿喀琉斯之踵”便暴露无遗。
想象一下,我们的网站业务蒸蒸日上,单台服务器已经无法承受巨大的访问压力。我们引入了**负载均衡(Load Balancer)**和多台应用服务器,组成了一个服务器集群。这时,传统会话机制的噩梦开始了。
状态的孤岛效应(Stateful Silos):假设用户Alice的登录请求,被负载均衡器分发到了服务器A。服务器A验证通过,在自己的内存中创建了Alice的Session,并将Session ID sid-alice-on-server-a
返回给了Alice的浏览器。Alice的下一次请求,比如查询她的订单,被负载均衡器(为了均衡负载)分发到了服务器B。服务器B收到了sid-alice-on-server-a
这把钥匙,但当它在自己的“储物间”(服务器B的内存)里查找时,它会一脸茫然——这里根本没有这个储物柜!在服务器B看来,Alice是一个未经身份验证的陌生人,因此拒绝了她的请求。
这就是**状态性(Statefulness)**带来的核心问题。用户的身份状态被绑定在了单台物理服务器上,形成了无法跨越的“状态孤岛”。
为了“共享”而付出的沉重代价:为了解决状态孤岛问题,工程师们设计了多种“会话共享”方案,但每一种方案都带来了新的复杂性和成本:
跨域(CORS)与移动端的天然壁垒:现代Web应用通常是前后端分离的。前端(比如一个运行在app.my-domain.com
的React或Vue应用)需要调用后端部署在api.my-domain.com
的API。基于Cookie的传统会话机制,在处理这种跨域请求时会遇到诸多安全限制(如浏览器的同源策略),需要进行繁琐的CORS配置。对于原生移动应用(iOS/Android App)来说,它们没有浏览器那样的原生Cookie管理机制,要与基于Web Session的认证系统无缝集成,往往需要进行额外的、不甚优雅的适配工作。
JWT的诞生,正是为了彻底斩断这些“状态的枷锁”,引领我们进入一个无状态、可扩展、跨平台友好的认证新纪元。
JWT(JSON Web Token)提出了一种革命性的思想:为什么身份信息一定要由服务器集中保管呢?为什么不能让用户自己带着“身份证明”来访问呢?
JWT的本质,就是一个自包含的(Self-Contained)、经过加密签名的字符串。它就像一本**“数字护照”**。
这本护照里包含了:
其工作流程与传统会话截然不同:
Authorization
头中,格式为 Bearer
。exp
声明)。这种**无状态(Stateless)**的特性,带来了巨大的架构优势:
每一个JWT,无论其内部承载了多么复杂的业务信息,其外在表现形式都是一个由三个部分组成的、以点(.
)分隔的字符串。它的结构遵循着一种神圣而不可更改的“三位一体”范式:
Header.Payload.Signature
xxxxx.yyyyy.zzzzz
在深入每一个部分之前,我们必须首先澄清一个至关重要、但极易被误解的核心概念:JWT默认情况下是“签名”而非“加密”的。这意味着,JWT的Header和Payload部分,仅仅是经过了Base64Url编码,而非加密。任何拿到你的JWT的人,都可以轻易地解码前两个部分,并读取其中的内容。你可以自己尝试一下,随便找一个公开的JWT,将其第一部分或第二部分拷贝到任何一个Base64解码器中,你就能看到其原始的JSON内容。
因此,一个黄金法则必须被刻在每一位使用JWT的工程师的脑海里:绝对不要在JWT的Payload中存放任何敏感信息! 比如用户的密码、银行卡号、身份证号码等。JWT的设计目标是验证身份和传递声明,而不是保密数据。它的安全性体现在第三部分——签名,这个签名保证了前两部分的数据在传输过程中没有被篡れません(Integrity),并且它确实是由可信的签发者所签发的(Authentication)。如果需要传输加密数据,应该使用JWE(JSON Web Encryption)标准,这是与JWT(JWS, JSON Web Signature)并列的另一个标准,我们将在本书的后续高级章节中深入探讨。
现在,让我们开始对这“三位一体”的逐一解剖。
头部(Header)是一个JSON对象,它的职责是充当这份JWT的“元数据”或“使用说明书”。它告诉接收方(Verifier)关于这个令牌本身的关键信息,尤其是“如何正确地验证我”。
一个最基本的Header包含两个字段:
{
"alg": "HS256",
"typ": "JWT"
}
alg
(Algorithm): 算法声明,这是Header中最重要的字段,没有之一。它明确声明了生成第三部分“签名”所使用的加密算法。接收方在验证签名时,必须使用这里声明的算法。这个字段的值是大小写敏感的字符串,由RFC 7518(JWA, JSON Web Algorithms)规范定义。常见的算法包括:
HS256
: 使用HMAC-SHA256算法,需要一个共享的密钥。这是最常用、最简单的对称签名算法。HS384
: 使用HMAC-SHA384算法,签名更长,理论上更安全。HS512
: 使用HMAC-SHA512算法,签名最长。RS256
: 使用带SHA-256的RSASSA-PKCS1-v1_5签名算法。需要一对公私钥。RS384
: 使用带SHA-384的RSA签名。RS512
: 使用带SHA-512的RSA签名。ES256
: 使用P-256曲线和SHA-256的ECDSA签名。比RSA更高效,密钥和签名更短。ES384
: 使用P-384曲线和SHA-384的ECDSA签名。ES512
: 使用P-521曲线和SHA-512的ECDSA签名。none
: 一个极其危险的“算法”,表示此JWT没有签名。这在某些特定调试场景下可能有用,但在生产环境中必须被禁用。接受alg: none
的令牌是JWT历史上最臭名昭著的漏洞之一。一个健壮的验证库(如PyJWT
)会强制要求你明确指定一个可接受的算法列表,从根本上杜绝此类攻击。typ
(Type): 类型声明。它声明了此令牌的媒体类型。对于JWT,这个值推荐被设置为字符串"JWT"
,以表明这是一个JSON Web Token。这个字段是可选的,但在实践中,为了明确起见,通常都会包含它。
除了这两个基本字段,Header还可以包含其他一些在高级场景中非常有用的字段:
cty
(Content Type): 内容类型声明。这个字段只有在JWT的Payload本身又是一个嵌套的JWT时才使用。它告诉处理程序,Payload中的内容需要被进一步解析。例如,可以设置为"JWT"
。这在某些复杂的身份委托(Identity Delegation)场景中可能会出现。
kid
(Key ID): 密钥ID声明,这是一个在生产环境中至关重要的字段。想象一个场景:认证服务为了安全,需要定期轮换签名密钥。在某个时间点,系统中可能同时存在由旧私钥签发的、尚未过期的令牌,以及由新私钥签发的新令牌。当一个资源服务收到令牌时,它如何知道该用哪个公钥(或共享密钥)去验证它呢?kid
就是为了解决这个问题而生的。
kid
字段,其值为当前正在使用的密钥的唯一标识符(例如,一个UUID、一个时间戳或一个版本号)。kid
以及其对应的公钥。kid
的值。然后,它根据这个kid
去“密钥清单”中查找对应的公钥,并用该公钥来验证签名。编码过程:从JSON到Base64Url
正如之前所说,Header的JSON对象并不会被加密,而是被Base64Url编码。这个编码过程必须被精确地执行。Base64Url是标准Base64的一个变种,专门为在URL中安全地传输数据而设计。
它与标准Base64的区别在于:
+
字符替换为 -
(连字符)。/
字符替换为 _
(下划线)。=
字符。因为JWT的三个部分由点号分隔,所以不需要填充符来确定数据边界。让我们用纯Python代码来实现这个编码过程,以加深理解。
# 文件名: custom_base64_encoder.py
# 作用: 演示如何将一个Python字典(代表Header或Payload)进行标准的JWT Base64Url编码。
import json # 引入json库,用于将Python字典序列化为JSON字符串
import base64 # 引入base64库,用于执行标准的Base64编码
def dict_to_base64url(data: dict) -> str:
"""
接收一个字典,将其转换为JWT规范的Base64Url字符串。
Args:
data (dict): 待编码的Python字典,例如JWT的Header或Payload。
Returns:
str: 经过Base64Url编码后的字符串。
"""
# 步骤1: 将Python字典序列化为紧凑的JSON字符串。
# separators=(',', ':') 是一个关键优化,它能去除JSON字符串中所有的空格,
# 从而生成最短的JSON表示,减小最终JWT的体积。
json_string = json.dumps(data, separators=(',', ':'))
print(f"步骤1: 序列化后的紧凑JSON字符串 -> {
json_string}")
# 步骤2: 将JSON字符串编码为UTF-8格式的字节串。
# 网络传输和加密操作通常都以字节为单位进行。
utf8_bytes = json_string.encode('utf-8')
print(f"步骤2: 编码为UTF-8字节串 -> {
utf8_bytes}")
# 步骤3: 使用标准的Base64对字节串进行编码。
# 注意,这里得到的是一个字节串,且可能包含 '+' 和 '/'。
standard_base64_bytes = base64.b64encode(utf8_bytes)
print(f"步骤3: 标准Base64编码后的字节串 -> {
standard_base64_bytes}")
# 步骤4: 将标准Base64编码转换为URL安全的Base64Url编码。
# 4a. 将 '+' 替换为 '-'
url_safe_bytes = standard_base64_bytes.replace(b'+', b'-')
# 4b. 将 '/' 替换为 '_'
url_safe_bytes = url_safe_bytes.replace(b'/', b'_')
print(f"步骤4: 替换特殊字符后的URL安全字节串 -> {
url_safe_bytes}")
# 步骤5: 去除末尾的填充 '=' 字符。
# rstrip(b'=') 会从字节串的右侧(末尾)移除所有的 '=' 字符。
no_padding_bytes = url_safe_bytes.rstrip(b'=')
print(f"步骤5: 去除填充符'='后的最终字节串 -> {
no_padding_bytes}")
# 步骤6: 将最终的字节串解码为ASCII或UTF-8字符串,作为JWT的一部分。
base64url_string = no_padding_bytes.decode('ascii')
print(f"步骤6: 解码为最终的字符串形式 -> {
base64url_string}")
return base64url_string
# --- 示例:对一个包含kid的Header进行编码 ---
if __name__ == '__main__':
# 定义一个较为复杂的Header
header_data = {
"alg": "RS256",
"typ": "JWT",
"kid": "auth-key-v1-20240520"
}
print("--- 开始对Header进行Base64Url编码 ---")
encoded_header = dict_to_base64url(header_data)
print("\n--- 编码完成 ---")
print(f"原始Header字典: {
header_data}")
print(f"最终生成的Base64Url编码 (JWT的第一部分): {
encoded_header}")
# 验证一下
# eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImF1dGgta2V5LXYxLTIwMjQwNTIwIn0
# 这是一个正确的编码结果
通过这个手动实现的过程,我们不再将Base64Url编码视为一个黑盒。我们精确地理解了从一个结构化的Python字典,到一串URL安全的字符串之间发生的每一个转换步骤。这是理解JWT构造的坚实基础。
好的,我们立刻承接上一节的内容,将我们的解剖刀转向JWT“三位一体”结构中承载核心信息的第二部分——载荷(Payload)。这是JWT的灵魂所在,是其作为“数字身份护照”的“个人信息页”。
载荷(Payload)是JWT三个组成部分中的第二个,也是信息量的核心。它同样是一个JSON对象,其唯一的使命就是承载需要传递的数据。这些数据,在JWT的语境下,不被称为“数据”或“信息”,而是有一个更严谨、更具法律意味的术语——“声明(Claims)”。
一个“声明”,就是关于一个主体(Subject),通常是用户,以及关于这个令牌本身的附加元数据的一个陈述(Statement)。例如,“这个令牌的主体是ID为user-123
的用户”是一个声明,“这个令牌将在2024年12月31日午夜过期”也是一个声明。载荷部分就是这些声明的集合。
与头部一样,载荷也是通过Base64Url编码后,成为JWT的第二部分,它不是加密的。这一点我们无论如何强调都不为过。这意味着,任何能够截获JWT的人,都能轻易地解码并阅读其全部内容。因此,载荷的设计与使用,必须时刻遵循安全第一的原则,绝不包含任何需要保密的敏感信息。
JWT的声明被划分为三个类别:注册声明(Registered Claims)、公共声明(Public Claims)和私有声明(Private Claims)。我们将以前所未有的深度,逐一剖析这些声明,尤其是注册声明,因为它们是构建安全、可互操作的JWT认证体系的基石。
注册声明是由JWT标准(RFC 7519)预先定义的一组声明。它们并非强制要求必须使用,但JWT标准强烈建议使用它们,因为这为不同的系统、不同的应用之间,提供了一套通用的、可互相理解的“标准语言”。当一个由A系统签发的JWT,需要被B系统验证时,如果双方都遵循了对注册声明的通用解释,那么互操作性就有了保障。
PyJWT
库在解码时,会对这些注册声明进行自动化的、内置的验证,极大地简化了开发者的工作,并避免了因手动验证逻辑不严谨而产生的安全漏洞。现在,让我们逐一深入这些至关重要的声明。
exp
(Expiration Time): 令牌的“生命终点”
exp
声明的值必须是一个NumericDate,即一个整数或浮点数,代表自Unix纪元(1970年1月1日00:00:00 UTC)以来的秒数。它定义了该JWT的过期时间。任何JWT的接收方,在处理一个JWT时,必须验证当前时间是否在该exp
时间之前。如果当前时间等于或晚于exp
时间,该JWT必须被拒绝。exp
是JWT最核心、最基础的安全机制。它为每一个令牌设定了生命的上限,极大地限制了令牌泄露后可能造成的危害。如果一个令牌永久有效,那么一旦它被攻击者窃取,攻击者就可以永久地冒充该用户。而通过设置一个较短的过期时间(例如15分钟),即使令牌被盗,攻击者的有效攻击窗口也被限制在这15分钟之内。PyJWT
的自动验证: 当你调用jwt.decode()
时,PyJWT
会默认检查exp
声明。如果令牌已经过期,它会自动抛出一个jwt.ExpiredSignatureError
异常。你无需编写任何if time.time() > payload['exp']:
这样的代码,PyJWT
已经为你以最安全的方式处理好了一切。exp
应该尽可能短,比如5分钟到1小时。这遵循了“最小权限”和“最小暴露窗口”的安全原则。nbf
(Not Before): 令牌的“启用时间”
nbf
声明的值同样是一个NumericDate。它定义了该JWT的生效时间。任何JWT的接收方,在处理一个JWT时,如果当前时间早于nbf
时间,该JWT必须被拒绝。nbf
不像exp
那样普遍,但在特定场景下非常有用。
nbf
时间的JWT,并分发给所有系统。所有系统都收到了令牌,但在nbf
指定的时间到达之前,它们都会拒绝使用该令牌执行操作,从而实现时间的精确同步。nbf
设置为明天凌晨的时间戳。PyJWT
的自动验证: jwt.decode()
同样会自动验证nbf
声明。如果当前时间早于nbf
时间,PyJWT
会抛出jwt.ImmatureSignatureError
异常。iat
(Issued At): 令牌的“出生证明”
iat
声明的值也是一个NumericDate,它记录了该JWT被签发的时间。iat
本身不直接决定令牌的有效性,但它为验证策略提供了更多维度。例如,一个安全策略可能会规定:“即使一个令牌尚未过期(exp
未到),但如果它是在24小时之前签发的(time.time()-iat > 86400
),我们也认为它风险较高,需要强制用户重新认证。”这可以作为一种额外的安全层,来缩短事实上的令牌生命周期,而无需频繁地轮换签名密钥。PyJWT
的验证: 默认情况下,PyJWT
不强制要求iat
存在。但你可以通过在decode
函数中设置require=["iat"]
选项,来强制要求载荷中必须包含iat
声明。iss
(Issuer): 令牌的“签发机构”
iss
声明的值是一个大小写敏感的字符串或URI,它标识了签发该JWT的主体(Principal)。iss
声明明确了令牌的来源。资源服务在验证令牌时,必须检查iss
是否是它所信任的那个签发者。这可以防止一个系统(如外部客户系统)签发的令牌,被错误地用于访问另一个系统(如内部管理系统)的资源。PyJWT
的自动验证: 在调用jwt.decode()
时,你可以传递issuer
参数。例如jwt.decode(token, key, algorithms=["HS256"], issuer="https://auth.my-company.com")
。PyJWT
会自动比较令牌中的iss
声明与你提供的值,如果不匹配,则抛出jwt.InvalidIssuerError
异常。aud
(Audience): 令牌的“预期接收方”
aud
声明的值是一个大小写敏感的字符串或URI的数组,它标识了该JWT的预期接收方。简而言之,它回答了“这个令牌是给谁用的?”这个问题。aud
是防止“令牌重定向攻击(Token Redirection Attack)”或“混淆代理问题(Confused Deputy Problem)”的核心机制。想象一个场景:你有一个“用户资料服务”(aud: "user-profile-api"
)和一个“支付服务”(aud: "payment-api"
)。用户请求更改自己的昵称,认证服务签发了一个aud
为"user-profile-api"
的JWT。如果攻击者截获了这个令牌,并用它去请求“支付服务”,而支付服务没有验证aud
,它只会看到这是一个合法的、由认证中心签发的令牌,然后可能会错误地执行某些操作。通过验证aud
,支付服务在收到这个令牌时,会发现aud
是"user-profile-api"
,而自己的身份是"payment-api"
,两者不匹配,于是立即拒绝该令牌。PyJWT
的自动验证: jwt.decode()
的audience
参数就是为此而生。例如jwt.decode(token, key, algorithms=["RS256"], audience="payment-api")
。PyJWT
会智能地处理aud
是单个字符串或数组的情况。如果令牌中的aud
声明与audience
参数不匹配,则抛出jwt.InvalidAudienceError
异常。sub
(Subject): 令牌的“所属主体”
sub
声明的值是一个大小写敏感的字符串或URI,它标识了该JWT所描述的主体,通常就是用户的唯一标识符。sub
的值应该是全局唯一的且永久不变的。使用用户的数据库主键(如UUID)是一个绝佳的选择。应该避免使用可变的属性,如电子邮件地址或手机号。因为一旦用户更改了他们的邮箱,如果sub
是邮箱,那么过去签发的所有关联令牌(例如在某些日志或关联系统中)都会与新身份脱钩,造成数据不一致。jti
(JWT ID): 令牌的“唯一序列号”
jti
声明的值是一个大小写敏感的字符串,它为该JWT提供了一个唯一的标识符。jti
是防止**重放攻击(Replay Attacks)**的最强大武器。一个重放攻击是指,攻击者截获了一个合法的JWT,然后在它过期之前,反复地用它来发起请求。虽然exp
限制了攻击的时间窗口,但在这个窗口内,攻击仍然可能造成危害(比如,用一个转账请求的JWT,重复提交100次)。jti
,接收令牌的服务需要建立一个“已处理jti
的缓存库”(例如,使用带有TTL的Redis Set)。每当收到一个JWT,服务在验证签名和过期时间等之后,还必须:
jti
。jti
是否存在于缓存库中。jti
添加到缓存库中,同时设置缓存的过期时间(TTL)为该JWT的剩余有效时间。让我们用代码来构建一个综合运用了上述注册声明,以及自定义私有声明的复杂Payload。
# 文件名: payload_constructor.py
# 作用: 演示如何构建一个结构丰富、包含多种声明的JWT Payload,并对其进行编码。
import json
import base64
import time
import uuid
# 借用上一章我们手动实现的编码函数
def dict_to_base64url(data: dict) -> str:
"""接收一个字典,将其转换为JWT规范的Base64Url字符串。"""
json_string = json.dumps(data, separators=(',', ':')) # 序列化为紧凑JSON
utf8_bytes = json_string.encode('utf-8') # 编码为UTF-8字节
standard_base64_bytes = base64.b64encode(utf8_bytes) # 标准Base64编码
url_safe_bytes = standard_base64_bytes.replace(b'+', b'-').replace(b'/', b'_') # 转为URL安全
no_padding_bytes = url_safe_bytes.rstrip(b'=') # 去除填充
return no_padding_bytes.decode('ascii') # 解码为字符串
def build_comprehensive_payload():
"""
构建一个包含注册声明、公共声明(以URI形式)和私有声明的JWT载荷。
"""
current_timestamp = int(time.time()) # 获取当前时间的Unix时间戳
# --- 构建Payload字典 ---
payload = {
# --- Registered Claims (注册声明) ---
"iss": "https://auth.my-awesome-app.com", # 签发者:我们的认证服务
"sub": "a1b2c3d4-e5f6-7890-1234-567890abcdef", # 主题:用户的唯一、不可变的UUID
"aud": [ # 受众:此令牌被授权用于访问订单服务和库存服务
"https_api_orders_my-awesome-app_com",
"https_api_inventory_my-awesome-app_com"
],
"exp": current_timestamp + 900, # 过期时间:令牌在签发15分钟后过期 (900秒)
"nbf": current_timestamp - 10, # 生效时间:令牌在签发前10秒即刻生效,以容忍轻微的时钟不同步
"iat": current_timestamp, # 签发时间:记录当前签发的时间点
"jti": str(uuid.uuid4()), # JWT ID:使用UUID v4确保每次签发的令牌都有一个全球唯一的ID,用于防重放
# --- Public Claims (公共声明) ---
# 使用URI来命名以避免冲突
"https://my-awesome-app.com/claims/email_verified": True,
# --- Private Claims (私有声明) ---
# 这些是我们应用内部自定义的声明
"user_name": "Alice", # 用户名,用于显示,不用于身份识别
"roles": ["customer", "premium_user"], # 用户角色列表
"tenant_id": "tenant-corp-xyz", # 多租户应用中的租户ID
"session_id": "sess_abc123" # 关联的会话ID,可用于服务端强制下线
}
return payload
if __name__ == '__main__':
print("--- 开始构建并编码一个复杂的Payload ---")
# 1. 构建载荷字典
complex_payload = build_comprehensive_payload()
print("\n[1] 构建的原始Payload字典内容:")
# 使用json.dumps美化打印输出
print(json.dumps(complex_payload, indent=2))
# 2. 对载荷进行Base64Url编码
encoded_payload = dict_to_base64url(complex_payload)
print("\n[2] 编码后的Base64Url (JWT的第二部分):")
print(encoded_payload)
# 3. 验证可读性:手动解码,证明其未加密
# a. 添加必要的填充 '='
padding_needed = 4 - (len(encoded_payload) % 4)
if padding_needed != 4:
encoded_payload += '=' * padding_needed
# b. 将 '-' 和 '_' 替换回 '+' 和 '/'
standard_base64 = encoded_payload.replace('-', '+').replace('_', '/')
# c. 使用标准Base64解码
decoded_bytes = base64.b64decode(standard_base64)
# d. 将字节解码为JSON字符串
decoded_json_string = decoded_bytes.decode('utf-8')
print("\n[3] 手动解码验证 (证明其内容是公开可读的):")
print(json.dumps(json.loads(decoded_json_string), indent=2))
签名是JWT的第三部分,也是其安全性的最终体现。它的存在不是为了保密(Confidentiality),而是为了两个同样重要、甚至在认证领域更为关键的目标:完整性(Integrity)和真实性(Authenticity)。
完整性(Integrity): 签名保证了JWT的前两个部分——头部(Header)和载荷(Payload)——在从签发者到接收者的传输过程中,没有被进行任何形式的篡改。哪怕只是在载荷中将"user_name": "Alice"
改为了"user_name": "AlIce"
,这样一个微小的变动,都会导致签名验证失败。它像一个数字封条,一旦被撕开(数据被修改),就会留下不可磨灭的痕迹。
真实性(Authenticity): 签名证明了JWT确实是由那个声称签发了它的实体所签发的。因为只有持有那个独一无二的“密钥”(对称加密中的共享密钥,或非对称加密中的私钥)的实体,才能计算出正确的签名。当资源服务用它所信任的密钥成功验证了一个签名时,它就能够确信,这个JWT确实来源于它所信任的认证服务,而不是某个中间人攻击者伪造的。
签名的生成和验证,是一场遵循着严格密码学协议的、精确的数学舞蹈。现在,我们将放慢每一个舞步,来彻底看清它的每一个细节。
签名的生成,永远发生在认证服务端。它需要三个关键输入:编码后的头部、编码后的载荷,以及一个神圣的、不可外泄的密钥。
步骤一:构建“签名输入体”(The Signing Input)
这是整个过程中最关键、也最容易被忽略的一步。签名算法并非分别对头部和载荷进行签名,而是对一个精确构造的、由前两者拼接而成的字符串进行签名。
这个“签名输入体”的构造规则是:
Base64Url(Header) + "." + Base64Url(Payload)
我们必须深刻地认识到:
.
)。这个点是签名内容的一部分,它和前后两个编码字符串一起,共同构成了被签名的最终数据。这个设计确保了JWT的整体结构本身也被纳入了签名的保护范围。任何试图改变JWT结构(比如移除某个部分)的行为,都会破坏这个“签名输入体”,从而导致签名失效。
步骤二:选择算法与密钥(The Algorithm and The Key)
签名的核心,是密码学算法的应用。
alg
字段决定的。例如,如果alg
是"HS256"
,那么就使用HMAC-SHA256算法。如果alg
是"RS256"
,就使用带SHA-256的RSA签名算法。HS256
),需要一个共享密钥(Shared Secret)。这是一个长而随机的字符串,认证服务用它来签名,所有需要验证的资源服务也必须拥有这个完全相同的密钥。RS256
, ES256
),需要一对密钥。认证服务使用**私钥(Private Key)来签名,这是一个绝对保密的密钥;而资源服务使用公钥(Public Key)**来验证,这是一个可以公开分发的密钥。步骤三:执行签名运算(The Signing Operation)
将“签名输入体”和“密钥”,喂给在alg
字段中指定的密码学算法。算法会输出一串二进制数据,这就是原始签名摘要(Raw Signature Digest)。
例如,对于HS256
,这个过程就是:
HMAC-SHA256(secret_key, signing_input)
步骤四:对签名进行编码(Encoding the Signature)
最后,将第三步生成的二进制原始签名摘要,同样进行Base64Url编码。编码后的字符串,就是JWT的第三部分,也就是最终我们看到的签名。
现在,我们将把前三节的所有知识融会贯通,编写一个完整的、不依赖任何JWT库的、从零开始手动生成HS256
签名的JWT的函数。这将是我们对JWT构造理解的最终检验。
# 文件名: manual_jwt_foundry.py
# 作用: 手工实现一个完整的JWT(HS256)编码器和解码器,以最底层的方式理解其工作原理。
import base64
import json
import hmac
import hashlib
import time
def dict_to_base64url(data: dict) -> str:
"""
一个辅助函数,将字典序列化并进行Base64Url编码。
(这个函数我们在之前的章节已经详细实现过)
"""
json_string = json.dumps(data, separators=(',', ':'), sort_keys=True) # sort_keys=True 保证每次编码结果一致
utf8_bytes = json_string.encode('utf-8')
standard_base64_bytes = base64.b64encode(utf8_bytes)
url_safe_bytes = standard_base64_bytes.replace(b'+', b'-').replace(b'/', b'_')
no_padding_bytes = url_safe_bytes.rstrip(b'=')
return no_padding_bytes.decode('ascii')
def manual_jwt_encode(payload: dict, secret: str, algorithm: str = "HS256") -> str:
"""
手动实现一个完整的JWT编码器,支持HS256。
Args:
payload (dict): JWT的载荷。
secret (str): 用于签名的共享密钥。
algorithm (str): 签名算法,本函数目前仅支持"HS256"。
Returns:
str: 完整的JWT字符串。
"""
print("--- [编码开始] 手动锻造JWT ---")
# 1. 准备并编码Header
header = {
"alg": algorithm, "typ": "JWT"}
encoded_header = dict_to_base64url(header)
print(f"1. 编码后的Header: {
encoded_header}")
# 2. 准备并编码Payload
encoded_payload = dict_to_base64url(payload)
print(f"2. 编码后的Payload: {
encoded_payload}")
# 3. 构建签名输入体 (Signing Input)
signing_input = f"{
encoded_header}.{
encoded_payload}".encode('utf-8')
print(f"3. 待签名的输入体: {
signing_input.decode('utf-8')}")
# 4. 执行签名
if algorithm == "HS256":
# a. 准备HMAC密钥
secret_bytes = secret.encode('utf-8')
# b. 使用hmac库和sha256算法计算签名摘要
raw_signature = hmac.new(secret_bytes, signing_input, hashlib.sha256).digest()
else:
raise ValueError("本手动编码器仅支持 'HS256' 算法")
print(f"4. 生成的原始签名摘要 (hex): {
raw_signature.hex()}")
# 5. 编码签名
encoded_signature = base64.urlsafe_b64encode(raw_signature).rstrip(b'=').decode('ascii')
print(f"5. 编码后的Signature: {
encoded_signature}")
# 6. 拼接成最终的JWT
jwt_token = f"{
encoded_header}.{
encoded_payload}.{
encoded_signature}"
print(f"6. 最终JWT: {
jwt_token}")
print("--- [编码完成] ---")
return jwt_token
if __name__ == '__main__':
# 定义我们的载荷和密钥
user_payload = {
"sub": "user-manual-001",
"name": "Hephaestus", # 赫淮斯托斯,希腊神话中的工匠之神
"iat": int(time.time()),
"exp": int(time.time()