介绍
官网介绍https://jwt.io/introduction/
什么是JSON Web令牌?JSON Web令牌(JWT)是一个开放标准(RFC 7519),它定义了一种紧凑而独立的方法,用于在各方之间安全地将信息作为JSON对象传输。由于此信息是经过数字签名的,因此可以被验证和信任。可以使用秘密(使用HMAC算法)或使用RSA或ECDSA的公钥/私钥对对JWT进行签名。
尽管可以对JWT进行加密以提供双方之间的保密性,但我们将重点关注已签名的令牌。签名的令牌可以验证其中包含的声明的完整性,而加密的令牌则将这些声明隐藏在其他方的面前。当使用公钥/私钥对对令牌进行签名时,签名还证明只有持有私钥的一方才是对其进行签名的一方。
什么时候应该使用JSON Web令牌?以下是JSON Web令牌有用的一些方案:
授权:这是使用JWT的最常见方案。一旦用户登录,每个后续请求将包括JWT,从而允许用户访问该令牌允许的路由,服务和资源。单一登录是当今广泛使用JWT的一项功能,因为它的开销很小并且可以在不同的域中轻松使用。 信息交换:JSON Web令牌是在各方之间安全地传输信息的一种好方法。因为可以对JWT进行签名(例如,使用公钥/私钥对),所以您可以确保发件人是他们所说的人。此外,由于签名是使用标头和有效负载计算的,因此您还可以验证内容是否未被篡改。 JSON Web令牌结构是什么?JSON Web令牌以紧凑的形式由三部分组成,这些部分由点(.)分隔,分别是:
标头 有效载荷 签名因此,JWT通常如下所示。
xxxxx.yyyyy.zzzzz
标头标头通常由两部分组成:令牌的类型(即JWT)和所使用的签名算法,例如HMAC SHA256或RSA。
例如:
{ "alg": "HS256", "typ": "JWT" }
然后,此JSON被Base64Url编码以形成JWT的第一部分。
payload令牌的第二部分是有效负载,其中包含声明。声明是有关实体(通常是用户)和其他数据的声明。claims有以下三种类型:注册的,公共的和私人claims。
请注意,声明名称仅是三个字符,因为JWT是紧凑的。
已注册的权利要求:这些是一组非强制性的但建议使用的预定义权利要求,以提供一组有用的可互操作的权利要求。其中一些是: iss(发出者), exp(到期时间), sub(主题), aud(受众)等。 公共声明:使用JWT的人可以随意定义这些声明。但为避免冲突,应在IANA JSON Web令牌注册表中定义它们,或将其定义为包含抗冲突名称空间的URI。 私人权利:这些都是使用它们同意并既不是当事人之间建立共享信息的自定义声明注册或公众的权利要求。payload示例可能是:
{ "sub": "1234567890", "name": "John Doe", "admin": true }
==notice:== 请注意,对于已签名的令牌,此信息尽管可以防止篡改,但任何人都可以读取。除非将其加密,否则请勿将机密信息放入JWT的有效负载或报头元素中。
签名要创建签名部分,您必须获取编码的标头,编码的有效负载,机密,标头中指定的算法,并对其进行签名。
例如,如果要使用HMAC SHA256算法,则将通过以下方式创建签名:
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
签名用于验证消息在此过程中没有更改,并且对于使用私钥进行签名的令牌,它还可以验证JWT的发送者是它所说的真实身份。
放在一起输出是三个用点分隔的Base64-URL字符串,可以在HTML和HTTP环境中轻松传递这些字符串,与基于XML的标准(例如SAML)相比,它更紧凑。
下面显示了一个JWT,它已对先前的标头和有效负载进行了编码,并用一个秘密进行了签名。
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
如果您想使用JWT并将这些概念付诸实践,则可以使用jwt.io Debugger解码,验证和生成JWT。
JSON Web令牌如何工作?在身份验证中,当用户使用其凭据成功登录时,将返回JSON Web令牌。由于令牌是凭据,因此必须格外小心以防止安全问题。通常,令牌的保留时间不应超过要求的时间。
由于缺乏安全性,您也不应该将敏感的会话数据存储在浏览器中。
每当用户想要访问受保护的路由或资源时,用户代理都应发送JWT,通常使用承载模式在Authorization标头中发送JWT 。标头的内容应如下所示:
Authorization: Bearer <token>
在某些情况下,这可以是无状态授权机制。服务器的受保护路由将在Authorization标头中检查有效的JWT ,如果存在,则将允许用户访问受保护的资源。如果JWT包含必要的数据,则可以减少查询数据库中某些操作的需求,尽管这种情况并非总是如此。
如果令牌是在Authorization标头中发送的,则跨域资源共享(CORS)不会成为问题,因为它不使用cookie。
下图显示了如何获取JWT并将其用于访问API或资源:
1. 应用程序或客户端向授权服务器请求授权。这是通过不同的授权流程之一执行的。例如,典型的符合OpenID Connect的Web应用程序将/oauth/authorize使用授权代码流通过端点。 2. 授予授权后,授权服务器会将访问令牌返回给应用程序。 3. 该应用程序使用访问令牌来访问受保护的资源(例如API)。
==使用签名的令牌,令牌中包含的所有信息都会向用户或其他方公开,即使他们无法更改它。这意味着您不应将机密信息放入令牌中。==
为什么要使用JSON Web令牌?让我们谈谈与简单Web令牌(SWT)和安全性声明标记语言令牌(SAML)相比,JSON Web令牌(JWT)的好处。
由于JSON不如XML冗长,因此在编码时JSON的大小也较小,从而使JWT比SAML更为紧凑。这使得JWT是在HTML和HTTP环境中传递的不错的选择。
在安全方面,只能使用HMAC算法由共享机密对SWT进行对称签名。但是,JWT和SAML令牌可以使用X.509证书形式的公用/专用密钥对进行签名。与签署JSON的简单性相比,使用XML Digital Signature签署XML而不引入模糊的安全漏洞是非常困难的。
JSON解析器在大多数编程语言中都很常见,因为它们直接映射到对象。相反,XML没有自然的文档到对象的映射。这使得使用JWT比使用SAML断言更加容易。
关于用法,JWT是在Internet规模上使用的。这突显了在多个平台(尤其是移动平台)上对JSON Web令牌进行客户端处理的简便性。 编码的JWT和编码的SAML的长度比较
如果您想了解有关JSON Web令牌的更多信息,甚至开始使用它们在自己的应用程序中执行身份验证,请浏览到Auth0上的JSON Web令牌登录页面。
用于令牌签名/验证的库
警告:了解有关具有非对称密钥的JSON Web令牌库中的关键漏洞的更多信息。
功能 pyjwt python-jose jwcrypto authlib Sign √ √ √ √ Verify √ √ √ √ iss check √ √ √ √ sub check × √ √ √ aud check √ √ √ √ exp check √ √ √ √ nbf check √ √ √ √ iat check √ √ √ √ jti check × √ √ √ HS256 √ √ √ √ HS384 √ √ √ √ HS512 √ √ √ √ PS256 √ × √ √ PS384 √ × √ √ PS512 √ × √ √ RS256 √ √ √ √ RS384 √ √ √ √ RS512 √ √ √ √ ES256 √ √ √ √ ES256K ? ? √ ? ES384 √ √ √ √ ES512 √ √ √ √ EdDSA ? ? √ ?安装 pyjwt
pip install pyjwt
JSON Web令牌是一种开放的行业标准 RFC 7519方法,用于在双方之间安全地表示声明。
官网:https://jwt.io/
使用HS256编码和解码令牌
编码encoded = jwt.encode({'some': 'payload'}, 'secret', algorithm='HS256') print(encoded)
结果
b'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzb21lIjoicGF5bG9hZCJ9.Joh1R2dYzkRvDkqv3sygm5YyK8Gi4ShZqbhK2gxcs2U'解码
decoded = jwt.decode( 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzb21lIjoicGF5bG9hZCJ9.Joh1R2dYzkRvDkqv3sygm5YyK8Gi4ShZqbhK2gxcs2U', 'secret', algorithms='HS256') print(decoded)
结果
{'some': 'payload'}
指定其他标题
编码encoded = jwt.encode({'some': 'payload'}, 'secret', algorithm='HS256', headers={'kid': '230498151c214b788dd97f22b85410a5'}) print(encoded)
结果
b'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6IjIzMDQ5ODE1MWMyMTRiNzg4ZGQ5N2YyMmI4NTQxMGE1In0.eyJzb21lIjoicGF5bG9hZCJ9.gdQ884yjlnLnIrYjfQaClE6rJC2x8v2OP2s_eXOLhZA'查看标题
只读取标题,而不验证。例如,在令牌发行者使用多个密钥而您无法提前知道要使用哪个发行者的公共密钥或共享机密进行验证的情况下,发行者可能会在标头中包含密钥的标识符。
print(jwt.get_unverified_header(encoded))
结果
{'typ': 'JWT', 'alg': 'HS256', 'kid': '230498151c214b788dd97f22b85410a5'}
查看Claimset 不验证
如果希望读取JWT的claimset,而不执行签名或任何注册claimset的验证,可以将verify参数设置为False。
如果没有数字签名信息,就不能信任claimset的完整性或真实性。
decoded = jwt.decode(encoded,verify=False) print(decoded)
结果
{'some': 'payload'}
注册要求的名字
JWT规范定义了一些注册索赔名称并定义了它们应该如何使用。PyJWT支持以下登记claim name:
“exp” (过期时间) Claim “nbf” (不早于时间认领) Claim “iss” (发行人) Claim “aud” (听众) Claim “iat” (签发时间) Claim 到期时间声明“ exp”(到期时间)声明标识了不得接受JWT进行处理的到期时间。“ exp”索赔的处理要求当前日期/时间必须早于“ exp”索赔中列出的到期日期/时间。实施者可以留出一些余地,通常不超过几分钟,以解决时钟偏差。它的值必须是包含NumericDate值的数字。使用此声明是可选的。
jwt.encode({'exp': 1371720939}, 'secret') jwt.encode({'exp': datetime.now()}, 'secret')
如果过期时间在过去,则在jwt.decode()中自动验证过期时间并引发 jwt.ExpiredSignatureError:
try: jwt.decode('JWT_STRING', 'secret', algorithms=['HS256']) except jwt.ExpiredSignatureError: # Signature has expired
PyJWT还支持到期时间定义的余地部分,这意味着您可以验证过去但不是很远的到期时间。例如,如果您有一个JWT有效负载,其有效时间设置为创建后的30秒,但您知道有时您将在30秒后对其进行处理,则可以将回旋时间设置为10秒,以便有一些余量:
使用UTC+8时间戳
import time import datetime exp = int(datetime.datetime.now().timestamp()) + 5 jwt_payload = jwt.encode({ 'some': 'payload', 'exp': exp }, 'secret') print(jwt_payload) time.sleep(10) # JWT payload现在已过期 # 但有一些回旋余地时间,它仍然有效 d = jwt.decode(jwt_payload, 'secret', leeway=10, algorithms=['HS256']) print(d)
结果
b'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzb21lIjoicGF5bG9hZCIsImV4cCI6MTYwNDY1NTk4Mn0.5QEuR0epy3ejg3qUSTUNyFjDAhVV7I-hL2NVw6DjfnI' {'some': 'payload', 'exp': 1604655982}
过期报错
import time import datetime exp = int(datetime.datetime.now().timestamp()) + 5 jwt_payload = jwt.encode({ 'some': 'payload', 'exp': exp }, 'secret') print(jwt_payload) time.sleep(10) d = jwt.decode(jwt_payload, 'secret', algorithms=['HS256']) print(d)
结果
b'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzb21lIjoicGF5bG9hZCIsImV4cCI6MTYwNDY1NjAxNn0.BYnvd3eiv9-IwvMVLe36tK2Y8f3ySR1DH9nzcTyK5xE' Traceback (most recent call last): File "D:/pyproject/opsdesk/jwt_test.py", line 46, in <module> d = jwt.decode(jwt_payload, 'secret', algorithms=['HS256']) File "D:\pyenvs\opsdesk\lib\site-packages\jwt\api_jwt.py", line 104, in decode self._validate_claims(payload, merged_options, **kwargs) File "D:\pyenvs\opsdesk\lib\site-packages\jwt\api_jwt.py", line 134, in _validate_claims self._validate_exp(payload, now, leeway) File "D:\pyenvs\opsdesk\lib\site-packages\jwt\api_jwt.py", line 175, in _validate_exp raise ExpiredSignatureError('Signature has expired') jwt.exceptions.ExpiredSignatureError: Signature has expired
可以使用options参数中的verify_exp参数关闭到期时间验证。
import time import datetime exp = int(datetime.datetime.now().timestamp()) + 5 jwt_payload = jwt.encode({ 'some': 'payload', 'exp': exp }, 'secret') print(jwt_payload) time.sleep(10) d = jwt.decode(jwt_payload, 'secret', options={'verify_exp': False}, algorithms=['HS256']) print(d)
结果
b'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzb21lIjoicGF5bG9hZCIsImV4cCI6MTYwNDY1NjE3OX0.inVHTscvwx85-3UNrPbL28NLEAitKRn0qJuUuxAh6PU' {'some': 'payload', 'exp': 1604656179}
不早于时间认领(nbf)
“ nbf”(不早于)声明标识了不得接受JWT进行处理的时间。处理“ nbf”索赔要求要求当前日期/时间必须晚于或等于“ nbf”索赔中所列的不早日期/时间。实施者可以留出一些余地,通常不超过几分钟,以解决时钟偏差。它的值必须是包含NumericDate值的数字。使用此声明是可选的。 该NBF要求的工作方式与同样EXP要求之上。
jwt.encode({'nbf': 1371720939}, 'secret') jwt.encode({'nbf': datetime.utcnow()}, 'secret')解码的时间早于nbf时间会报错
jwt.exceptions.ImmatureSignatureError: The token is not yet valid (nbf)
import jwt import time import datetime nbf = int(datetime.datetime.now().timestamp()) + 30 print(nbf) jwt_payload = jwt.encode({ 'some': 'payload', 'nbf': nbf }, 'secret') print(jwt_payload) time.sleep(10) print(int(datetime.datetime.now().timestamp())) d = jwt.decode(jwt_payload, 'secret', algorithms=['HS256']) print(d)
结果
# nbf 1604656451 b'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzb21lIjoicGF5bG9hZCIsIm5iZiI6MTYwNDY1NjQ1MX0.QLgnsKaJ5Es1-Du6RM9YM9KZYGUUS8Vly-EGKNqLnhg' # 解码的时间。解码的时间小于nbf的时间 1604656431 Traceback (most recent call last): File "D:/pyproject/opsdesk/jwt_test.py", line 16, in <module> d = jwt.decode(jwt_payload, 'secret', algorithms=['HS256']) File "D:\pyenvs\opsdesk\lib\site-packages\jwt\api_jwt.py", line 104, in decode self._validate_claims(payload, merged_options, **kwargs) File "D:\pyenvs\opsdesk\lib\site-packages\jwt\api_jwt.py", line 131, in _validate_claims self._validate_nbf(payload, now, leeway) File "D:\pyenvs\opsdesk\lib\site-packages\jwt\api_jwt.py", line 165, in _validate_nbf raise ImmatureSignatureError('The token is not yet valid (nbf)') jwt.exceptions.ImmatureSignatureError: The token is not yet valid (nbf)解码的时间晚于nbf时间, 可以正常解码
import jwt import time import datetime nbf = int(datetime.datetime.now().timestamp()) + 5 print(nbf) jwt_payload = jwt.encode({ 'some': 'payload', 'nbf': nbf }, 'secret') print(jwt_payload) time.sleep(10) print(int(datetime.datetime.now().timestamp())) d = jwt.decode(jwt_payload, 'secret', algorithms=['HS256']) print(d)
1604656580 b'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzb21lIjoicGF5bG9hZCIsIm5iZiI6MTYwNDY1NjU4MH0.Pep7K84thUI8CB0I05yTUCkhpOGObE0sfxPokgZ7YqQ' 1604656585 {'some': 'payload', 'nbf': 1604656580}
发行人Claim (iss)
“发行人”(iss)Claim标识了发布联合函授的主体。此权利要求的处理通常是特定于应用程序的。“ iss”值是区分大小写的字符串,其中包含StringOrURI值。使用此声明是可选的。
正确的import jwt payload = { 'some': 'payload', 'iss': 'urn:foo' } token = jwt.encode(payload, 'secret') print(token) decoded = jwt.decode(token, 'secret', issuer='urn:foo', algorithms=['HS256']) print(decoded)
结果
b'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzb21lIjoicGF5bG9hZCIsImlzcyI6InVybjpmb28ifQ.KUsdL-cGUBYkqRJTCt1IDxI_YTN4-1CwcZdWkPk1vP8' {'some': 'payload', 'iss': 'urn:foo'}发行人声明不正确
将引发jwt.InvalidIssuerError。
import jwt payload = { 'some': 'payload', 'iss': 'urn:foo' } token = jwt.encode(payload, 'secret') print(token) decoded = jwt.decode(token, 'secret', issuer='tli:foo', algorithms=['HS256']) print(decoded)
结果
b'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzb21lIjoicGF5bG9hZCIsImlzcyI6InVybjpmb28ifQ.KUsdL-cGUBYkqRJTCt1IDxI_YTN4-1CwcZdWkPk1vP8' Traceback (most recent call last): File "E:/pyproject/opsdesk/jwt_test.py", line 12, in <module> decoded = jwt.decode(token, 'secret', issuer='tli:foo', algorithms=['HS256']) File "E:\pyenv\opsdesk\lib\site-packages\jwt\api_jwt.py", line 104, in decode self._validate_claims(payload, merged_options, **kwargs) File "E:\pyenv\opsdesk\lib\site-packages\jwt\api_jwt.py", line 137, in _validate_claims self._validate_iss(payload, issuer) File "E:\pyenv\opsdesk\lib\site-packages\jwt\api_jwt.py", line 214, in _validate_iss raise InvalidIssuerError('Invalid issuer') jwt.exceptions.InvalidIssuerError: Invalid issuer
观众认领(aud)
“听觉”(听众)声明标识了JWT的目标收件人。每个打算处理JWT的主体必须在受众声明中标识自己的价值。如果处理该声明的委托人在该声明存在时没有用“ aud”声明中的值标识自己,则必须拒绝JWT。通常,==“ aud”值是一个区分大小写的字符串数组,每个字符串都包含一个StringOrURI值。在特殊情况下,当JWT有一个听众时,“ aud”值可以是一个包含StringOrURI值的区分大小写的字符串。== 受众价值的解释通常是特定于应用程序的。使用此声明是可选的。
正确的观众import jwt payload = { 'some': 'payload', 'aud': 'urn:foo' } token = jwt.encode(payload, 'secret') print(token) decoded = jwt.decode(token, 'secret', audience='urn:foo', algorithms=['HS256']) print(decoded)
结果
b'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzb21lIjoicGF5bG9hZCIsImF1ZCI6InVybjpmb28ifQ.ZU7C_3t-YUIUugIlV8xkqFTrSbRrhchmZNi7HANorAU' {'some': 'payload', 'aud': 'urn:foo'}错误的观众
会报错jwt.exceptions.InvalidAudienceError: Invalid audience
import jwt payload = { 'some': 'payload', 'aud': 'urn:foo' } token = jwt.encode(payload, 'secret') print(token) decoded = jwt.decode(token, 'secret', audience='aud:lk', algorithms=['HS256']) print(decoded)
结果
b'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzb21lIjoicGF5bG9hZCIsImF1ZCI6InVybjpmb28ifQ.ZU7C_3t-YUIUugIlV8xkqFTrSbRrhchmZNi7HANorAU' Traceback (most recent call last): File "E:/pyproject/opsdesk/jwt_test.py", line 12, in <module> decoded = jwt.decode(token, 'secret', audience='aud:lk', algorithms=['HS256']) File "E:\pyenv\opsdesk\lib\site-packages\jwt\api_jwt.py", line 104, in decode self._validate_claims(payload, merged_options, **kwargs) File "E:\pyenv\opsdesk\lib\site-packages\jwt\api_jwt.py", line 140, in _validate_claims self._validate_aud(payload, audience) File "E:\pyenv\opsdesk\lib\site-packages\jwt\api_jwt.py", line 204, in _validate_aud raise InvalidAudienceError('Invalid audience') jwt.exceptions.InvalidAudienceError: Invalid audience
签发时间(iat)
Iat(签发时间)声明标识了JWT的发布时间。此声明可用于确定JWT的年龄。它的值必须是包含NumericDate值的数字。使用此声明是可选的。
正确的import jwt import datetime timestamp = int(datetime.datetime.now().timestamp() - 120) payload = { 'some': 'payload', 'iat': timestamp } token = jwt.encode(payload, 'secret') print(token) decoded = jwt.decode(token, 'secret', algorithms=['HS256']) print(decoded)
结果
b'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzb21lIjoicGF5bG9hZCIsImlhdCI6MTYwNDY3NjAzM30.BO5upqsrfRdtBVSmwjuJr74sqAIrtnyyA3oII-JmAAE' {'some': 'payload', 'iat': 1604676033}非数字
如果签发时间为非数字,则抛出jwt.exceptions.InvalidIssuedAtError: Issued At claim (iat) must be an integer.
import jwt timestamp = 'ABC' payload = { 'some': 'payload', 'iat': timestamp } token = jwt.encode(payload, 'secret') print(token) decoded = jwt.decode(token, 'secret', algorithms=['HS256']) print(decoded)
结果
b'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzb21lIjoicGF5bG9hZCIsImlhdCI6IkFCQyJ9.-e_MEjH4RjBFW_jseIlOxQZtRj5F2rtR-ktFgHAodSU' Traceback (most recent call last): File "E:\pyenv\opsdesk\lib\site-packages\jwt\api_jwt.py", line 154, in _validate_iat int(payload['iat']) ValueError: invalid literal for int() with base 10: 'ABC' During handling of the above exception, another exception occurred: Traceback (most recent call last): File "E:/pyproject/opsdesk/jwt_test.py", line 15, in <module> decoded = jwt.decode(token, 'secret', algorithms=['HS256']) File "E:\pyenv\opsdesk\lib\site-packages\jwt\api_jwt.py", line 104, in decode self._validate_claims(payload, merged_options, **kwargs) File "E:\pyenv\opsdesk\lib\site-packages\jwt\api_jwt.py", line 128, in _validate_claims self._validate_iat(payload, now, leeway) File "E:\pyenv\opsdesk\lib\site-packages\jwt\api_jwt.py", line 156, in _validate_iat raise InvalidIssuedAtError('Issued At claim (iat) must be an integer.') jwt.exceptions.InvalidIssuedAtError: Issued At claim (iat) must be an integer.