在第(5)小节中我和你讨论了几种创建和验证令牌的技术。使用随机字符串是一种最简单常用的方式,使用这种方式创建的令牌本身不携带任何有效的信息,只是充当数据库检索的索引值。而另外一种创建令牌的方式是让令牌本身裹挟一些关键信息——例如令牌的过期时间及授权用户、关联的权限范围、被授权的客户端等信息,这种令牌就是目前流行的大名鼎鼎的JWT令牌,这也是我们今天谈论的主角。
值得注意的是即使授权服务器颁发给客户端的访问令牌是一个JWT令牌,这也不能改变访问令牌在OAuth 2中的含义。访问令牌对客户端依然是没有任何意义的字符串,客户端不能也不应该试图解析令牌本身的内容,而应将其视为一个随机字符串在资源调用请求时发送给资源服务器,资源服务器在得到JWT令牌后解析令牌包含的信息,然后基于这些信息做出授权决策。
从《让子弹飞》说起——什么是JWT
在姜文导演的《让子弹飞》中,葛优饰演的马邦德在开头携带的委任状和我们今天讨论的JWT( JSON Web Token)有异曲同工之妙,JSON Web Token(JWT)是一种开放标准,它定义了一种紧凑且自包含的方式,用于在各方之间安全地传输JSON对象信息。由IETF OAuth工作组开发的RFC 7519文档定义了JWT的结构和处理规则。委任状的目的是在两个政府部门间传递委任信息,而JWT的目的也是一方向另一方以JSON对象的形式传递一些声明或者断言,例如用户的姓名、性别、职位、是否超过18岁等信息。更为重要的是JWT标准化了这些JSON对象的属性以及属性的含义。如果用JSON的形式表达这张委任状传递的信息,那么它携带和传递的信息如下:
{"姓名":"马邦德","职位":"县长","任职地":"鹅县"}
如上图所示,这张委任状可以很明显分成三个部分。类似的,一个JWT令牌也可以分为如下三个部分(在后文我们将会看到当JWT是一个JWE令牌时,会包含五部分)——HEADER(头部)、PAYLOAD(数据体)、SIGNATURE(签名),其中每个部分都是以Base64URL编码的,并用句点(.)分隔。
- HEADER:定义了用于签名消息的算法(用属性alg标识)以及一些其他的可选的JWT元数据信息(例如令牌的类型等信息,用属性typ标识)。RFC 7519 JWT文档并没有定义任何标准的HEADER头属性,具体的头属性信息取决于JWT令牌是JWS还是JWE令牌,由JWS文档和JWE文档规约和定义。我将在下文描述JWS和JWE时详细进行展开。
{"alg":"HS256","typ":"JWT","cty":"JWT"}
- PAYLOAD:JWT的有效载荷或者JWT声明集。它是对表示为JSON形式的业务数据进行Base64 URL编码之后的结果。JSON对象的成员是JWT发布者发布的一对一对的声明。JWT中的每一个声明的属性名称必须是唯一的。
{"姓名":"马邦德","职位":"县长"}
JWT规范制定了三种类型的声明集——已注册的声明集、公共声明集以及私有声明集。已注册的声明集指的是已经在互联网号码分配局(英语:Internet Assigned Numbers Authority,缩写IANA)的JSON Web Token 声明集注册表中注册的声明 (你可以参阅链接https://www.iana.org/assignments/jwt/jwt.xhtml更进一步了解这些细节)。尽管这些声明被视为已注册的声明,但JWT规范并未强制要求使用它们。这完全取决于JWT之上的其他规范来决定哪些是强制性的,哪些不是强制性的。例如,在OpenID Connect规范中,iss是一个强制声明。以下列出了JWT规范中定义的已注册的声明集。
- iss:JWT的发布者。这向令牌的接收者说明了令牌的发布者是谁,一般的这是一个区分大小写的字符串,它包含字符或URI地址。例如上面委任状中的“中华民国萨南康省主席巴青泰”就是JWT的发布者。
- sub:标识JWT的主体,即JWT的拥有者。对应于OAuth 2即标识为资源拥有者的唯一标识。对应于上面的委任状JWT,“马邦德”是这个JWT的sub。
- aud:标识JWT的受众或者预期的接收者。一般情况下,aud的值是大小写敏感的字符串数组。如果JWT的预期受众只有一个,此时aud的值是一个大小写敏感的字符串。aud参数的值必须是JWT发布者和接受者预先商定好的值,JWT的接收者负责对aud值校验规则的制定和值的校验。在上面的委任状JWT中,鹅县是JWT委任状的受众,康城不会接收和认可这张JWT委任状。
- exp:JWT的过期时间,是从1970-01-01 00:00:00.000到当前时间的秒数。JWT的发行方具体确定JWT的过期时间,JWT规范并没有规约令牌过期时间的值。
- iat:JWT的签发时间,是从1970-01-01 00:00:00.000到当前时间的秒数。
- nbf:英文not before的缩写,表达的是JWT的接收者在nbf属性中指定的时间前应将该令牌视为无效。当JWT声明集中存在nbf属性时,JWT的令牌的有效时长等于exp和nbf之间的差值。一般的,nbf的值都等于iat的值。
- jti:JWT的唯一标识,该值对于令牌发行者是唯一的。
公共声明集是由其他构建在JWT之上的规约和定义的声明。这些声明同样在IANA的JSON Web Token 声明集注册表中注册。如下图所示,OpenID Connect 规范制定了一组自己的声明集,这些声明集在ID令牌中体现和使用(我在OIDC的部分会向你介绍这些细节)。由OpenID Connect 规范制定的这些声明集被JWT规范称为公共声明集。
不在已注册的声明集或公共声明集中的声明名称被称为是私有声明集。这种声明集由不同的JWT发行者定义和使用。应当谨慎使用该类型的声明集,因为这些自定义的声明可能与公共声明集或已注册的声明集有语义冲突,对使用者造成干扰。
- SIGNATURE:签名是对JWT HEADER和JWT PAYLOAD签名的结果,签名的算法被定义在JWT HEADER的alg属性中。签名是为了保证JWT的HEADER和PAYLOAD没有被篡改,签名是JWT的发行者生成的,JWT的接收者需要根据签名算法重新对HEADER和PAYLOAD进行计算,并且比对计算结果是否与JWT本身的签名信息吻合。在我们的委任状JWT中,可以将盖章理解为签名,目的就是为了防止有人对委任状进行篡改。如果没有签名,可以很容易把“马邦德”修改成“张麻子”。当然JWT允许没有SIGNATURE部分,此时HEADER里的alg属性的值必须是none,此时JWT只有HEADER和PAYLOAD两部分,我们把这种形式的JWT称为明文JWT。
JSON Web Signature (JWS)
什么是JWS呢?回到我们上面的委任状,我们发现我们的委任状很容易被篡改。有时候只需要寥寥几笔,这张委任状的信息就发生巨大的变化。如下图所示,我们在这张委任状上做上水印,那么就很难篡改这张委任状的内容。JWS存在的目的也是这样,JWS定义了一系列签名算法对Header和Payload进行签名,从而防止第三方对JWT令牌内容的恶意篡改。JWT的接收方可以按照既定的约束和算法校验签名值的有效性,从而确保JWT携带的信息是原始的未被篡改过的。值得注意的是,和打了水印的委任状一样,JWS并没有对内容进行加密,任何拿到这个JWS令牌的第三方都可以阅读当前JWT的内容。
不同于JWT,JWS是由IETF的JOSE工作组开发和制定的,它被定义在RFC 7515文档中。见文知意,JWS表示PAYLOAD是被数字签名的。你可以将JWS看成是JWT的扩展和实现。JWS比JWT更灵活和自由,JWS的有效载荷可以是任何形式,例如JSON、XML甚至是二进制,签名消息可以通过两种方式进行序列化:JWS压缩序列化(JWS compact serialization)和JWS JSON序列化(JWS JSON serialization)。我们只把有效载荷形式是JSON且序列化方式是JWS压缩序列化的JWS称为JWT。
JWS 压缩序列化
JWS压缩序列化的的格式与我们上文描述的JWT的样式一样。如下图所示,它也由三个部分——JOSE Header、JWS Payload和JWS Signature组成。每个部分都是经过Base64URL编码的,各部分之间用句点(.)作为分割符号。
JOSE Header
JWS规范中规约和定了11个标准的JOSE Header属性。同我们上文介绍过的JWT,JWS也将Header头属性划分成了三个种类——已注册的Header属性名称、公共Header属性名称、私有Header属性名称,它们的定义与上文JWT一致,我在此不再赘述。我只为你描述已经在IANA注册过的Header头属性。
- alg(Algorithm):必须属性,对JWS Payload和JOSE HEADER进行签名的算法的名称。alg属性的值来自于IETF的JOSE工作组开发和制定的rfc7518规范。你可以在RFC 7518规范的第三部分(https://www.rfc-editor.org/rfc/rfc7518#section-3)看到JWS已经注册的算法属性值。在JWS规范中定义了两种推荐的签名算法,一种是HMAC这样的对称算法,另一种是RSA或椭圆曲线(ECC)这样的非对称算法。 1. 使用SHA-256的HMAC,在JMA规范中被称为HS256算法。这是JWT中最常使用的签名算法,使用这种算法的优势在于速度。2. RSASSA-PKCS1-v1_5 SHA-256签名算法,在JMA规范中被称为RS256算法。这种算法是我们熟知的公/私钥算法,JWS的发布方使用私钥创建签名消息,而JWS的接收方使用公钥验证消息的合法性。3. ECDSA P-256 SHA-256算法,在JMA规范中被称为ES256算法。这是RSA算法的替代方案,和上面的RS256算法类似,但是它对硬件的要求更低。
- jku(JWK Set URL):指向 JSON Web Key (JWK) 集合的URL地址。JWK集合指的是一组JSON编码的公钥集。其中的一个其对应于用于对JWS进行数字签名的密钥。该URL必须是用HTTPS进行暴露。
- jwk (JSON Web Key) :一个对JSON载荷进行签名的私钥对应的公钥。上面的jku指向一组JWK集合,而该参数直接将签名的公钥嵌入JOSE Header中。
- kid (Key ID):用于对载荷进行数字签名的密钥的ID。
- x5u (X.509 URL) :指向 X.509证书集的URL地址。
- x5c (X.509 Certificate Chain):一个具体的内嵌到JOSE Header的 X.509证书。
- x5t(X.509 Certificate SHA-1 Thumbprint):与kid参数类似,用于定位具体的X.509证书。X.509证书的SHA-1指纹。
- x5t#S256 (X.509 Certificate SHA-256 Thumbprint) :与x5t参数作用类似,但是是X.509证书的SHA-256指纹。
- typ (Type) Header Parameter:当使用JWS压缩序列化时type的值是“JOSE”,当使用JWS JSON序列化时此时typ的值是“JOSE+JSON”。
- cty (Content Type) :用于描述JWS令牌的结构,当JWS是嵌套的JWT(嵌套JWT指的是包含另外一个JWT的JWT)时该参数的值必须存在且值必须是jwt。
- crit (Critical):用于向JWS的接收者指示当前的Jose Header内是否包含自定义的属性。
JWS Payload
JWS的有效载荷,是需要被签名的内容。JWS并没有规约载荷的形式,可以是JSON、XML或者二进制数据。我们在此只讨论JSON格式的载荷数据。
JWS签名
通过对JWS Payload和JOSE Header计算之后的数字签名。防止恶意用户对Header和Payload的篡改。
如何构建一个压缩序列化的JWS
要创建一个JWS,请执行以下步骤(我在小册对应的github仓库提供了示例,你可以查阅:)。
- 创建JOSE Header JSON对象。我在上面已经向你解释了标准的JWS JOSE Heder属性。
- 对上面生成的JOSE Header JSON对象进行Base64URL编码,生成JWS的第一个元素。即BASE64URL(JOSE Heder)
- 创建JSON格式的JWS Payload。
- 对JSON格式的JWS Payload进行Base64URL编码,生成JWS的第二个元素。即BASE64URL(JWS Payload)。
- 构造即将要被进行签名的消息体,消息体包含JOSE Header和JWS Payload的内容。即 ASCII(BASE64URL(UTF8(JOSE Header)) || ‘.’ ||BASE64URL(JWS Payload))。
- 对上面构建的消息体计算签名。你在第(1)步的JOSE Header中定义了对消息体进行签名的算法,同时你通过某种方式声明了公钥,在这里你使用公钥对应的私钥对消息体进行签名。
- 对上面生成的签名进行Base64URL编码,至此生成了JWS的第三个元素。即BASE64URL(JWS Signature )。
JWS JSON序列化
与JWS压缩序列化的结果不同,使用JWS JSON序列化的JWS令牌有更复杂的结构。JWS JSON序列化可以对相同的Payload生成不同的签名。如下图所示,这个JSON序列化对象包含两个顶级元素,其中payload是它们共享的同一个载荷,另一个signatures顶级元素是一个数组对象,数组内的每一个对象是对同一载荷进行不同签名的结果,每个签名使用不同的算法和不同的密钥进行签名。signatures中子对象的属性说明如下:
- protected:必须进行签名保护的头。即BASE64URL(UTF8(JWS Protected Header))
- header:不参与签名计算的JOSE Header。protected中定义的JOSE Header和header中定义的Header共同组成了完整的JOSE Header。
- signature:对protected中定义的受保护的Header和Payload计算得到的签名,其中算法由protected中的alg参数表示,密钥由header中kid参数值选择标识。即alg(ASCII(BASE64URL(UTF8(JWS Protected Header)) || ‘.’ || BASE64URL(JWS Payload)))。
相比于JWS压缩序列化,JWS JSON序列化可以有选择地对JOSE Header进行签名。JWS压缩序列化存在的意义是给JWS令牌的接收者提供了选择的空间,他们可以自由选择自己支持的签名算法对签名进行校验。
构建一个JWS JSON序列化的JWS令牌
下面是一个用JWS JSON序列化构建的JWS令牌。这个JWS令牌享有相同的载荷(payload)以及两个不同的数字签名。
{"payload":
"eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGF
tcGxlLmNvbS9pc19yb290Ijp0cnVlfQ","signatures":[{"protected":"eyJhbGciOiJSUzI1NiJ9","header":{"kid":"2010-12-29"},"signature":
"cC4hiUPoj9Eetdgtv3hF80EGrhuB__dzERat0XF9g2VtQgr9PJbu3XOiZj5RZ
mh7AAuHIm4Bh-0Qc_lF5YKt_O8W2Fp5jujGbds9uJdbF9CUAr7t1dnZcAcQjb
KBYNX4BAynRFdiuB--f_nZLgrnbyTyWzO75vRK5h6xBArLIARNPvkSjtQBMHl
b1L07Qe7K0GarZRmB_eSN9383LcOLn6_dO--xi12jzDwusC-eOkHWEsqtFZES
c6BfI7noOPqvhJ1phCnvWh6IeYI2w9QOYEUipUTI8np6LbgGY9Fs98rqVt5AX
LIhWkWywlVmtVrBp0igcN_IoypGlUPQGe77Rw"},{"protected":"eyJhbGciOiJFUzI1NiJ9","header":{"kid":"e9bc097a-ce51-4036-9562-d2ade882db0d"},"signature":
"DtEhU3ljbEg8L38VWAfUAqOyKAM6-Xx-F4GawxaepmXFCgfTjDxw5djxLa8IS
lSApmWQxfKTUJqPP3-Kg6NU1Q"}]}
上面的示例代码中有效载荷(payload)的JSON数据如下:
{"iss":"joe","exp":1300819380,"http://example.com/is_root":true}
signatures JSON数组中第一个JSON签名对象使用RSASSA-PKCS1-v1_5 SHA-256签名算法,第二个使用ECDSA P-256 SHA-256算法,这些算法被定义在IETF的JOSE工作组开发和制定的JSON Web Algorithms (JWA)规范中。下面是构建这个JSON序列化的JWS令牌的过程。
- 构建Payload顶级元素,这个Payload是两个签名共享的数据。上面的有效载荷数据如下。
{"iss":"joe","exp":1300819380,"http://example.com/is_root":true}
我们对它进行BASE64URL计算,得到Payload的数据是
eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFt
cGxlLmNvbS9pc19yb290Ijp0cnVlfQ
- 构建每个签名受保护的头(Protected Headers)的值。 用于第一个签名的JWS受保护的头值为是:
{"alg":"RS256"}
将此JWS受保护的头进行BASE64URL计算,即BASE64URL(UTF8(JWS Protected Header)) ,得到protected属性值:
eyJhbGciOiJSUzI1NiJ9
用于第二个签名的JWS受保护的头值为是:
{"alg":"ES256"}
将此JWS受保护的头进行BASE64URL计算,即BASE64URL(UTF8(JWS Protected Header)) ,得到第二个签名protected属性值:
eyJhbGciOiJFUzI1NiJ9
- 构建每个签名不受保护的头(Unprotected Headers)。 每个签名的不受保护的头里提供了密钥ID值,即kid。第一个密钥ID(kid)的值是:
{"kid":"2010-12-29"}
第二个密钥ID(kid)的值是:
{"kid":"e9bc097a-ce51-4036-9562-d2ade882db0d"}
- 得到两个签名完整的JOSE Header值。 将JWS受保护的头和不受保护的头组合起来,得到两个签名完整的JOSE Header。第一个签名完整的JOSE Header是:
{"alg":"RS256","kid":"2010-12-29"}
第二个签名完整的JOSE Header是:
{"alg":"ES256","kid":"e9bc097a-ce51-4036-9562-d2ade882db0d"}
- 将上面Payload和受保护的头结合起来,依次得到两个签名原始的未被签名的消息,即BASE64URL(UTF8(JWS Protected Header)) || ‘.’ || BASE64URL(JWS Payload)。该步骤和构建压缩序列化JWS的步骤相同因此不再赘述。
- 依次对上面两个原始的未被签名的消息进行签名计算。该步骤和构建压缩序列化JWS的步骤相同因此不再赘述。
- 依次将这些结果依次组合起来,得到最终的签名结果。
JSON Web Encryption(JWE)
WE是JWT的另一种形式。它具有比JWS更复杂的结构。我们上面介绍的JWS可以对部分或全部的头和有效载荷进行签名,签名可以防止中间人对令牌进行篡改,从而保护令牌的完整性。而JWE可以保证数据的机密性。如果我们以上面的委任状JWT为例,如果这些内容是被加密过的,那么只有委任状的接收者鹅城才能解密和阅读这些内容,这对其他接收方来说好像“天书”一般。
JWE也是IEFT的JOSE工作组开发和制定的,它被定义在RFC 7516文档中。与JWS一样,JWE的载荷(Payload)可以是任何形式,例如JSON、XML甚至是二进制数据,它也支持两种序列化方式——JWE压缩序列化(JWE Compact Serialization)和JWE JSON序列化(JWE JSON Serialization)。我们只把载荷数据格式是JSON并且序列化方式是JWE压缩序列化的JWE称为JWT。在文中我们将只讨论JWE压缩序列化且Payload格式是JSON的JWE。
JWE压缩序列化
如下图所示,一般的,一个JWE压缩序列化后的JWE可以分成五个部分,每个部分都是经过Base64URL编码的,各部分之间用句点(.)作为分割符号。
- JWE Protected Header:类似于JWS的JOSE Header。JSON对象,描述一组加密操作的参数。
- JWE Encrypted Key:这是一个对称密钥,用于加密密文和其他加密数据。在JWE中,内容加密密钥( Content Encryption Key,简称CEK)用来对明文的载荷(Payload)数据进行加密,CEK是一个对称密钥,使用CEK对称密钥对明文载荷加密后,我们需要使用alg定义的算法对CEK对称密钥进行加密。一般的alg定义的对CEK进行加密的算法都是非对称加密算法。JWE这样设计的原因在于Payload数据一般比较大,使用非对称加密需要消耗大量的CPU资源,因此使用对称加密算法CEK密钥对Payload进行加密后,对CEK进行非对称加密是一种更为合理的做法。对CEK加密后的结果被组织在JWE的第二部分——JWE Encrypted Key中。
- JWE Initialization Vector:加密明文时使用的初始化矢量值,某些算法可能不使用初始化向量。
- JWE Ciphertext:实际的密文数据。
- JWE Authentication Tag:在加密明文JSON数据的过程中产生的附加数据,来防止密文被篡改。可以将其理解为JWS的签名。
JWE的JOSE Header
我在JWS的部分向你描述了JWS规范定义的一组标准化的Header名称。JWE规范同样定义了一组标准的Header名称,这些Header名称大部分与JWS一致。因此我只选择性地描述这些Header名称,其它你可以参考JWS的JOSE Header部分,也可以直接阅读JWE规范的section-4部分获取这些内容的细节。如下图所示,我向你描述了JWE Header中的关键内容和它们的作用。
- alg (Algorithm):在JWE中,内容加密密钥( Content Encryption Key,简称CEK)用来对明文的载荷(Payload)数据进行加密,CEK是一个对称密钥,使用CEK对称密钥对明文载荷加密后,我们需要使用alg定义的算法对CEK对称密钥进行加密。一般的alg定义的对CEK进行加密的算法都是非对称加密算法。JWE这样设计的原因在于Payload数据一般比较大,使用非对称加密需要消耗大量的CPU资源,因此使用对称加密算法CEK密钥对Payload进行加密后,对CEK进行非对称加密是一种更为合理的做法。对CEK加密后的结果被组织在JWE的第二部分——JWE Encrypted Key中。
- enc(Encryption Algorithm) :对Payload进行对称加密的算法名称。这些算法同样需要在IETF的JOSE工作组开发和制定的JSON Web Algorithms (JWA)规范中选取。
- zip(Compression Algorithm):对明文JSON Payload进行压缩的算法,一般的,DEF压缩算法是JWE推荐的压缩算法。
- jku (JWK Set URL):略。
- jwk (JSON Web Key):略。
- kid (Key ID):略。
- x5u (X.509 URL):略。
- x5c (X.509 Certificate Chain):略。
- x5t (X.509 Certificate SHA-1 Thumbprint):略。
- x5t#S256 (X.509 Certificate SHA-256 Thumbprint):略。
- typ (Type):略。
- cty (Content Type):略。
- crit (Critical):略。
如何构建一个压缩序列化的JWE
JWE压缩序列化将加密内容表现成一个紧凑的URL安全的字符串。这个字符串的格式是:
BASE64URL(UTF8(JWE Protected Header)) || '.' ||
BASE64URL(JWE Encrypted Key) || '.' ||
BASE64URL(JWE Initialization Vector) || '.' ||
BASE64URL(JWE Ciphertext) || '.' ||
BASE64URL(JWE Authentication Tag)
下面是一个JWE的令牌实例,在这个实例中我们期望将明文“The true sign of intelligence is not knowledge but imagination.”(智慧的真正标志不是知识,而是想象力。)生成一个JWE令牌。我们讲解生成这样一个JWE令牌的过程和步骤。
eyJlbmMiOiJBMjU2R0NNIiwiYWxnIjoiUlNBLU9BRVAifQ.
evbz8fVldyDfdcLDVYaS9NUFSnNenHulZBq_wD1YBZ8kEMAeDico15aXMhD4RYZMjdRuLd8H5DvLAEHkfMS2yCzrHKILG73QxebWk6cN_G7YzhRiocgi-hs6xmXH9Sw3qnPnBh-wbhaRdK1c0hYSpvzGEQnWzP5UySXJluItkLkTwS3lHj-Skh-SUhUtD2qC6lDNxXd5gxNXZBymcNUdXIu_h-jO2hjaEWlICEUXw3w2O_BochirYPqPOM9z0ixE7ut1IxMfGZbVY7frn5EO3IHlYK2pf_9PGGvJsiRYs8KrfpSUFCooXvz2veX4FpDRiuTS_WIfeErPtSf-CWq-wQ.
BnMsAKUqn8N8VQtn.
IAzLN7DgnQNXGtRTr0ppjwdJt6gpJYIn1hStIyCCXiymYEtRXE95JSzKbPQVp_Kg37gUfxWw_z8E4Chl5bd6.
aQ9L41BB1zbD8jAU9cTAdQ
- 生成JWE受保护的头(JWE Protected Header)信息,即整个JWE的第一部分。在我们的JWE中,我们使用RSAES-OAEP算法对Content Encryption Key加密生成JWE Encrypted Key,使用使用256位密钥的AES GCM算法,生成密文(ciphertext )和验证标签(Authentication Tag)。因此JWE的JOSE Header如下:
{
"alg":"RSA-OAEP",
"enc":"A256GCM"
}
对上面的值进行BASE64URL编码计算,即 BASE64URL(UTF8(JWE Protected Header)),得到整个JWE的第一部分——JWE Protected Header。
- 生成一个随机的Content Encryption Key (CEK)。
- 使用RSAES-OAEP算法对Content Encryption Key加密生成JWE Encrypted Key,然后对其进行Base64url编码。这是整个JWE的第二部分。对其进行BASE64URL编码后,得到JWE的第二部分,即BASE64URL(JWE Encrypted Key)。
- 生成随机的JWE初始化向量(JWE Initialization Vector),然后对其进行Base64url编码。这是整个JWE的第三部分。
- 构建附加认证数据(Additional Authenticated Data),即ASCII(BASE64URL(UTF8(JWE Protected Header)))。
- 如果需要令牌压缩,则必须按照Header参数下zip属性定义的压缩算法压缩明文形式的JSON负载。
- 按照enc头参数定义的内容加密算法,使用CEK(Content Encryption Key,第2步生成)、JWE初始化向量(JWE Initialization Vector,在第4步中生成)和附加认证数据(Additional Authenticated Data,在第5步中生成)加密压缩的JSON有效载荷,生成密文数据以及验证标签值(这是在加密载荷过程中生成的附加数据,是对密文数据的签名,可以防止密文数据被篡改)。
- 对上面生成的JWE密文数据进行BASE64URL编码,生成JWE的第四部分数据(JWE Ciphertext),即BASE64URL(JWE Ciphertext)。
- 对JWE身份验证标记值(Authentication Tag value)进行BASE64URL编码,生成JWE第五部分值。
- 将上面各部分的值按照下面的顺序组合起来,得到最终的JWE令牌值。
BASE64URL(UTF8(JWE Protected Header)) || '.' ||
BASE64URL(JWE Encrypted Key) || '.' ||
BASE64URL(JWE Initialization Vector) || '.' ||
BASE64URL(JWE Ciphertext) || '.' ||
BASE64URL(JWE Authentication Tag)
撤销和失效JWT
当我们使用JWT时,一个无法逃避的问题是——如何撤销或者让JWT失效。JWT已经被我们广泛使用,但被人诟病的是,一旦JWT被签发,就再也无法控制JWT。将JWT的持续时间设置为较短的时间(几分钟或几秒)看起来是一种可行解,另一种显而易见的做法是持久化每一个JWT,然后使用JWT时通过数据库进行校验,但这与JWT的理念是相悖的,使用JWT的意义就在于无状态地校验和使用令牌,一旦你拿到了JWT,就无需再向令牌签发方问询令牌的有效性。遗憾的是,JOSE工作组并没有给出一个标准答案,我在此列出一些可行的方案。如下图所示,第三方客户端在第(1)步中向授权服务器的令牌端点发起令牌请求,在第(2)步中客户端得到了授权服务器签发的一个JWS令牌,这个令牌使用RSA的私钥进行签名,在第(3)步中客户端携带该JWT令牌向授权服务器的API端点发起资源调用请求,资源服务器使用签名私钥对应的公钥验证签名。然而如果授权服务器因为某些原因吊销了该JWS令牌,资源服务器如何知晓该JWS令牌已经被失效了呢?
- 尽量使用较短生命周期的访问令牌,而使用较长生命周期的刷新令牌。例如JWT访问令牌的生命周期设置为五到十分钟,而刷新令牌的生命周期设置为一周甚至以月为单位,资源服务器只在五到十分钟内认为JWT令牌是有效的,当撤销该JWT令牌时优先撤销该访问令牌对应的刷新令牌,所以下一次当JWT访问令牌失效时当客户端尝试使用刷新令牌获得新的访问令牌将失败,这迫使终端用户再次进行登录或授权操作。但是这种方案的弊端在于撤销JWT的动作并非立竿见影,我们需要忍受这五到十分钟内的偏差。
- 旋转密钥。如果资源服务器每次校验JWS令牌时都从授权服务器的JWK Set端点拉取最新的公钥集(如上图的第四步所示,我们的授权服务器也会支持这个JWK Set端点),如果当时签名该JWS的公钥被删除,那么资源服务器就会验签失败。假设一开始签名该JWS令牌私钥对应的公钥kid是ABC,如果后面删除了该kid对应的公钥,那么资源服务器将验签失败。使用这种方法同样有弊端,资源服务器必须不断轮询向授权服务器的JWK Set端点发起请求拉取最新的公钥集,这牺牲了一些JWT令牌无状态的优势。另外一个弊端在于删除一个公钥,那么当时使用公钥对应的私钥签发的所有JWT令牌都会被撤销。
- 构建JWT令牌白名单列表。签发JWT令牌后,我们将JWT令牌的jti(JWT令牌的唯一标识)保存在内存数据库中,撤销该JWT令牌后我们将jti从数据库中删除。那么当下一次资源服务器校验令牌时,需要检查jti是否还存在,在Redis中这就是一条简单的命令——EXISTS key。如果不存在那么表示该令牌已经被撤销。
- 构建JWT令牌黑名单列表。与上面构建白名单的方式不同,这种方式只需要记录被撤销的令牌jti。这大大减小了保存的体积。如果该JWT令牌被撤销,那么它的jti将会在黑名单列表,如果存在表示该令牌已经被撤销
总结
- 除了不透明令牌,JWT这种自包含的令牌变得越来越流行和实用。JOSE规范可以认为是JWT的扩展,它提供了包括令牌签名、令牌加密、令牌加密算法、令牌公钥集等相关的内容。
- 如无必要,请不要实现自己的JWT库,因为构建JWT需要充分的安全和加密相关的知识。即使对于有丰富经验的开发者,实现自己的JWT库仍然需要深思熟虑,你必须保证自己的JWT库是经过充分验证和测试的。你可以在这个地址(https://openid.net/developers/jwt/)选择适合自己需求的JWT库。
- JWT一个显著的缺陷在于“覆水难收”,因此如何回收或者撤销JWT是开发者面临的一个重大挑战。我在上文向你介绍了业界主流的撤销和失效JWT的方式,你可以灵活选择和使用。
版权归原作者 weixin_45946850 所有, 如有侵权,请联系我们删除。