快递客服主要做什么| b超fl是什么意思| 最好的洗发水是什么牌子| 撬墙角是什么意思| 大便发黑是什么原因| 定增是什么意思| 乙酰氨基酚片是什么药| 上飞机不能带什么| lka是什么意思| 月子吃什么最下奶| 什么是胰腺炎| 鹅吃什么食物| 吃豆腐是什么意思| 企业背书是什么意思| 为什么没有广东大学| 电风扇不转是什么原因| louis是什么意思| 为什么土豆不能炒鸡蛋| 生活是什么| 急火攻心是什么生肖| 东西是什么意思| 姜文和姜武是什么关系| 娇羞是什么意思| 手足口是什么| 线差是什么意思| sheep什么意思| 他长什么样| 小孩肚子疼是什么原因| 仓鼠不能吃什么| 降血压吃什么| 鼻炎吃什么药好| 王毅什么级别| 阿昔洛韦是什么药| 达字五行属什么| 脱疽是什么意思| 总胆红素偏高是什么病| 身上长痣是什么原因| 眼睛发涩是什么原因导致的| 外阴白斑瘙痒用什么药| 胃幽门螺杆菌有什么症状| 什么树叶| 吃完饭就打嗝是什么原因| 马蜂窝治什么病最好| 马桶堵了用什么疏通| 司马光和司马迁是什么关系| 简直了是什么意思| 蜂蜜对人体有什么好处和功效| 硫酸镁注射有什么作用| 喉咙细菌感染吃什么药| 梦见小孩是什么| 澳门什么时候回归祖国| 甲状腺有什么反应| 掰手指头响有什么危害| 嘴唇下面长痘痘是什么原因| 椭圆机是什么| 鲁冰花是什么意思| 相知是什么意思| 冬枣是什么季节的水果| 龙猫是什么动物| 为什么会得hpv| 唐氏综合症是什么意思| 69年属什么生肖| 一个彭一个瓦念什么| 刻代表什么生肖| 头皮痛是什么原因| 尿急是什么症状| 心脏神经官能症吃什么药| 优柔寡断是什么意思| 草朋刀是什么字| 96年什么命| 平时血压高突然变低什么原因| 什么的生长| 强高是什么意思| 右眼上眼皮跳是什么预兆| 胸部中间痛什么原因引起的| 不是一路人是什么意思| trab抗体偏高代表什么| 脑干出血是什么原因| 什么样子| 七月十八是什么日子| 胆囊结石有什么影响| 阿罗裤是什么意思| 什么鸣什么吠| 丹参片和复方丹参片有什么区别| 炖牛骨头放什么调料| 高血压可以喝什么饮料| 什么叫直系亲属| 乳房疼痛应该挂什么科| 微米是什么单位| 师兄是什么意思| 11号来月经什么时候是排卵期| 为什么香蕉不能放冰箱| 红月亮是什么兆头| 每次来月经都会痛经什么原因| 眼角疼是什么原因| 喉咙痛吃什么药好| 吃什么最容易减肥| 佟丽娅是什么民族| 枸杞泡水喝有什么好处| bioisland是什么牌子| 脸上皮肤痒是什么原因| 农业户口和居民户口有什么区别| 脚怕冷是什么原因引起的| 怀孕10天左右有什么症状| 胎盘位于前壁是什么意思| 蛇形分班是什么意思| 什么是沉没成本| 支原体阳性什么意思| 备孕男性吃什么精子强| 二级烧伤是什么程度| 肺部感染有什么症状| 什么是血浆| mep是什么意思| 白衣天使是什么意思| 加盟什么店最赚钱投资小| 甘油三酯高应该注意什么| 鸡同鸭讲是什么意思| 甲状腺结节不能吃什么东西| 褥疮用什么药| 脑梗做什么检查最准确| 晚上吃什么能减肥| 羊奶不能和什么一起吃| 四时是什么时辰| 上皮细胞高是什么原因| 尿液少是什么原因| 藿香泡水喝有什么好处| 头寸是什么意思| 龙的本命佛是什么佛| 万箭穿心是什么意思| 法院起诉离婚需要什么材料| 部分空蝶鞍是什么意思| hedgren是什么品牌| 胃疼可以吃什么药| cd是什么元素| 双肺钙化灶是什么意思| crt是什么| 眩晕是怎么回事是什么原因引起| 境内是什么意思| 中风的人吃什么好| 新疆有什么特产| 7月28号是什么星座| 肠澼是什么意思| 非甾体是什么意思| 辛巳五行属什么| 1978年属马五行缺什么| 甘蔗男是什么意思| 985是什么学校| 小腿肌肉痛是什么原因| 地铁是什么| 胸闷挂什么科室| 减肥最快的方法是什么| 什么叫保守治疗| 血小板分布宽度偏高是什么意思| 唐僧叫什么名字| 气阴两虚吃什么药| 肌酐下降是什么原因| 除湿气喝什么茶| 男孩小名叫什么好听| 痛风能吃什么菜| 急性荨麻疹是什么原因引起的| 卵圆孔未闭挂什么科| 五劳七伤什么生肖| 水烧开后有白色沉淀物是什么| pph是什么意思| 百香果是什么季节的| 流鼻血什么原因| 妊娠期是什么意思| 芝士是什么东西| 发飙什么意思| 木丹念什么| 什么书买不到| 子宫直肠窝积液是什么意思| 撸铁是什么| 上午九点到十一点是什么时辰| 净身出户是什么意思| 隔离霜和粉底液有什么区别| 什么是熵| 七月七日是什么节日| 振水音阳性提示什么| 艾灸是什么东西| 吃什么增肥| 贫血补什么| 梦见办丧事是什么兆头| ca是什么意思| 女人吃什么补气血效果最好| 小孩子发烧手脚冰凉是什么原因| 什么时候闰十二月| 八月是什么星座| 伊玛目是什么意思| 小孩子睡觉流口水是什么原因| 1976年出生属什么生肖| 霉菌性阴道炎用什么药好得快| 乐松是什么药| 治疗狐臭最好的方法是什么| 肚子疼吃什么药| 陪嫁一般陪些什么东西| 有张有弛是什么意思| 冰箱发热是什么原因| 遗精是什么意思| 荥在中医读什么| 眼屎多用什么眼药水好| 女人手指粗短是什么命| 漠视是什么意思| 胃酸是什么酸| 转移灶是什么意思| 手指甲出现双层是什么原因| 上半身胖属于什么体质| 小孩热感冒吃什么药好| 物色什么意思| 交替脉见于什么病| 瘦肚子吃什么水果| 星期天左眼皮跳是什么预兆| 骨蒸潮热 是什么意思| 兔子可以吃什么| 福州立冬吃什么| 什么是友谊| 吃了榴莲不能吃什么| 困水是什么意思| 肺癌靶向治疗是什么意思| 净身是什么| 尿后余沥是什么意思| 心影不大是什么意思| 大便潜血什么意思| 爸爸生日礼物送什么| 锹形虫吃什么| 外周动脉僵硬度增高什么意思| 阿达是什么意思| 随波逐流什么意思| 霆字五行属什么| 杀虫剂中毒有什么症状| 尾牙是什么意思| 我会送你红色玫瑰是什么歌| 吃什么保养子宫和卵巢| 自卑是什么意思| 米粉和米线有什么区别| 经血发黑是什么原因| 糖类抗原什么意思| 烧心胃酸吃什么药| 胃疼恶心吃什么药效果好| 长期喝酒对身体有什么危害| 釜底抽薪什么意思| 肠胃不好吃什么水果比较好| 五什么四什么| 便秘吃什么润肠通便| 什么是通勤| 障碍性贫血是什么病| 软件开发需要学什么| 白芷泡水喝有什么功效| 情是什么意思| 七月一号是什么星座| 早些泄挂什么科| 放生鱼有什么好处| 相恋纪念日送什么礼物| 纳呆是什么意思| 6月24日是什么日子| 彩超挂什么科| 梦到车坏了是什么意思| 教师节送什么礼物好| 梦见考试是什么预兆| 手容易出汗是什么原因| 八髎区疼是什么原因| 什么叫潮吹| 扩容是什么意思| 同型半胱氨酸高挂什么科| 百度

娱乐圈最干净女星 相恋11年终于怀孕网友纷纷送上祝福

网络 通信技术
本文会翻炒一个用以产生访问令牌的开源标准JWT,介绍JWT的规范、底层实现原理、基本使用和应用场景。
百度 而在这些目标当中,最让恒大主帅卡纳瓦罗感到扼腕叹息的并不是奥巴梅扬,而是罗马后腰纳英戈兰。

 [[382145]]

本文转载自微信公众号「Throwable」,作者Throwable。转载本文请联系Throwable公众号。

本文会翻炒一个用以产生访问令牌的开源标准JWT,介绍JWT的规范、底层实现原理、基本使用和应用场景。

JWT规范

很可惜维基百科上没有搜索到JWT的条目,但是从jwt.io的首页展示图中,可以看到描述:

JSON Web Tokens are an open, industry standard RFC 7519 method for representing claims securely between two parties

从这段文字中可以提取到JWT的规范文件RFC 7519,里面有详细地介绍JWT的基本概念,Claims的含义、布局和算法实现等,下面逐个展开击破。

JWT基本概念

JWT全称是JSON Web Token,如果从字面上理解感觉是基于JSON格式用于网络传输的令牌。实际上,JWT是一种紧凑的Claims声明格式,旨在用于空间受限的环境进行传输,常见的场景如HTTP授权请求头参数和URI查询参数。JWT会把Claims转换成JSON格式,而这个JSON内容将会应用为JWS结构的有效载荷或者应用为JWE结构的(加密处理后的)原始字符串,通过消息认证码(Message Authentication Code或者简称MAC)和/或者加密操作对Claims进行数字签名或者完整性保护。

这里有三个概念在其他规范文件中,简单提一下:

  • JWE(规范文件RFC 7516):JSON Web Encryption,表示基于JSON数据结构的加密内容,加密机制对任意八位字节序列进行加密、提供完整性保护和提高破解难度,JWE中的紧凑序列化布局如下
  1. BASE64URL(UTF8(JWE Protected Header)) || '.' || 
  2. BASE64URL(JWE Encrypted Key) || '.' || 
  3. BASE64URL(JWE Initialization Vector) || '.' || 
  4. BASE64URL(JWE Ciphertext) || '.' || 
  5. BASE64URL(JWE Authentication Tag) 
  • JWA(规范文件RFC 7518):JSON Web Algorithm,JSON Web算法,数字签名或者MAC算法,应用于JWS的可用算法列表如下:

总的来说,JWT其实有两种实现,基于JWE实现的依赖于加解密算法、BASE64URL编码和身份认证等手段提高传输的Claims的被破解难度,而基于JWS的实现使用了BASE64URL编码和数字签名的方式对传输的Claims提供了完整性保护,也就是仅仅保证传输的Claims内容不被篡改,但是会暴露明文。「目前主流的JWT框架中大部分都没有实现JWE,所以下文主要通过JWS的实现方式进行深入探讨」。

JWT中的Claims

Claim有索赔、声称、要求或者权利要求的含义,但是笔者觉得任一个翻译都不怎么合乎语义,这里保留Claim关键字直接作为命名。JWT的核心作用就是保护Claims的完整性(或者数据加密),保证JWT传输的过程中Claims不被篡改(或者不被破解)。Claims在JWT原始内容中是一个JSON格式的字符串,其中单个Claim是K-V结构,作为JsonNode中的一个field-value,这里列出常用的规范中预定义好的Claim:

简称 全称 含义
iss Issuer 发行方
sub Subject 主体
aud Audience (接收)目标方
exp Expiration Time 过期时间
nbf Not Before 早于该定义的时间的JWT不能被接受处理
iat Issued At JWT发行时的时间戳
jti JWT ID JWT的唯一标识

这些预定义的Claim并不要求强制使用,何时选用何种Claim完全由使用者决定,而为了使JWT更加紧凑,这些Claim都使用了简短的命名方式去定义。在不和内建的Claim冲突的前提下,使用者可以自定义新的公共Claim,如:

简称 全称 含义
cid Customer ID 客户ID
rid Role ID 角色ID

一定要注意,在JWS实现中,Claims会作为payload部分进行BASE64编码,明文会直接暴露,敏感信息一般不应该设计为一个自定义Claim。

JWT中的Header

在JWT规范文件中称这些Header为JOSE Header,JOSE的全称为Javascript Object Signature Encryption,也就是Javascript对象签名和加密框架,JOSE Header其实就是Javascript对象签名和加密的头部参数。「下面列举一下JWS中常用的Header」:

 

简称 全称 含义
alg Algorithm 用于保护JWS的加解密算法
jku JWK Set URL 一组JSON编码的公共密钥的URL,其中一个是用于对JWS进行数字签名的密钥
jwk JSON Web Key 用于对JWS进行数字签名的密钥相对应的公共密钥
kid Key ID 用于保护JWS进的密钥
x5u X.509 URL X.509相关
x5c X.509 Certificate Chain X.509相关
x5t X.509 Certificate SHA-1 Thumbprin X.509相关
x5t#S256 X.509 Certificate SHA-256 Thumbprint X.509相关
typ Type 类型,例如JWTJWS或者JWE等等
cty Content Type 内容类型,决定payload部分的MediaType

最常见的两个Header就是alg和typ,例如:

  1.   "alg""HS256"
  2.   "typ""JWT" 

JWT的布局

主要介绍JWS的布局,前面已经提到过,JWS的「紧凑布局」如下:

  1. ASCII(BASE64URL(UTF8(JWS Protected Header)) || '.' ||  
  2. BASE64URL(JWS Payload)) || '.' || 
  3. BASE64URL(JWS Signature) 

其实还有「非紧凑布局」,会通过一个JSON结构完整地展示Header参数、Claims和分组签名:

  1.     "payload":"<payload contents>"
  2.     "signatures":[ 
  3.     {"protected":"<integrity-protected header 1 contents>"
  4.     "header":<non-integrity-protected header 1 contents>, 
  5.     "signature":"<signature 1 contents>"}, 
  6.     ... 
  7.     {"protected":"<integrity-protected header N contents>"
  8.     "header":<non-integrity-protected header N contents>, 
  9.     "signature":"<signature N contents>"}] 

非紧凑布局还有一个扁平化的表示形式:

  1.     "payload":"<payload contents>"
  2.     "protected":"<integrity-protected header contents>"
  3.     "header":<non-integrity-protected header contents>, 
  4.     "signature":"<signature contents>" 

其中Header参数部分可以参看上一小节,而签名部分可以参看下一小节,剩下简单提一下payload部分,payload(有效载荷)其实就是完整的Claims,假设Claims的JSON形式是:

  1.    "iss""throwx"
  2.    "jid": 1 

那么扁平化非紧凑格式下的payload节点就是:

  1. {   
  2.    ...... 
  3.    "payload": { 
  4.       "iss""throwx"
  5.       "jid": 1 
  6.    } 
  7.    ...... 

JWS签名算法

JWS签名生成依赖于散列或者加解密算法,可以使用的算法见前面贴出的图,例如HS256,具体是HMAC SHA-256,也就是通过散列算法SHA-256对于编码后的Header和Claims字符串进行一次散列计算,签名生成的伪代码如下:

  1. ## 不进行编码 
  2. HMACSHA256( 
  3.   base64UrlEncode(header) + "." + 
  4.   base64UrlEncode(payload), 
  5.   256 bit secret key 
  6.  
  7. ## 进行编码 
  8. base64UrlEncode( 
  9.     HMACSHA256( 
  10.        base64UrlEncode(header) + "." + 
  11.        base64UrlEncode(payload) 
  12.        [256 bit secret key]) 

其他算法的操作基本相似,生成好的签名直接加上一个前置的.拼接在base64UrlEncode(header).base64UrlEncode(payload)之后就生成完整的JWS。

JWT的生成、解析和校验

前面已经分析过JWT的一些基本概念、布局和签名算法,这里根据前面的理论进行JWT的生成、解析和校验操作。先引入common-codec库简化一些编码和加解密操作,引入一个主流的JSON框架做序列化和反序列化:

  1. <dependency> 
  2.     <groupId>commons-codec</groupId> 
  3.     <artifactId>commons-codec</artifactId> 
  4.     <version>1.15</version> 
  5. </dependency> 
  6. <dependency> 
  7.     <groupId>com.fasterxml.jackson.core</groupId> 
  8.     <artifactId>jackson-databind</artifactId> 
  9.     <version>2.11.0</version> 
  10. </dependency> 

为了简单起见,Header参数写死为:

  1.   "alg""HS256"
  2.   "typ""JWT" 

使用的签名算法是HMAC SHA-256,输入的加密密钥长度必须为256 bit(如果单纯用英文和数字组成的字符,要32个字符),这里为了简单起见,用00000000111111112222222233333333作为KEY。定义Claims部分如下:

  1.   "iss""throwx"
  2.   "jid": 10087,  # <---- 这里有个笔误,本来打算写成jti,后来发现写错了,不打算改 
  3.   "exp": 1613227468168     # 20210213     

生成JWT的代码如下:

  1. @Slf4j 
  2. public class JsonWebToken { 
  3.  
  4.     private static final String KEY = "00000000111111112222222233333333"
  5.  
  6.     private static final String DOT = "."
  7.  
  8.     private static final Map<String, String> HEADERS = new HashMap<>(8); 
  9.  
  10.     private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); 
  11.  
  12.     static { 
  13.         HEADERS.put("alg""HS256"); 
  14.         HEADERS.put("typ""JWT"); 
  15.     } 
  16.  
  17.     String generateHeaderPart() throws JsonProcessingException { 
  18.         byte[] headerBytes = OBJECT_MAPPER.writeValueAsBytes(HEADERS); 
  19.         String headerPart = new String(Base64.encodeBase64(headerBytes,false ,true), StandardCharsets.US_ASCII); 
  20.         log.info("生成的Header部分为:{}", headerPart); 
  21.         return headerPart; 
  22.     } 
  23.  
  24.     String generatePayloadPart(Map<String, Object> claims) throws JsonProcessingException { 
  25.         byte[] payloadBytes = OBJECT_MAPPER.writeValueAsBytes(claims); 
  26.         String payloadPart = new String(Base64.encodeBase64(payloadBytes,false ,true), StandardCharsets.UTF_8); 
  27.         log.info("生成的Payload部分为:{}", payloadPart); 
  28.         return payloadPart; 
  29.     } 
  30.  
  31.     String generateSignaturePart(String headerPart, String payloadPart) { 
  32.         String content = headerPart + DOT + payloadPart; 
  33.         Mac mac = HmacUtils.getInitializedMac(HmacAlgorithms.HMAC_SHA_256, KEY.getBytes(StandardCharsets.UTF_8)); 
  34.         byte[] output = mac.doFinal(content.getBytes(StandardCharsets.UTF_8)); 
  35.         String signaturePart = new String(Base64.encodeBase64(outputfalse ,true), StandardCharsets.UTF_8); 
  36.         log.info("生成的Signature部分为:{}", signaturePart); 
  37.         return signaturePart; 
  38.     } 
  39.  
  40.     public String generate(Map<String, Object> claims) throws Exception { 
  41.         String headerPart = generateHeaderPart(); 
  42.         String payloadPart = generatePayloadPart(claims); 
  43.         String signaturePart = generateSignaturePart(headerPart, payloadPart); 
  44.         String jws = headerPart + DOT + payloadPart + DOT + signaturePart; 
  45.         log.info("生成的JWT为:{}", jws); 
  46.         return jws; 
  47.     } 
  48.  
  49.     public static void main(String[] args) throws Exception { 
  50.         Map<String, Object> claims = new HashMap<>(8); 
  51.         claims.put("iss""throwx"); 
  52.         claims.put("jid", 10087L); 
  53.         claims.put("exp", 1613227468168L); 
  54.         JsonWebToken jsonWebToken = new JsonWebToken(); 
  55.         System.out.println("自行生成的JWT:" + jsonWebToken.generate(claims)); 
  56.     } 

执行输出日志如下:

  1. 23:37:48.743 [main] INFO club.throwable.jwt.JsonWebToken - 生成的Header部分为:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9 
  2. 23:37:48.747 [main] INFO club.throwable.jwt.JsonWebToken - 生成的Payload部分为:eyJpc3MiOiJ0aHJvd3giLCJqaWQiOjEwMDg3LCJleHAiOjE2MTMyMjc0NjgxNjh9 
  3. 23:37:48.748 [main] INFO club.throwable.jwt.JsonWebToken - 生成的Signature部分为:7skduDGxV-BP2p_CXyr3Na7WBvENNl--Pm4HQ8cJuEs 
  4. 23:37:48.749 [main] INFO club.throwable.jwt.JsonWebToken - 生成的JWT为:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ0aHJvd3giLCJqaWQiOjEwMDg3LCJleHAiOjE2MTMyMjc0NjgxNjh9.7skduDGxV-BP2p_CXyr3Na7WBvENNl--Pm4HQ8cJuEs 
  5. 自行生成的JWT:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ0aHJvd3giLCJqaWQiOjEwMDg3LCJleHAiOjE2MTMyMjc0NjgxNjh9.7skduDGxV-BP2p_CXyr3Na7WBvENNl--Pm4HQ8cJuEs 

可以在jwt.io上验证一下:

解析JWT的过程是构造JWT的逆向过程,首先基于点号.分三段,然后分别进行BASE64解码,然后得到三部分的明文,头部参数和有效载荷需要做一次JSON反序列化即可还原各个部分的JSON结构:

  1. public Map<Part, PartContent> parse(String jwt) throws Exception { 
  2.     System.out.println("当前解析的JWT:" + jwt); 
  3.     Map<Part, PartContent> result = new HashMap<>(8); 
  4.     // 这里暂且认为所有的输入JWT的格式都是合法的 
  5.     StringTokenizer tokenizer = new StringTokenizer(jwt, DOT); 
  6.     String[] jwtParts = new String[3]; 
  7.     int idx = 0; 
  8.     while (tokenizer.hasMoreElements()) { 
  9.         jwtParts[idx] = tokenizer.nextToken(); 
  10.         idx++; 
  11.     } 
  12.     String headerPart = jwtParts[0]; 
  13.     PartContent headerContent = new PartContent(); 
  14.     headerContent.setRawContent(headerPart); 
  15.     headerContent.setPart(Part.HEADER); 
  16.     headerPart = new String(Base64.decodeBase64(headerPart), StandardCharsets.UTF_8); 
  17.     headerContent.setPairs(OBJECT_MAPPER.readValue(headerPart, new TypeReference<Map<String, Object>>() { 
  18.     })); 
  19.     result.put(Part.HEADER, headerContent); 
  20.     String payloadPart = jwtParts[1]; 
  21.     PartContent payloadContent = new PartContent(); 
  22.     payloadContent.setRawContent(payloadPart); 
  23.     payloadContent.setPart(Part.PAYLOAD); 
  24.     payloadPart = new String(Base64.decodeBase64(payloadPart), StandardCharsets.UTF_8); 
  25.     payloadContent.setPairs(OBJECT_MAPPER.readValue(payloadPart, new TypeReference<Map<String, Object>>() { 
  26.     })); 
  27.     result.put(Part.PAYLOAD, payloadContent); 
  28.     String signaturePart = jwtParts[2]; 
  29.     PartContent signatureContent = new PartContent(); 
  30.     signatureContent.setRawContent(signaturePart); 
  31.     signatureContent.setPart(Part.SIGNATURE); 
  32.     result.put(Part.SIGNATURE, signatureContent); 
  33.     return result; 
  34.  
  35. enum Part { 
  36.  
  37.     HEADER, 
  38.  
  39.     PAYLOAD, 
  40.  
  41.     SIGNATURE 
  42.  
  43. @Data 
  44. public static class PartContent { 
  45.  
  46.     private Part part; 
  47.  
  48.     private String rawContent; 
  49.  
  50.     private Map<String, Object> pairs; 

这里尝试用之前生产的JWT进行解析:

  1. public static void main(String[] args) throws Exception { 
  2.     JsonWebToken jsonWebToken = new JsonWebToken(); 
  3.     String jwt = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ0aHJvd3giLCJqaWQiOjEwMDg3LCJleHAiOjE2MTMyMjc0NjgxNjh9.7skduDGxV-BP2p_CXyr3Na7WBvENNl--Pm4HQ8cJuEs"
  4.     Map<Part, PartContent> parseResult = jsonWebToken.parse(jwt); 
  5.     System.out.printf("解析结果如下:\nHEADER:%s\nPAYLOAD:%s\nSIGNATURE:%s%n"
  6.             parseResult.get(Part.HEADER), 
  7.             parseResult.get(Part.PAYLOAD), 
  8.             parseResult.get(Part.SIGNATURE) 
  9.     ); 

解析结果如下:

  1. 当前解析的JWT:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ0aHJvd3giLCJqaWQiOjEwMDg3LCJleHAiOjE2MTMyMjc0NjgxNjh9.7skduDGxV-BP2p_CXyr3Na7WBvENNl--Pm4HQ8cJuEs 
  2. 解析结果如下: 
  3. HEADER:PartContent(part=HEADER, rawContent=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9, pairs={typ=JWT, alg=HS256}) 
  4. PAYLOAD:PartContent(part=PAYLOAD, rawContent=eyJpc3MiOiJ0aHJvd3giLCJqaWQiOjEwMDg3LCJleHAiOjE2MTMyMjc0NjgxNjh9, pairs={iss=throwx, jid=10087, exp=1613227468168}) 
  5. SIGNATURE:PartContent(part=SIGNATURE, rawContent=7skduDGxV-BP2p_CXyr3Na7WBvENNl--Pm4HQ8cJuEs, pairs=null) 

验证JWT建立在解析JWT完成的基础之上,需要对解析出来的头部参数和有效载做一次MAC签名,与解析出来的签名做校对。另外,可以自定义校验具体的Claim项,如过期时间和发行者等。一般校验失败会针对不同的情况定制不同的运行时异常便于区分场景,这里为了方便统一抛出IllegalStateException:

  1. public void verify(String jwt) throws Exception { 
  2.     System.out.println("当前校验的JWT:" + jwt); 
  3.     Map<Part, PartContent> parseResult = parse(jwt); 
  4.     PartContent headerContent = parseResult.get(Part.HEADER); 
  5.     PartContent payloadContent = parseResult.get(Part.PAYLOAD); 
  6.     PartContent signatureContent = parseResult.get(Part.SIGNATURE); 
  7.     String signature = generateSignaturePart(headerContent.getRawContent(), payloadContent.getRawContent()); 
  8.     if (!Objects.equals(signature, signatureContent.getRawContent())) { 
  9.         throw new IllegalStateException("签名校验异常"); 
  10.     } 
  11.     String iss = payloadContent.getPairs().get("iss").toString(); 
  12.     // iss校验 
  13.     if (!Objects.equals(iss, "throwx")) { 
  14.         throw new IllegalStateException("ISS校验异常"); 
  15.     } 
  16.     long exp = Long.parseLong(payloadContent.getPairs().get("exp").toString()); 
  17.     // exp校验,有效期14天 
  18.     if (System.currentTimeMillis() - exp > 24 * 3600 * 1000 * 14) { 
  19.         throw new IllegalStateException("exp校验异常,JWT已经过期"); 
  20.     } 
  21.     // 省略其他校验项 
  22.     System.out.println("JWT校验通过"); 

类似地,用上面生成过的JWT进行验证,结果如下:

  1. 当前校验的JWT:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ0aHJvd3giLCJqaWQiOjEwMDg3LCJleHAiOjE2MTMyMjc0NjgxNjh9.7skduDGxV-BP2p_CXyr3Na7WBvENNl--Pm4HQ8cJuEs 
  2. 当前解析的JWT:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ0aHJvd3giLCJqaWQiOjEwMDg3LCJleHAiOjE2MTMyMjc0NjgxNjh9.7skduDGxV-BP2p_CXyr3Na7WBvENNl--Pm4HQ8cJuEs 
  3. 23:33:00.174 [main] INFO club.throwable.jwt.JsonWebToken - 生成的Signature部分为:7skduDGxV-BP2p_CXyr3Na7WBvENNl--Pm4HQ8cJuEs 
  4. JWT校验通过 

「上面的代码存在硬编码问题,只是为了用最简单的JWS实现方式重新实现了JWT的生成、解析和校验过程」,算法也使用了复杂程度和安全性极低的HS256,所以在生产中并不推荐花大量时间去实现JWS,可以选用现成的JWT类库,如auth0和jjwt。

JWT的使用场景和实战

JWT本质是一个令牌,更多场景下是作为会话ID(session_id)使用,作用是'维持会话的粘性'和携带认证信息(如果用JWT术语,应该是安全地传递Claims)。笔者记得很久以前使用的一种Session ID解决方案是由服务端生成和持久化Session ID,返回的Session ID需要写入用户的Cookie,然后用户每次请求必须携带Cookie,Session ID会映射用户的一些认证信息,这一切都是由服务端管理,一个很常见的例子就是Tomcat容器中出现的J(ava)SESSIONID。与之前的方案不同,JWT是一种无状态的令牌,它并不需要由服务端保存,携带的数据或者会话的数据都不需要持久化,使用JWT只需要关注Claims的完整性和合法性即可,生成JWT时候所有有效数据已经通过编码存储在JWT字符串中。正因JWT是无状态的,一旦颁发后得到JWT的客户端都可以通过它与服务端交互,JWT一旦泄露有可能造成严重安全问题,因此实践的时候一般需要做几点:

  • JWT需要设置有效期,也就是exp这个Claim必须启用和校验
  • JWT需要建立黑名单,一般使用jti这个Claim即可,技术上可以使用布隆过滤器加数据库的组合(数量少的情况下简单操作甚至可以用Redis的SET数据类型)
  • JWS的签名算法尽可能使用安全性高的算法,如RSXXX
  • Claims尽可能不要写入敏感信息
  • 高风险场景如支付操作等不能仅仅依赖JWT认证,需要进行短信、指纹等二次认证

PS:身边有不少同事所在的项目会把JWT持久化,其实这违背了JWT的设计理念,把JWT当成传统的会话ID使用了

 

JWT一般用于认证场景,搭配API网关使用效果甚佳。多数情况下,API网关会存在一些通用不需要认证的接口,其他则是需要认证JWT合法性并且提取JWT中的消息载荷内容进行调用,针对这个场景:

  • 对于控制器入口可以提供一个自定义注解标识特定接口需要进行JWT认证,这个场景在Spring Cloud Gateway中需要自定义实现一个JWT认证的WebFilter
  • 对于单纯的路由和转发可以提供一个URI白名单集合,命中白名单则不需要进行JWT认证,这个场景在Spring Cloud Gateway中需要自定义实现一个JWT认证的GlobalFilter

下面就Spring Cloud Gateway和jjwt,贴一些骨干代码,限于篇幅不进行细节展开。引入依赖:

  1. <dependencyManagement> 
  2.     <dependencies> 
  3.         <dependency> 
  4.             <groupId>org.springframework.cloud</groupId> 
  5.             <artifactId>spring-cloud-dependencies</artifactId> 
  6.             <version>Hoxton.SR10</version> 
  7.             <type>pom</type> 
  8.             <scope>import</scope> 
  9.         </dependency> 
  10.     </dependencies> 
  11. </dependencyManagement> 
  12. <dependencies> 
  13.     <dependency> 
  14.         <groupId>io.jsonwebtoken</groupId> 
  15.         <artifactId>jjwt-api</artifactId> 
  16.         <version>0.11.2</version> 
  17.     </dependency> 
  18.     <dependency> 
  19.         <groupId>io.jsonwebtoken</groupId> 
  20.         <artifactId>jjwt-impl</artifactId> 
  21.         <version>0.11.2</version> 
  22.         <scope>runtime</scope> 
  23.     </dependency> 
  24.     <dependency> 
  25.         <groupId>io.jsonwebtoken</groupId> 
  26.         <artifactId>jjwt-jackson</artifactId> 
  27.         <version>0.11.2</version> 
  28.         <scope>runtime</scope> 
  29.     </dependency> 
  30.     <dependency> 
  31.         <groupId>org.projectlombok</groupId> 
  32.         <artifactId>lombok</artifactId> 
  33.         <version>1.18.18</version> 
  34.         <scope>provided</scope> 
  35.     </dependency> 
  36.     <dependency> 
  37.         <groupId>org.springframework.cloud</groupId> 
  38.         <artifactId>spring-cloud-starter-gateway</artifactId> 
  39.     </dependency> 
  40. </dependencies> 

然后编写JwtSpi和对应的实现HMAC256JwtSpiImpl:

  1. @Data 
  2. public class CreateJwtDto { 
  3.  
  4.     private Long customerId; 
  5.  
  6.     private String customerName; 
  7.  
  8.     private String customerPhone; 
  9.  
  10. @Data 
  11. public class JwtCacheContent { 
  12.  
  13.     private Long customerId; 
  14.  
  15.     private String customerName; 
  16.  
  17.     private String customerPhone; 
  18.  
  19. @Data 
  20. public class VerifyJwtResultDto { 
  21.  
  22.     private Boolean valid; 
  23.  
  24.     private Throwable throwable; 
  25.  
  26.     private long jwtId; 
  27.  
  28.     private JwtCacheContent content; 
  29.  
  30. public interface JwtSpi { 
  31.  
  32.     /** 
  33.      * 生成JWT 
  34.      * 
  35.      * @param dto dto 
  36.      * @return String 
  37.      */ 
  38.     String generate(CreateJwtDto dto); 
  39.  
  40.     /** 
  41.      * 校验JWT 
  42.      * 
  43.      * @param jwt jwt 
  44.      * @return VerifyJwtResultDto 
  45.      */ 
  46.     VerifyJwtResultDto verify(String jwt); 
  47.  
  48.     /** 
  49.      * 把JWT添加到封禁名单中 
  50.      * 
  51.      * @param jwtId jwtId 
  52.      */ 
  53.     void blockJwt(long jwtId); 
  54.  
  55.     /** 
  56.      * 判断JWT是否在封禁名单中 
  57.      * 
  58.      * @param jwtId jwtId 
  59.      * @return boolean 
  60.      */ 
  61.     boolean isInBlockList(long jwtId); 
  62.  
  63. @Component 
  64. public class HMAC256JwtSpiImpl implements JwtSpi, InitializingBean, EnvironmentAware { 
  65.  
  66.     private SecretKey secretKey; 
  67.     private Environment environment; 
  68.     private int minSeed; 
  69.     private String issuer; 
  70.     private int seed; 
  71.     private Random random; 
  72.  
  73.     @Override 
  74.     public void afterPropertiesSet() throws Exception { 
  75.         String secretKey = Objects.requireNonNull(environment.getProperty("jwt.hmac.secretKey")); 
  76.         this.minSeed = Objects.requireNonNull(environment.getProperty("jwt.exp.seed.min"Integer.class)); 
  77.         int maxSeed = Objects.requireNonNull(environment.getProperty("jwt.exp.seed.max"Integer.class)); 
  78.         this.issuer = Objects.requireNonNull(environment.getProperty("jwt.issuer")); 
  79.         this.random = new Random(); 
  80.         this.seed = (maxSeed - minSeed); 
  81.         this.secretKey = new SecretKeySpec(secretKey.getBytes(), "HmacSHA256"); 
  82.     } 
  83.  
  84.     @Override 
  85.     public void setEnvironment(Environment environment) { 
  86.         this.environment = environment; 
  87.     } 
  88.  
  89.     @Override 
  90.     public String generate(CreateJwtDto dto) { 
  91.         long duration = this.random.nextInt(this.seed) + minSeed; 
  92.         Map<String, Object> claims = new HashMap<>(8); 
  93.         claims.put("iss", issuer); 
  94.         // 这里的jti最好用类似雪花算法之类的序列算法生成,确保唯一性 
  95.         claims.put("jti", dto.getCustomerId()); 
  96.         claims.put("uid", dto.getCustomerId()); 
  97.         claims.put("exp", TimeUnit.NANOSECONDS.toMillis(System.nanoTime()) + duration); 
  98.         String jwt = Jwts.builder() 
  99.                 .setHeaderParam("typ""JWT"
  100.                 .signWith(this.secretKey, SignatureAlgorithm.HS256) 
  101.                 .addClaims(claims) 
  102.                 .compact(); 
  103.         // 这里需要缓存uid->JwtCacheContent的信息 
  104.         JwtCacheContent content = new JwtCacheContent(); 
  105.         // redis.set(KEY[uid],toJson(content),expSeconds); 
  106.         return jwt; 
  107.     } 
  108.  
  109.     @Override 
  110.     public VerifyJwtResultDto verify(String jwt) { 
  111.         JwtParser parser = Jwts.parserBuilder() 
  112.                 .requireIssuer(this.issuer) 
  113.                 .setSigningKey(this.secretKey) 
  114.                 .build(); 
  115.         VerifyJwtResultDto resultDto = new VerifyJwtResultDto(); 
  116.         try { 
  117.             Jws<Claims> parseResult = parser.parseClaimsJws(jwt); 
  118.             Claims claims = parseResult.getBody(); 
  119.             long jti = Long.parseLong(claims.getId()); 
  120.             if (isInBlockList(jti)) { 
  121.                 throw new IllegalArgumentException(String.format("jti is in block list,[i:%d]", jti)); 
  122.             } 
  123.             long uid = claims.get("uid", Long.class); 
  124.             // JwtCacheContent content = JSON.parse(redis.get(KEY[uid]),JwtCacheContent.class); 
  125.             // resultDto.setContent(content); 
  126.             resultDto.setValid(Boolean.TRUE); 
  127.         } catch (Exception e) { 
  128.             resultDto.setValid(Boolean.FALSE); 
  129.             resultDto.setThrowable(e); 
  130.         } 
  131.         return resultDto; 
  132.     } 
  133.  
  134.     @Override 
  135.     public void blockJwt(long jwtId) { 
  136.  
  137.     } 
  138.  
  139.     @Override 
  140.     public boolean isInBlockList(long jwtId) { 
  141.         return false
  142.     } 

然后是JwtGlobalFilter和JwtWebFilter的非完全实现:

  1. @Component 
  2. public class JwtGlobalFilter implements GlobalFilter, Ordered, EnvironmentAware { 
  3.  
  4.     private final AntPathMatcher pathMatcher = new AntPathMatcher(); 
  5.  
  6.     private List<String> accessUriList; 
  7.  
  8.     @Autowired 
  9.     private JwtSpi jwtSpi; 
  10.  
  11.     private static final String JSON_WEB_TOKEN_KEY = "X-TOKEN"
  12.     private static final String UID_KEY = "X-UID"
  13.     private static final String JWT_ID_KEY = "X-JTI"
  14.  
  15.     @Override 
  16.     public void setEnvironment(Environment environment) { 
  17.         accessUriList = Arrays.asList(Objects.requireNonNull(environment.getProperty("jwt.access.uris")) 
  18.                 .split(",")); 
  19.     } 
  20.  
  21.     @Override 
  22.     public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { 
  23.         ServerHttpRequest request = exchange.getRequest(); 
  24.         // OPTIONS 请求直接放行 
  25.         HttpMethod method = request.getMethod(); 
  26.         if (Objects.nonNull(method) && Objects.equals(method, HttpMethod.OPTIONS)) { 
  27.             return chain.filter(exchange); 
  28.         } 
  29.         // 获取请求路径 
  30.         String requestPath = request.getPath().value(); 
  31.         // 命中请求路径白名单 
  32.         boolean matchWhiteRequestPathList = Optional.ofNullable(accessUriList) 
  33.                 .map(paths -> paths.stream().anyMatch(path -> pathMatcher.match(path, requestPath))) 
  34.                 .orElse(false); 
  35.         if (matchWhiteRequestPathList) { 
  36.             return chain.filter(exchange); 
  37.         } 
  38.         HttpHeaders headers = request.getHeaders(); 
  39.         String token = headers.getFirst(JSON_WEB_TOKEN_KEY); 
  40.         if (!StringUtils.hasLength(token)) { 
  41.             throw new BusinessException(BusinessErrorCode.TOKEN_ERROR.getCode(), "token is null"); 
  42.         } 
  43.         VerifyJwtResultDto resultDto = jwtSpi.verify(token); 
  44.         if (Objects.equals(resultDto.getValid(), Boolean.FALSE)) { 
  45.             throw new BusinessException(BusinessErrorCode.TOKEN_ERROR.getCode(), resultDto.getThrowable()); 
  46.         } 
  47.         headers.set(JWT_ID_KEY, String.valueOf(resultDto.getJwtId())); 
  48.         headers.set(UID_KEY, String.valueOf(resultDto.getContent().getCustomerId())); 
  49.         return chain.filter(exchange); 
  50.     } 
  51.  
  52.     @Override 
  53.     public int getOrder() { 
  54.         return 1; 
  55.     } 
  56.  
  57. @Component 
  58. public class JwtWebFilter implements WebFilter { 
  59.  
  60.     @Autowired 
  61.     private RequestMappingHandlerMapping requestMappingHandlerMapping; 
  62.  
  63.     @Autowired 
  64.     private JwtSpi jwtSpi; 
  65.  
  66.     private static final String JSON_WEB_TOKEN_KEY = "X-TOKEN"
  67.     private static final String UID_KEY = "X-UID"
  68.     private static final String JWT_ID_KEY = "X-JTI"
  69.  
  70.     @Override 
  71.     public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) { 
  72.         // OPTIONS 请求直接放行 
  73.         HttpMethod method = exchange.getRequest().getMethod(); 
  74.         if (Objects.nonNull(method) && Objects.equals(method, HttpMethod.OPTIONS)) { 
  75.             return chain.filter(exchange); 
  76.         } 
  77.         HandlerMethod handlerMethod = requestMappingHandlerMapping.getHandlerInternal(exchange).block(); 
  78.         if (Objects.isNull(handlerMethod)) { 
  79.             return chain.filter(exchange); 
  80.         } 
  81.         RequireJWT typeAnnotation = handlerMethod.getBeanType().getAnnotation(RequireJWT.class); 
  82.         RequireJWT methodAnnotation = handlerMethod.getMethod().getAnnotation(RequireJWT.class); 
  83.         if (Objects.isNull(typeAnnotation) && Objects.isNull(methodAnnotation)) { 
  84.             return chain.filter(exchange); 
  85.         } 
  86.         HttpHeaders headers = exchange.getRequest().getHeaders(); 
  87.         String token = headers.getFirst(JSON_WEB_TOKEN_KEY); 
  88.         if (!StringUtils.hasLength(token)) { 
  89.             throw new BusinessException(BusinessErrorCode.TOKEN_ERROR.getCode(), "token is null"); 
  90.         } 
  91.         VerifyJwtResultDto resultDto = jwtSpi.verify(token); 
  92.         if (Objects.equals(resultDto.getValid(), Boolean.FALSE)) { 
  93.             throw new BusinessException(BusinessErrorCode.TOKEN_ERROR.getCode(), resultDto.getThrowable()); 
  94.         } 
  95.         headers.set(JWT_ID_KEY, String.valueOf(resultDto.getJwtId())); 
  96.         headers.set(UID_KEY, String.valueOf(resultDto.getContent().getCustomerId())); 
  97.         return chain.filter(exchange); 
  98.     } 

最后是一些配置属性:

  1. jwt.hmac.secretKey='00000000111111112222222233333333' 
  2. jwt.exp.seed.min=360000 
  3. jwt.exp.seed.max=8640000 
  4. jwt.issuer='throwx' 
  5. jwt.access.uris=/index,/actuator/* 

使用JWT曾经遇到的坑

笔者负责的API网关使用了JWT应用于认证场景,算法上使用了安全性稍高的RS256,使用RSA算法进行签名生成。项目上线初期,JWT的过期时间都固定设置为7天,生产日志发现该API网关周期性发生"假死"现象,具体表现为:

  • Nginx自检周期性出现自检接口调用超时,提示部分或者全部API网关节点宕机
  • API网关所在机器的CPU周期性飙高,在用户访问量低的时候表现平稳
  • 通过ELK进行日志排查,发现故障出现时段有JWT集中性过期和重新生成的日志痕迹

排查结果表明JWT集中过期和重新生成时候使用RSA算法进行签名是CPU密集型操作,同时重新生成大量JWT会导致服务所在机器的CPU超负载工作。「初步的解决方案是」:

  • JWT生成的时候,过期时间添加一个随机数,例如360000(1小时的毫秒数) ~ 8640000(24小时的毫秒数)之间取一个随机值添加到当前时间戳加7天得到exp值

这个方法,对于一些老用户营销场景(老用户长时间没有登录,他们客户端缓存的JWT一般都已经过期)没有效果。有时候运营会通过营销活动唤醒老用户,大量老用户重新登录有可能出现爆发性大批量重新生成JWT的情况,对于这个场景提出两个解决思路:

  • 首次生成JWT时候,考虑延长过期时间,但是时间越长,风险越大
  • 提升API网关所在机器的硬件配置,特别是CPU配置,现在很多云厂商都有弹性扩容方案,可以很好应对这类突发流量场景

小结

主流的JWT方案是JWS,此方案是只编码和签名,不加密,务必注意这一点,JWS方案是无状态并且不安全的,关键操作应该做多重认证,也要做好黑名单机制防止JWT泄漏后造成安全性问题。JWT不存储在服务端,这既是它的优势,同时也是它的劣势。很多软件架构都无法做到尽善尽美,这个时候只能权衡利弊。

参考资料:

RFC 7519

jjwt部分源码

(本文完 c-3-w e-a-20210219)

 

责任编辑:武晓燕 来源: Throwable
相关推荐

2025-08-05 08:33:39

JDK底层UUID

2025-08-05 14:41:07

布隆过滤器算法

2025-08-05 11:48:57

广域网优化

2025-08-05 13:41:56

容器Docker

2025-08-05 02:25:00

CAP分布式系统

2025-08-05 09:00:00

NodeJS实现JWT

2025-08-05 16:58:39

应用通信安全苹果

2025-08-05 08:26:10

LooperAndroid内存

2025-08-05 09:01:07

DHCP

2025-08-05 15:22:31

论文抄袭

2025-08-05 15:24:05

Snowflake算法开源

2025-08-05 11:15:39

setTimeout前端代码

2025-08-05 09:57:52

空结构体map属性

2025-08-05 15:48:09

CSS Hack

2025-08-05 17:41:23

物联网

2025-08-05 08:30:36

vuereactvdom

2025-08-05 11:46:10

2025-08-05 07:38:23

displaycontentsCSS

2025-08-05 12:12:49

.NETJWTjson

2025-08-05 08:08:27

闭包编译器
点赞
收藏

51CTO技术栈公众号

入木三分是什么生肖 george是什么牌子 抵抗力差吃什么可以增强抵抗力 75属什么生肖 转奶是什么意思
甲亢是什么回事 闪回是什么意思 身上长疮是什么原因引起的 喝酒前吃什么不容易醉 柠檬泡水喝有什么功效
乳腺结节是什么引起的 孔雀开屏是什么意思 本来无一物何处惹尘埃什么意思 夏天脚开裂是什么原因 流连忘返是什么生肖
北京为什么叫四九城 谢霆锋什么学历 男人吃四环素治什么病 惹是什么意思 阿昔洛韦片是什么药
什么是宦官hcv7jop5ns1r.cn 减肥期间应该吃什么hcv8jop2ns0r.cn 7月初二是什么星座hcv7jop9ns9r.cn 上钟什么意思shenchushe.com 婴儿喝什么牌奶粉好hcv8jop4ns8r.cn
压寨夫人是什么意思0735v.com 南北杏和什么煲汤止咳化痰imcecn.com 虾皮是什么虾96micro.com 浮瓜沉李什么意思hcv8jop0ns3r.cn 196是什么意思hcv9jop0ns0r.cn
后背有痣代表什么hcv9jop2ns1r.cn hm是什么hcv8jop0ns7r.cn 十字架代表什么意思hcv8jop0ns1r.cn 缺钾是什么症状hcv7jop6ns7r.cn 红牛加什么提高性功能hcv9jop4ns4r.cn
容貌是什么意思hcv7jop5ns3r.cn 血管炎不能吃什么食物hcv8jop3ns1r.cn 一个日一个处一个口念什么hcv8jop2ns3r.cn 高密度脂蛋白胆固醇偏低什么意思hcv9jop0ns5r.cn 别开生面什么意思hcv9jop1ns6r.cn
百度