最近在对接外部服务时,遇到JWT相关的一些问题,这篇文章做一下关于JWT的一些总结。
摘要
本文内容主要有俩点:1 说明JWT采用HS256生成token的基本原理 ; 2 说明github 主流JWT生成库在采用HS256情况下生成token不一致的原因;
问题说明
在某次对接外部平台的API接口时,对方说明需要采用JWT HS256获取授权token,但是在生成token的过程中,自己编码实现及采用第三方包生成,以及不同第三方包生成的token之间存在差异,部分可以调通对方接口,部分则不可以,遂引发对该问题的探究。
JWT简介
JSON Web Token 是一种开放标准 (涉及到的RFC: RF7515 RF7519 RF7797 RF8725 )定义了一种用于可以在各方之间安全传输信息的Json对象。该对象可以通过 HMAC 算法或者使用 RSA 或 ECDSA的公钥、私钥进行签名,则该信息是可以进行验证和信任的。
使用场景
授权
授权是最常见的场景,用户登录后,每个后续请求都将包含JWT,允许用户访问该令牌允许的路由、服务器和资源。通常使用在Bearer 模式的 Authorization header中,格式如下:
Authorization: Bearer <token>
信息交换
JWT可以进行安全传输信息,JWT可以签名,可以使用公钥、私钥对确定发送者的身份,验证内容是否被篡改。
JWT结构
在紧凑的形式中,JWT 由三部分组成,采用点 (.) 进行分隔,其通常结构如下:
xxxxx.yyyyyy.zzzzzz
header
header 通常由两部分组成:令牌类型 JWT 及所使用的签名算法 eg: SHA256 , 该json被Base64Url编码作为JWT的第一部分,格式如下:
{
"alg":"HS256",
"typ":"JWT"
}
payload
payload 指有效负载,其中包含关于实体和附加数据的陈述作用的声明,分为三种类型,包含注册声明、公共声明、私人声明等。
注册声明
预定义的声明,并非强制性,但建议使用,类似: iss (发行者)、exp(到期时间)、sub(主题)
公共声明
可以由使用JWT的人随意定义,但是为了避免冲突,应该在 Iana json web token 令牌注册表中定义
私人声明
在同意使用它们各方之间共享信息而创建的自定义声明
格式如下:
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
signature
创建signatire需要 encoded header 、encoded payload 、 secret key 、algorithm 对其进行签名, HMAC SHA256 签名算法如下:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
putting all together
输出是由点分割的三个 Base64-URL 字符串,即为JWT格式,格式如下:
base64UrlEncode(header).base64UrlEncode(payload).signature
比较研究
RFC 文档比较
在JWT中主要涉及到的RFC文档包括: RF7515 RF7519 RF7797 RF8725
RF7515与RFC7797的比较
RFC7515 RFC7797 都是关于 JWS (JSON web signature)的相关规范,但其涉及到JWS的不同方面并服务于不同的目的。
名称 | 简述 | 主要变更点说明 |
---|---|---|
RFC7515 | 定义JWS的语法与处理,指定了JWS的格式及并描述了如何对JWS内容进行数据签名或者验证,提供了在应用程序中使用JWS的基本框架 | |
RFC7797 | 是RFC7515的一项补充规范,向JWS 引入分离内容的功能,允许将JWS payload 与 JWS 本身分开存储和传输,为处理 payload 比较大或者分离的情况,不必对payload 进行编码,以改善空间和时间 | 在该RFC文档的实现下,将会在header中添加标注b64 以标明是否需要进行Base64url 转化,则在一些具体的实现或者对接文档中会见到并不会对payload进行base64url 转化,这种现象也是正常的。 |
RFC7519与RFC8725比较
RFC7519 RFC8725 都是关于 JWT (JSON Web Tokens)的相关规范, RF8725 是对JWT的最佳实践的一些规范。
名称 | 简述 | 主要变更点说明 |
---|---|---|
RFC7519 | 定义了JWT的主要规范,在本文的 JWT结构部分便是按照RFC7519的 | |
RFC8725 | 提供了在不同上下文中安全使用JWT的最佳实践,提供了使用JWT的指南和建议,包含令牌格式、密码算法、令牌生命周期等主题。 | 使用HTTPS 传输JWT 限制JWT生命周期 使用强密码算法 避免在错误信息或者其他响应中泄漏时间信息 |
第三方模块比较
第三方模块比较
因为在第三方服务对接时,通常使用python,则搜索github,并在其中发现可用于直接调用的主流库有pyjwt 、python-jose、authlib等,该处选择主要的pyjwt与authlib进行对比分析。
pyjwt
简介
python基于RFC7519实现的关于JWT解码编码的第三方模块 github
主要提供以下功能:
-
生成JWT
使用PyJWT可以方便地生成JWT,该过程包括设置头部、载荷和签名。可以使用多种算法来生成签名,如HMAC、RSA和ECDSA。下面是一个使用HMAC-SHA256算法生成JWT的示例:
import jwt payload = {'user_id': 1234567890, 'name': 'John Doe'} secret_key = 'secret' jwt_token = jwt.encode(payload, secret_key, algorithm='HS256') print(jwt_token)
-
解码JWT
使用PyJWT可以轻松解码JWT,获取其头部和载荷信息。下面是一个解码JWT并获取信息的示例
import jwt jwt_token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxMjM0NTY3ODkwLCJuYW1lIjoiSm9obiBEb2UifQ.pS6S...XqW8' secret_key = 'secret' decoded = jwt.decode(jwt_token, secret_key, algorithms=['HS256']) print(decoded)
-
验证签名
使用PyJWT可以验证JWT的签名是否有效。可以使用多种算法来验证签名,如HMAC、RSA和ECDSA。下面是一个验证JWT签名的示例
import jwt jwt_token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxMjM0NTY3ODkwLCJuYW1lIjoiSm9obiBEb2UifQ.pS6S...XqW8' secret_key = 'secret' try: decoded = jwt.decode(jwt_token, secret_key, algorithms=['HS256']) print("JWT signature is valid") except jwt.InvalidSignatureError: print("JWT signature is invalid")
authlib
简介
用于简化Web应用程序中身份验证和授权的实现。它提供了一系列工具和协议的实现,以便于开发人员在应用程序中轻松添加安全验证和授权功能,如OAuth、OpenID Connec、JWT等。
Authlib的主要功能包括:
- OAuth支持:Authlib提供了OAuth 1.0、OAuth 2.0协议的实现,包括授权、访问令牌管理等功能。OAuth是一种流行的授权协议,用于允许第三方应用程序在用户授权的情况下访问他们的资源。
- OpenID Connect支持:Authlib提供了OpenID Connect协议的实现,它建立在OAuth 2.0协议之上,为身份验证提供了一个标准化的框架。开发人员可以使用Authlib来验证OpenID Connect提供者的令牌、获取用户信息等。
- JWT支持:Authlib提供了对JSON Web Tokens(JWTs)的支持,用于处理安全令牌的编码、解码和验证。JWT是一种开放标准,用于在不同实体之间安全地传输信息,比如在客户端和服务器之间。
- 第三方身份验证支持:Authlib支持多种第三方身份验证提供商,如Google、Facebook、Twitter等。开发人员可以使用Authlib来实现与这些身份验证提供商的身份验证和授权。
- 多种框架支持:Authlib支持多种Web框架,如Flask、Django、FastAPI等。开发人员可以使用Authlib来简化身份验证和授权的实现,并且不需要编写大量的重复代码。
JWT编码和解码示例:
import time
from authlib.jose import jwt
# 编码JWT
payload = {'sub': '1234567890', 'name': 'John Doe', 'iat': int(time.time())}
secret_key = 'your-secret-key'
encoded_jwt = jwt.encode({'alg': 'HS256', 'typ': 'JWT'}, payload, secret_key)
# 解码JWT
decoded_jwt = jwt.decode(encoded_jwt, secret_key, algorithms=['HS256'])
pyjwt authlib 比较
名称 | 区别 |
---|---|
pyjwt | pyjwt主要关注JWT,提供了编码、解码和验证JWT的基本功能 pyjwt相对来说比较小巧,代码量少,功能相对简单,可更容易实现定制化的解决方案 |
authlib | authlib提供了更全面的OAuth 1.0a/2.0和OpenID Connect协议支持,同时也提供了JWT编码和解码的功能 功能更多,配置更加复杂,定制化开发难度较pyjwt高 |
总结
综上,在实际使用过程中,我们在使用第三方库或者自己进行JWT对接时,要了解对方所使用的具体的RFC协议,而不仅仅是指定JWT及加密方式就可以的。在不同的RFC协议版本中,对同种方式的实现是存在差别的的,而这种差别可能会导致我们不能及时与对方进行对接,尤其是在使用第三方库时,要了解第三方库中关于JWT的具体实现时所遵循的RFC协议,以避免不必要的问题。
参考文档
https://jwt.io/
https://www.iana.org/assignments/jwt/jwt.xhtml
https://www.rfc-editor.org/rfc/rfc6750
https://www.rfc-editor.org/rfc/rfc1945
https://en.wikipedia.org/wiki/HMAC
https://en.wikipedia.org/wiki/RSA_(cryptosystem)
https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm
https://www.rfc-editor.org/rfc/rfc7515
https://www.rfc-editor.org/rfc/rfc7519
https://www.rfc-editor.org/rfc/rfc7797
https://www.rfc-editor.org/rfc/rfc8725
https://base64.guru/standards/base64url
https://github.com/jpadilla/pyjwt/