需求:

需要将一些敏感信息保存在JWT中,以便提高业务处理效率。

众所周知JWT协议RFC7519使用Base64Url对Header和Payload的Json字符串进行编解码。A JWT is represented as a sequence of URL-safe parts separated by period ('.') characters. Each part contains a base64url-encoded value.

不幸地的是Base64Url编码方式不是加密手段。任何得到JWT的人都可以使用公开的解码方式将JWT的原文解析出来。所以很多介绍JWT的文章都提醒不要在JWT中携带敏感信息。但是在特定业务场景下,如果JWT中含有某些关键信息,就可以节省后台很多额外操作,例如数据库查询、服务接口访问等。进而缩短后台响应时间,改善用户体验。

既然在JWT中携带敏感信息能带来这么大的好处,那么花点精力实现这个功能看起来是值得的。提到敏感信息的保密,自然会想到加密和解密。将敏感信息的密文放入JWT中,即使JWT泄露,由于没有密钥,获得JWT的人也无法对其进行解密。而服务器端只要增加一个解密过程就能提取出敏感信息,提高后续业务处理效率。

加密算法主要分两大类:对称加密和非对称加密。因为JWT是由服务端创建,客户端转手后又发回服务端使用。所以加密和解密都发生在服务端,不涉及到密钥的分发,相较其他加密场景要简单很多。所以我选择了加解密运算速度快的对称加密算法AES作为敏感信息的加密方式。

项目中使用JJWT java库创建和校验JWT,将AES加解密过程放入对JJWT封装的接口中。复用创建JWT的数字签名密钥作为AES的加密密钥和初始向量。加解密过程对调用者是透明的。

Maven依赖:

        <!--Java JWT 依赖库-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>

JJWT封装接口:

import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm; import javax.crypto.spec.SecretKeySpec;
import javax.xml.bind.DatatypeConverter;
import java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.Map; public class JWTUtil {
/**
* 生成JWT签名字符串
*
* @param publicClaims 无需加密保持明文方式的JWT claims
* @param privateClaims: 需要加密的JWT claims
* @param ttlMillis: JWT过期时长(毫秒)
* @param key: JWT HS256签名的密钥,也是AES加密的密钥
*
* @return JWT字符串
*
*/
public static String createJWT(Map<String, Object> publicClaims, Map<String, Object> privateClaims,long ttlMillis, String key) { SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
long nowMillis = System.currentTimeMillis(); byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(key);
Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName()); if(null != privateClaims && !privateClaims.isEmpty()) {
String jsonStr = JsonUtil.map2JsonStr(privateClaims);
//使用同一个密钥对私有声明进行加密
String encrypteClaims = AesEncryptUtil.encrypt(jsonStr, key, key);
if(null != encrypteClaims) {
if(null == publicClaims) {
publicClaims = new HashMap<>();
}
publicClaims.put("privateClaims", encrypteClaims);
}
} JwtBuilder builder = Jwts.builder().signWith(signatureAlgorithm, signingKey);
if(null != publicClaims && publicClaims.size() > 0) {
builder.setClaims(publicClaims);
} if (ttlMillis >= 0) {
long expMillis = nowMillis + ttlMillis;
Date exp = new Date(expMillis);
builder.setExpiration(exp);
} return builder.compact();
} /**
* 解析并校验JWT, 校验过程是JJWT内部实现的,会校验JWT是否过期,是否被篡改。
*
* @param jwt JWT字符串
* @param key: JWT HS256签名的密钥,也是AES加密的密钥
*
* @return JWT claims的Map对象
*
*/
public static Map<String, Object> parseJWT(String jwt, String key){ Map<String, Object> privateMap = null; //parser函数会在参数缺失、校验失败、token过期等情况下抛出runtime异常,所以调用者需要捕获该runtime异常
Map<String, Object> originalMap = Jwts.parser()
.setSigningKey(DatatypeConverter.parseBase64Binary(key))
.parseClaimsJws(jwt).getBody(); //解析私有声明
if(null != originalMap && originalMap.containsKey("privateClaims")) {
String encryptedStr = (String)originalMap.get("privateClaims");
if(null != encryptedStr && !encryptedStr.isEmpty()) {
String decryptedStr = AesEncryptUtil.decrypt(encryptedStr, key, key);
if(null != decryptedStr && !decryptedStr.isEmpty()) {
privateMap = JsonUtil.jsonStr2Map(decryptedStr);
}
} originalMap.remove("privateClaims");
if(null != privateMap && privateMap.size() > 0) {
originalMap.putAll(privateMap);
}
} return originalMap;
}
}

AES加密和解密接口:

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.UnsupportedEncodingException; public class AesEncryptUtil {
private static final String encode = "UTF-8";
private static final String mode = "AES/CBC/PKCS5Padding"; /**
* JDK只支持AES-128加密,也就是密钥长度必须是128bit;参数为密钥key,key的长度小于16字符时用"0"补充,key长度大于16字符时截取前16位
**/
private static SecretKeySpec get128BitsKey(String key) {
if (key == null) {
key = "";
}
byte[] data = null;
StringBuffer buffer = new StringBuffer(16);
buffer.append(key);
//小于16后面补0
while (buffer.length() < 16) {
buffer.append("0");
}
//大于16,截取前16个字符
if (buffer.length() > 16) {
buffer.setLength(16);
}
try {
data = buffer.toString().getBytes(encode);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return new SecretKeySpec(data, "AES");
} /**
* 创建128位的偏移量,iv的长度小于16时后面补0,大于16,截取前16个字符;
*
* @param iv
* @return
*/
private static IvParameterSpec get128BitsIV(String iv) {
if (iv == null) {
iv = "";
}
byte[] data = null;
StringBuffer buffer = new StringBuffer(16);
buffer.append(iv);
while (buffer.length() < 16) {
buffer.append("0");
}
if (buffer.length() > 16) {
buffer.setLength(16);
}
try {
data = buffer.toString().getBytes(encode);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return new IvParameterSpec(data);
} /**
* 填充方式为Pkcs5Padding的加密函数
* 填充方式为Pkcs5Padding时,最后一个块需要填充χ个字节,填充的值就是χ,也就是填充内容由JDK确定
*
* @param srcContent: 明文
* @param password: 加密密钥(不足128bits时,填"0"补足)
* @param iv: 初始向量(不足128bits时,填"0"补足)
*
* @return 密文(16进制表示)
*
*/
public static String encrypt(String srcContent, String password, String iv) {
SecretKeySpec key = get128BitsKey(password);
IvParameterSpec ivParameterSpec = get128BitsIV(iv);
try {
Cipher cipher = Cipher.getInstance(mode);
cipher.init(Cipher.ENCRYPT_MODE, key, ivParameterSpec);
byte[] byteContent = srcContent.getBytes(encode);
byte[] encryptedContent = cipher.doFinal(byteContent);
String result = HexUtil.byte2HexStr(encryptedContent);
return result;
} catch (Exception e) {
e.printStackTrace();
}
return null;
} /**
* 填充方式为Pkcs5Padding的解密函数
* 填充方式为Pkcs5Padding时,最后一个块需要填充χ个字节,填充的值就是χ,也就是填充内容由JDK确定
*
* @param encryptedContent: 密文
* @param password: 加密密钥(不足128bits时,填"0"补足)
* @param iv: 初始向量(不足128bits时,填"0"补足)
*
* @return 密文(16进制表示)
*
*/
public static String decrypt(String encryptedContent, String password, String iv) {
SecretKeySpec key = get128BitsKey(password);
IvParameterSpec ivParameterSpec = get128BitsIV(iv);
try {
byte[] content = HexUtil.hexStr2Byte(encryptedContent);
Cipher cipher = Cipher.getInstance(mode);
cipher.init(Cipher.DECRYPT_MODE, key, ivParameterSpec);
byte[] decryptedContent = cipher.doFinal(content);
String result = new String(decryptedContent);
return result;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}

WTUtil的创建接口“createJWT()”允许调用者指定那些JWT的声明需要加密,那些声明保持为明文的状态。例如JWT的过期时间“exp”,客户端可能需要这个值进行Token有效性判断,所以该claim就需要保持为明文。在调用者解析JWT的时候,JWTUtil的解析接口会将公开明文的publicClaims和解密后的privateClaims放到一个Map中,所以publicClaims和privateClaims Map的key值不要有重名的现象。相信实际的应用场景也不需要重名的Claims。

将以下3个claims生成JWT:

{
"groupId": "6fd5a193016d",
"userName": "test123",
"exp": 1528771799
}

未使用AES加密的JWT字符串:

eyJhbGciOiJIUzI1NiJ9.eyJncm91cElkIjoiNmZkNWExOTMwMTZkIiwidXNlck5hbWUiOiJ0ZXN0MTIzIiwiZXhwIjoxNTI4NzcxNzk5fQ.vJQ4CeYKx3n6B709w35Xdv4fVB2YTr-tsAWr3tCe6A8

使用https://jwt.io/#debugger解析后的原文:

将“groupId”和“userName”作为敏感信息使用AES加密后的JWT字符串:

eyJhbGciOiJIUzI1NiJ9.eyJwcml2YXRlQ2xhaW1zIjoiMUYyOTMxNjM2NzlBMDM2NDI5RkE3NzMwRTc2OUQyQUY0NjdEMkM3M0Y1NDQxNEExMTVCQUI4MzdCQTEwODQ2NjU0QjA2MTE0OTEzQkJGOUNDMkRCQjdFQzM0RTc2NjIwIiwiZXhwIjoxNTI4Nzc5NjAzfQ.5xb_uxBHMAPvShsOC-pQIS746OjW5XMjj5tAcxwFCq8

使用https://jwt.io/#debugger解析后的JWT原文:

使用“JWTUtil.parseJWT()”接口解析后的JWT原文Map对象:

{groupId=6fd5a193016d, userName=test123, exp=1528771799}

可以看到AES加密后,“groupId”和“userName”变成了JWT中“privateClaims”对应的密文。这样敏感信息就不怕泄露了。这个方法的缺点主要就是JWT字符串的长度从151增加到了243。如果JWT长度增加的太多,JJWT的接口还可以使用压缩算法对JWT字符串进行压缩。

spring cloud实战与思考(五) JWT之携带敏感信息的更多相关文章

  1. spring cloud实战与思考(四) JWT之Token主动失效

    需求: JWT泄露.密码重置等场景下,需要将未过期但是已经不安全的JWT主动失效. 本文不再复述JWT的基础知识,不了解的小伙伴可以自行Google一下.这里主要是针对以上需求聊一聊解决方案.如果服务 ...

  2. spring cloud实战与思考(二) 微服务之间通过fiegn上传一组文件(上)

    需求场景: 微服务之间调用接口一次性上传多个文件. 上传文件的同时附带其他参数. 多个文件能有效的区分开,以便进行不同处理. Spring cloud的微服务之间接口调用使用Feign.原装的Feig ...

  3. spring cloud实战与思考(一) spring config全局配置方案设计

    “spring cloud”的配置中心工具“spring cloud config”提供了分布式系统配置文件集中管理解决方案.该工具功能强大,实现也很简单.网上可以搜索到很多开发教程和用例.本文并不是 ...

  4. spring cloud实战与思考(三) 微服务之间通过fiegn上传一组文件(下)

    需求场景: 用户调用微服务1的接口上传一组图片和对应的描述信息.微服务1处理后,再将这组图片上传给微服务2进行处理.各个微服务能区分开不同的图片进行不同处理. 上一篇博客已经讨论了在微服务之间传递一组 ...

  5. [Spring Cloud实战 | 第六篇:Spring Cloud Gateway+Spring Security OAuth2+JWT实现微服务统一认证授权

    一. 前言 本篇实战案例基于 youlai-mall 项目.项目使用的是当前主流和最新版本的技术和解决方案,自己不会太多华丽的言辞去描述,只希望能勾起大家对编程的一点喜欢.所以有兴趣的朋友可以进入 g ...

  6. Spring Cloud实战 | 最终篇:Spring Cloud Gateway+Spring Security OAuth2集成统一认证授权平台下实现注销使JWT失效方案

    一. 前言 在上一篇文章介绍 youlai-mall 项目中,通过整合Spring Cloud Gateway.Spring Security OAuth2.JWT等技术实现了微服务下统一认证授权平台 ...

  7. Spring Cloud实战 | 最八篇:Spring Cloud +Spring Security OAuth2+ Axios前后端分离模式下无感刷新实现JWT续期

    一. 前言 记得上一篇Spring Cloud的文章关于如何使JWT失效进行了理论结合代码实践的说明,想当然的以为那篇会是基于Spring Cloud统一认证架构系列的最终篇.但关于JWT另外还有一个 ...

  8. Spring Cloud实战之初级入门(五)— 配置中心服务化与配置实时刷新

    目录 1.环境介绍 2.配置中心服务化 2.1 改造mirco-service-spring-config 2.2 改造mirco-service-provider.mirco-service-con ...

  9. Spring Cloud实战 | 第九篇:Spring Cloud整合Spring Security OAuth2认证服务器统一认证自定义异常处理

    本文完整代码下载点击 一. 前言 相信了解过我或者看过我之前的系列文章应该多少知道点我写这些文章包括创建 有来商城youlai-mall 这个项目的目的,想给那些真的想提升自己或者迷茫的人(包括自己- ...

随机推荐

  1. JavaScript高级程序设计学习(四)之引用类型

    在javascript中也是有引用类型的,java同样如此. javascript常见也比较常用的引用类型就熟Object和Array. 一个对象和一个数组,这个在前后端分离开发中也用的最多.比如aj ...

  2. [转]Qt 之 QFileSystemWatcher

    简述 QFileSystemWatcher类用于提供监视文件和目录修改的接口. QFileSystemWatcher通过监控指定路径的列表,监视文件系统中文件和目录的变更. 调用addPath()函数 ...

  3. C# 语法二 值类型引用类型

    1.值类型 2.引用类型 一 值类型 值类型存放在栈中,引用类型存放在堆中. 值类型有:数值.布尔.字符,例如:int i;bool i2;char cr='a'; 二 引用类型 大多数类型是引用类型 ...

  4. java中使用阻塞队列实现生产这与消费这之间的关系

    需求如下: 有一个生产者和一个消费者,生产者不断的生产产品,消费这不断的消费产品.产品总数为N. 1.生产顺序按队列的方式,先进先出. 2.生产者和消费这可以同时进行. 3.当生产者生产了N个产品后不 ...

  5. Android TimeAnimator && TimeListener翻译

    TimeAnimator:提供了一个简单的回调机制,通过 TimeAnimator.TimeListener,在动画的每一帧处通知你.这个动画器没有时间,插值或是对象值设定.回调监听器为每一帧动画接受 ...

  6. linux中断源码分析 - 初始化(二)

    本文为原创,转载请注明:http://www.cnblogs.com/tolimit/ 本篇文章主要讲述源码中是如何对中断进行一系列的初始化的. 回顾 在上一篇概述中,介绍了几个对于中断来说非常重要的 ...

  7. 开放的dae模型

    从网上看到了这段代码,就Copy过来了. 其实面对dae这种开放的模型格式,我们可以做很多事情,就像通常的XML文件一样. //------------------------------------ ...

  8. React-JSX简介

    JSX 本身其实也是一种表达式 在编译之后呢,JSX 其实会被转化为普通的 JavaScript 对象.这也就意味着,你其实可以在 if 或者 for 语句里使用 JSX,将它赋值给变量,当作参数传入 ...

  9. LiveCharts文档-3开始-8自定义工具提示

    原文:LiveCharts文档-3开始-8自定义工具提示 LiveCharts文档-3开始-8自定义工具提示 默认每个需要tooltip或者legend的chart都会初始化一个DefaultLeng ...

  10. NOI Day2线上同步赛崩盘记

    Preface 蒟蒻愉快的NOI线上赛Day2之行,不过因为太菜就凉了 这次由于策略&&网络的问题,最后两题都没有交,结果就靠T1稳住拿了75分就回家了. 我真是太菜了. 屠龙勇士 首 ...