1 概述
JSON Web Token (JWT)
是一种用于在两个系统之间传输声明(
Claims
)的方式,它具有紧凑、URL 安全的特点。所谓“紧凑”,是指它本身教小,适用于空间受限的环境,如 HTTP 授权头和 URI 查询参数。
Claims
在JWT中被编码为一个JSON对象,作为
JSON Web Signature (JWS)
结构的有效载荷 或
JSON Web Encryption (JWE)
结构的文本信息。 JWS/JWE 使用 MAC( Message Authentication Code)算法和加密算法对
Claims
进行数字签名或整体保护。JWT 总是以 JWS 紧凑序列化或 JWE 紧凑序列化的形式来表示。
2 相关术语
-
JWS
JSON Web Signature(JSON Web签名):一个代表数字签名或 MAC加密信息的数据结构。 它使用基于JSON数据结构来表示的、使用数字签名或 Message Authentication Code(MAC)来保护的内容。JWS 的加密机制为任意字节序列提供完整性保护。 -
JWE
JSON Web Encryption(JSON Web加密):一个代表加密内容的数据结构。 它使用基于JSON的数据结构表示加密内容。JWE加密机制对任意字节序列进行加密并提供完整性保护。 -
JOSE Header
即 JSON Object Signing and Encryption Header,JSON对象含有签名和加密信息的头部。 -
Claim
“声明”,是关于某个主题的一条判断信息。一条 “声明” 包含一个 名称/值 对。 -
Claim Name
“声明” 的名称部分,一定是个字符串。 -
Claim Value
“声明” 的值。可以是任意的 JSON 值。 -
Nested JWT
(嵌套JWT)
一种采用嵌套签名和/或加密的JWT。在嵌套JWT中,JWT用作封闭 JWS 或 JWE 结构的有效负载或明文值。 -
Unsecured JWT
(不安全的JWT)
未进行整体性保护或加密的JWT。 -
Collision-Resistant Name
(防碰撞名称)
命名空间中的一种名称,使名称的分配方式不太可能与其他名称冲突。防碰撞名称空间的示例包括:域名、ITU-T X.660和X.670建议系列中定义的对象标识符(OID)以及通用唯一标识符(UUID)[RFC4122]。当使用管理委派的命名空间时,名称的定义者需要采取合理的预防措施,以确保他们能够控制用于定义名称的命名空间。 -
StringOrURI
一种 JSON 字符串值。它对字符串的特殊要求是,虽然可以使用任意字符,但任何包含 “:” 字符的值必须是 URI。StringOrURI 值是大小写敏感的,不应用任何转换或规范化。 -
NumericDate
一个 JSON 数值,代表从UTC时间1970年1月1日零点到指定UTC时间的秒数,忽略闰秒。这相当于IEEE Std 1003.1,2013版[POSIX.1]的定义“从纪元算起的秒数”,在该定义中,除了可以表示非整数值外,每天的时间精确为86400秒。详情请参见 RFC 3339[RFC3339]。
3 JWT 概述
JWT 以 JSON 对象形式来表示一组声明(Claims),该对象以 JWS 和/或 JWE 结构编码。这个JSON 对象就是 JWT 声明集。根据 RFC 7159[RFC7159] 的第4节,JSON 对象由零个或多个名称-值对组成,其中名称为字符串,值为任意JSON值。这些成员是JWT代表的索赔。根据 RFC 7159[RFC7159]第2节,此JSON对象允许在任何JSON值或结构字符之前或之后包含空格或换行符。
- 在 JWT 声明集中,一个 Claim的名称称为“声明名称”,相应的值称为“声明值”。JOSE 头的内容描述了应用于JWT声明集的加密方式。
- 对于 JWS 格式的 JWT,声明集会被数字签名或者MAC加密。
- 对于 JWE 格式的 JWT,声明集会被 JWE 加密并作为纯文本信息。
- JWT可以被封装在另一个JWE或JWS结构中,这就是嵌套JWT。
JWT 用点(.)分隔成多个部分,每一部分都是一个 URL 安全的字符串,都是用 URL base64 编码的。
URL base64 编码:由于标准的Base64编码后可能出现字符+和/,在URL中就不能直接作为参数,所以又有一种”url safe”的base64编码,其实就是把字符+和/分别变成-和_ 。
JWT 中的被点分隔的段数取决于 JWS / JWE 的不同序列化方式。
3.1 JWT 案例
下面是一个 JOSE 头的案例,它表示编码部分是个 JWT,且此 JWT 是通过 HMAC SHA-256 算法进行MAC编码的 JWS 结构:
{"typ":"JWT",
"alg":"HS256"}
为了消除上述 JSON 对象表示中可能存在的歧义,下面还包括本例中用于上述JOSE头的实际UTF-8表示的字节序列(注:UTF-8是用一个或多个字节来表示字符)。(请注意,由于换行符(CRLF与LF)在不同平台的表示不同、行的开头间距和结尾间距不同、最后一行是否有终止符等等一些因素,都可能会产生歧义。在本例中使用的表示法中,第一行没有前导空格或尾随空格,第一行和第二行之间出现CRLF换行符(13,10),第二行有一个前导空格(32),没有尾随空格,最后一行没有终止换行符。)在本例中,代表JOSE头的UTF-8表示的八位字节(使用JSON数组表示法)是:
[123, 34, 116, 121, 112, 34, 58, 34, 74, 87, 84, 34, 44, 13, 10, 32,
34, 97, 108, 103, 34, 58, 34, 72, 83, 50, 53, 54, 34, 125]
Base64url编码对 JOSE 头的UTF-8的八位字节进行编码,得到JOSE头的编码结果:
eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9
下面是一个 JWT 声明集的案例:
{"iss":"joe",
"exp":1300819380,
"http://example.com/is_root":true}
下面是上述声明集以UTF-8表示的字节序列,它就是 JWS 的有效载荷:
[123, 34, 105, 115, 115, 34, 58, 34, 106, 111, 101, 34, 44, 13, 10,
32, 34, 101, 120, 112, 34, 58, 49, 51, 48, 48, 56, 49, 57, 51, 56,
48, 44, 13, 10, 32, 34, 104, 116, 116, 112, 58, 47, 47, 101, 120, 97,
109, 112, 108, 101, 46, 99, 111, 109, 47, 105, 115, 95, 114, 111,
111, 116, 34, 58, 116, 114, 117, 101, 125]
其 base64url 编码如下:
eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ
首先,使用 HMAC SHA-256 算法计算 经过 base64url 编码的JOSE报头和有效载荷,得到 HMAC 值,然后以 JWS 中指定的方式对 HMAC 值进行 base64url 编码,最终得到编码后的 JWS 签名:
dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
使用小圆点,将上述编码后的字符串连接到一起,就得到了一个完整的 JWT:
eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9
.
eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFt
cGxlLmNvbS9pc19yb290Ijp0cnVlfQ
.
dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
(注:上面的换行是为了有更好的显示效果)
4 JWT 的声明
JWT 中的 声明 不能有重复的名称,它们可以分为三类:注册声明、公共声明、私有声明。
4.1 已被注册的声明
本节下文列举的声明名称是在 IANA “JSON Web Token Claims” 第10.1节中被注册的。
这些声明并不是必须的、一定要有的声明,而是为声明的互操作性打下了基础。
对于这些注册声明,一个应用程序应该明确说明会用到哪些声明,以及这些声明是必须的还是可选的,以及何时是必须的、何时是可选的。
JWT 中的声明要尽可能的短,因为 JWT 本身就应用于空间受限的环境。
4.1.1 “iss” 声明(颁发者)
“iss” 声明(Issuer)说明了该 JWT 是由谁颁布的。不同的应用程序对它的处理方式可能是不同的。“iss” 声明的值是一个区分大小写的 StringOrURI 类型的字符串。
4.1.2 “sub” 声明(主角)
“sub” 声明(Subject)代表着此 JWT 的主角,即这个 JWT 的声明集都是关于谁的,JWT 的声明集通常都是对此 sub 的各个角度的描述。因此,在 JWT 颁发者那里,此 sub 应该是一个条件唯一或全局唯一的身份。
不同的应用程序对它的处理方式也是不同的。它也是一个区分大小写的 StringOrURI 类型的字符串。
4.1.3 “aud” 声明(受众)
“aud” 声明(Audience)代表着 JWT 的预期接收者。每个打算处理JWT的接收者都必须检查自己是否出现在 “aud” 值中,如果不存在,说明不应该处理此 JWT,因此必须拒绝此 JWT。
通常,“aud”值是一个区分大小写的
StringOrURI 字符串数组(多个受众),或单个StringOrURI 字符串(只有一个受众)
。不同的应用程序对它的处理方式也是不同的。
注:aud 与 client_id 的区别
JWT 中的 aud 声明代表 应接受令牌的资源服务器,aud 值是一个字符串——通常是
被访问的资源
,例如https://contoso.com.
OAuth 中的 client_id 表示 向资源服务器请求资源的客户端应用程序。
客户端应用 (例如某个 iOS 应用) 向您的身份验证服务器请求JWT,这样,它将传递 client_id 和 client_secret 以及可能需要的所有用户凭据。授权服务器使用client_id 和 client_secret 验证客户端,并返回JWT,JWT 包含一个 aud 声明,表示此 JWT 适用于哪些资源服务器。如果 aud 包含 www.myfunwebapp.com,但是客户端尝试在 www.supersecretwebapp.com 上使用此 JWT,则访问将被拒绝,因为资源服务器通过验证 JWT 就会发现 JWT 并不适合它。
总之,client 使用某个凭据请求 JWT, JWT 中的 aud 决定着该 client 能访问哪些资源。
受众”aud”、客户端 “client” 和 认证服务器”issuer” 三者之间的关系为:
4.1.4 “exp” 声明(过期时间)
“exp” 声明(Expiration Time)代表着过期时间,在该时刻之后,此 JWT 必须不能再被接受。一般地,为了解决可能尽的时钟偏差,验证者可以将过期时间往后延期几分钟。
“exp” 的值必须是一个代表 NumericDate 值的数字(见上文术语定义)。 “exp” 声明是一个可选声明。
4.1.5 “nbf” 声明(生效时间)
“nbf”(Not Before)代表着 JWT 的生效时间,在该时间之后才能处理该 JWT。与 “exp” 一样,它是个 NumericDate 类型的数字。此声明可选。
4.1.6 “iat” 声明(颁发时间)
“iat” (Issued At)代表着 JWT 的颁发时间,它是个 NumericDate 类型的数字。此声明可选。
4.1.7 “jti” 声明(JWT ID)
“jti” 是 JWT 的一个唯一标识符,主要用来作为一次性 token,从而回避重放(replay)攻击。“jti” 的值区分大小写。此声明可选。
4.2 公共声明
JWT 的使用者其实是可以任意定义自己的声明的。一些组织或公司可能会定义一些声明,并交给全社会来使用它,这种情况下,为了防止名称冲突,就需要将声明定义为公共声明。因此,
公共声明必须要包含一个防冲突的名称
。反之,任何具有防冲突名称的声明也都可以称为公共声明。
公共声明可以注册到 IANA “JSON Web Token Claims” 注册表中成为注册声明。
防碰撞的常用方法是,将其定义为一个 UUID,或者采用一个 URI 前缀作为一个安全防冲突的命名空间。
4.3 私有声明
私有声明是 JWT 提供者和消费者所共同定义的声明,既不是注册声明,也不是公共声明。由于私有声明是自定义的,具有名称冲突的风险,因此要小心使用。
私有声明 跟 公共声明 区别在于:公共声明 的接收方在拿到 JWT 之后,都知道怎么对它们进行验证(尤其是注册的声明),而自定义的私有声明不会验证,除非明确告诉接收方要对这些 声明 进行验证及其验证规则才行。
5 JOSE 头
JWT 头部也是一个 JSON 对象,它描述了JWT 加密方式,以及一些可选的附加属性。JWS 和 JWE 结构的头部的组织规则是不同的。
5.1 “typ” 属性(媒体类型)
“typ” 属性代表着此 JWT 的媒体类型(Media Type)。关于媒体类型见
IANA.MediaTypes
的说明。当明知对象是JWT时,应用程序通常不会使用它。这个参数被 JWT 实现忽略;此参数的任何处理都由 JWT 应用程序执行。
一般来说,如果要使用该属性,建议将其的值设为
application/jwt
。对于 Auth2.0 使用的 Access Token,其值一般为
application/at+jwt
,其中
at+jwt
表示以 JWT 形式表示的 Access Token,这是 Auth 2.0的相关要求。
此声明可选。
5.2 “cty” 属性(内容类型)
“cty” 表示 Content Type,用来表示关于 JWT 的结构信息。
在不使用嵌套签名或加密操作的正常情况下,不建议使用此头参数。当使用嵌套签名或加密时,必须给出该属性,且值必须为 “JWT”,以表示在该 JWT 中携带了嵌套 JWT。 有关嵌套JWT的示例,请参见下文。
5.3 将声明对复制到头属性中
在一些使用加密 JWT 的应用程序中,有时候需要将某些声明对以明文(未加密)的形式给出。例如,应用程序处理规则可能会使用它们,以在解密 JWT 之前知道如何处理。
可以将 JWT 声明集中存在的声明复制到 JWT 头中。这时,接收它们的应用程序应验证它们的值是否相同,除非应用程序为这些声明定义了其他的处理规则。应用程序有责任确保只有可以安全地以未加密方式传输的声明才能作为JWT中的头参数值复制。
6 不安全的 JWT
为了支持其他的安全保护机制(即 JWT 内容的保护机制既不是签名也不是加密),还可以在不使用签名或加密的情况下创建 JWT。
不安全的 JWT,就是 “alg” 头属性值为 “none” ,且其 JWS 签名值为空字符串的 JWS。
正如
JWA 规范
给的定义:它是一个使用 JWT 声明集合作为有效载荷的未进行安全保护的 JWS。
6.1 不安全的 JWT 案例
下面的一个 JOSE 头表示编码的对象是个不安全的 JWT:
{"alg":"none"}
其 UTF-8 形式的 Base64url 编码的值为:
eyJhbGciOiJub25lIn0
下面是一个 JWT 声明集的案例:
{"iss":"joe",
"exp":1300819380,
"http://example.com/is_root":true}
此集合的 UTF-8 字节序列的 Base64url 编码(也就是 JWS 的载荷)的值为:
eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ
编码的 JWS 签名是个空字符串。
将上述三部分用点连接起来,就是一个完整的不安全的 JWT:
eyJhbGciOiJub25lIn0
.
eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ
.
其中的换行进近是为了方便阅读。
7 创建与验证 JWT
7.1 创建一个 JWT
下面是创建一个 JWT 的步骤。在步骤的输入和输出之间没有依赖关系的情况下,步骤的顺序并不重要。
- 创建一个 JWT 声明集。注意,空格是有意义的,而且在编码之前不必采用规范化的 JSON。
- 将 JWT 声明集 使用 UTF-8 的字节序列来表示。
-
创建一个 JOSE 头,并在其中定义所需的属性和值。JWT 必须符合
JWS
或
JWE
规范。注意,空格是有意义的,而且在编码之前不必采用规范化形式。 -
根据创建的 JWT 是 JWS 还是 JWE 分类,此步骤是不同的:
– 如果 JWT 是个JWS,需遵循
JWS 规范
中规定的步骤,将声明集作为 JWS 载荷来创建 JWS。
– 如果 JWT 是个 JWE,需遵循
JWE 规范
中规定的步骤,将声明集作为 JWE 的明文。 - 如果需要嵌套签名或加密,将 JWS 或 JWE 作为一个有效载荷或明文,重新回到第3步,将 JOSE 头中添加 “cty”(content type)属性,且其值为 “JWT”。
7.2 验证一个 JWT
验证步骤如下:
- 验证 JWT 包含至少一个圆点(“.”)字符。
- 抽取 JWT 第一个圆点之前的部分作为一个 JOSE 头,对其进行 Base64url 解码。
- 验证解码后的 JOSE 头,判断它是不是一个 JSON 对象的 UTF-8 编码形式。
- 验证 JOSE 头中的参数和值是否支持,可忽略不支持的参数。
-
判断此 JET 是一个 JWS 还是一个 JWE。采用
JWE 规范
中第9节 “区别 JWS 与 JWE 对象” 给出的方法进行判断。 -
如果此 JWT 是个JWS,根据
JWS 规范
中给出的步骤进行验证,其载荷的 Base64url 解码就是传递的信息。如果此 JWT 是个 JWE,按照 JWE 规范中给出的步骤进行验证,验证结果明文就是要传递的信息。 - 如果 JOSE 头中包含 “cty”(content type)且其值为“JWT”,那么携带的有效信息就是一另外一个 JWT,此时,重新返回第1步,将有效信息作为 JWT 进行验证。
- 最后,对有效信息进行 Base64url 解码,验证结果是否是一个 UTF-8 字节流,并判断代表的 JSON 对象是否有效。
8 隐私相关事项
JWT可能包含隐私敏感信息。在这种情况下,必须采取措施防止向非预期方披露该信息。实现这一点的一种方法是使用加密的JWT并对收件人进行身份验证。另一种方法是确保包含未加密隐私敏感信息的JWT只能使用支持端点身份验证的加密协议(如传输层安全性(TLS))进行传输。从JWT中省略隐私敏感信息是最小化隐私问题的最简单方法。
参考资料