JWT(二):使用 Java 实现 JWT
JWT(一):认识 JSON WebToken
JWT(二):使用 Java 实现 JWT
介绍
原理在上篇《JWT(一):认识 JSON Web Token》已经说过了,实现起来并不难,你可以自己写一个 jwt 工具类(如果你有兴趣的话)
当然了,重复造轮子不是程序员的风格,我们主张拿来主义!
JWT 官网提供了多种语言的 JWT 库,详情可以参考 https://jwt.io/#debugger 页面下半部分
建议使用 jjwt库 ,它的github地址 https://github.com/jwtk/jjwt
jjwt 版本 0.10.7,它和 0.9.x 有很大的区别,一定要注意!!!
本文分5部分
- 第1部分:以简单例子演示生成、验证、解析 jwt 过程
- 第2部分:介绍 jjwt 的常用方法
- 第3部分:封装一个常用的 jwt 工具类
如果只是拿来主义,看到这里就可以了 - 第4部分:介绍 jjwt 的各种签名算法
- 第5部分:对 jwt 进行安全加密
简单例子
引入 MAVN 依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.10.7</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.10.7</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.10.7</version>
<scope>runtime</scope>
</dependency>
一个例子
// 生成密钥
String key = "0123456789_0123456789_0123456789";
SecretKey secretKey = new SecretKeySpec(key.getBytes(), SignatureAlgorithm.HS256.getJcaName());
// 1. 生成 token
String token = Jwts.builder() // 创建 JWT 对象
.setSubject("JSON Web Token") // 设置主题(声明信息)
.signWith(secretKey) // 设置安全密钥(生成签名所需的密钥和算法)
.compact(); // 生成token(1.编码 Header 和 Payload 2.生成签名 3.拼接字符串)
System.out.println(token);
//token = token + "s";
// 2. 验证token,如果验证token失败则会抛出异常
try {
Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(token);
// OK, we can trust this token
System.out.println("验证成功");
} catch (JwtException e) {
//don't trust the token!
System.out.println("验证失败");
}
// 3. 解析token
Claims body = Jwts.parser() // 创建解析对象
.setSigningKey(secretKey) // 设置安全密钥(生成签名所需的密钥和算法)
.parseClaimsJws(token) // 解析token
.getBody(); // 获取 payload 部分内容
System.out.println(body);
输出结果:
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJKU09OIFdlYiBUb2tlbiJ9.QwmY_0qXW4BhAHcDpxz62v3xqkFYbg5lsZQhM2t-kVs
验证成功
{sub=JSON Web Token}
常用方法
以下内容建议参考源码获知更多详情
Jwts.builder() 创建了 DefaultJwtBuilder 对象,该对象的常用方法如下:
Header
在 compact()
方法中会自动根据签名算法设置头部信息,当然也可以手动设置
- setHeader(Header header): JwtBuilder
- setHeader(Map<String, Object> header): JwtBuilder
- setHeaderParams(Map<String, Object> params): JwtBuilder
- setHeaderParam(String name, Object value): JwtBuilder
参数 Header 对象 可通过 Jwts.header(); 创建,它简单得就像一个 map (把它当做 map 使用即可)
Payload
至少设置一个 claims,否则在生成签名时会抛出异常
- setClaims(Claims claims): JwtBuilder
- setClaims(Map<String, Object> claims): JwtBuilder
- addClaims(Map<String, Object> claims): JwtBuilder
- setIssuer(String iss): JwtBuilder
- setSubject(String sub): JwtBuilder
- setAudience(String aud): JwtBuilder
- setExpiration(Date exp): JwtBuilder
- setNotBefore(Date nbf): JwtBuilder
- setIssuedAt(Date iat): JwtBuilder
- setId(String jti): JwtBuilder
- claim(String name, Object value: JwtBuilder
参数对象 Claims 同 Header 类似,通过 Jwts.claims() 创建,同样简单得就像一个 map
值得注意的一点是:不要在 setXxx 之后调用 setClaims(Claims claims) 或 setClaims(Map<String, Object> claims),因为这两个方法会覆盖所有已设置的 claim
Signature
- signWith(Key key)
- signWith(Key key, SignatureAlgorithm alg)
- signWith(SignatureAlgorithm alg, byte[] secretKeyBytes)
- signWith(SignatureAlgorithm alg, String base64EncodedSecretKey)
- signWith(SignatureAlgorithm alg, Key key)
以上方法最终就是设置两个对象:key 和 algorithm,分别代表密钥和算法
方法内部生成密钥使用的方法的和演示中的一样
SecretKey key = new SecretKeySpec(secretKeyBytes, alg.getJcaName());
注意:key 的长度必须符合签名算法的要求(避免生成弱密钥)
HS256:bit 长度要>=256,即字节长度>=32
HS384:bit 长度要>=384,即字节长度>=48
HS512:bit 长度要>=512,即字节长度>=64
在 secret key algorithms 名称中的数字代表了最小bit长度
更多签名算法的详情,请参考签名算法小节
封装 JWT 工具类
package com.liuchuanv.jwt;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.SignatureException;
import javax.crypto.spec.SecretKeySpec;
import java.security.Key;
import java.util.Date;
import java.util.Map;
import java.util.UUID;
/**
* JSON Web Token 工具类
*
* @author LiuChuanWei
* @date 2019-12-11
*/
public class JwtUtils {
/**
* key(按照签名算法的字节长度设置key)
*/
private final static String SECRET_KEY = "0123456789_0123456789_0123456789";
/**
* 过期时间(毫秒单位)
*/
private final static long TOKEN_EXPIRE_MILLIS = 1000 * 60 * 60;
/**
* 创建token
* @param claimMap
* @return
*/
public static String createToken(Map<String, Object> claimMap) {
long currentTimeMillis = System.currentTimeMillis();
return Jwts.builder()
.setId(UUID.randomUUID().toString())
.setIssuedAt(new Date(currentTimeMillis)) // 设置签发时间
.setExpiration(new Date(currentTimeMillis + TOKEN_EXPIRE_MILLIS)) // 设置过期时间
.addClaims(claimMap)
.signWith(generateKey())
.compact();
}
/**
* 验证token
* @param token
* @return 0 验证成功,1、2、3、4、5 验证失败
*/
public static int verifyToken(String token) {
try {
Jwts.parser().setSigningKey(generateKey()).parseClaimsJws(token);
return 0;
} catch (ExpiredJwtException e) {
e.printStackTrace();
return 1;
} catch (UnsupportedJwtException e) {
e.printStackTrace();
return 2;
} catch (MalformedJwtException e) {
e.printStackTrace();
return 3;
} catch (SignatureException e) {
e.printStackTrace();
return 4;
} catch (IllegalArgumentException e) {
e.printStackTrace();
return 5;
}
}
/**
* 解析token
* @param token
* @return
*/
public static Map<String, Object> parseToken(String token) {
return Jwts.parser() // 得到DefaultJwtParser
.setSigningKey(generateKey()) // 设置签名密钥
.parseClaimsJws(token)
.getBody();
}
/**
* 生成安全密钥
* @return
*/
public static Key generateKey() {
return new SecretKeySpec(SECRET_KEY.getBytes(), SignatureAlgorithm.HS256.getJcaName());
}
}
测试代码如下:
//Map<String, Object> map = new HashMap<String, Object>();
//map.put("userId", 1002);
//map.put("userName", "张晓明");
//map.put("age", 12);
//map.put("address", "山东省青岛市李沧区");
//String token = JwtUtils.createToken(map);
//System.out.println(token);
String token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI0ZWM2NWNhNC0wZjVmLTRlOTktOTI5NS1mYWUyN2UwODIzYzQiLCJpYXQiOjE1NzY0OTI4NjYsImV4cCI6MTU3NjQ5NjQ2NiwiYWRkcmVzcyI6IuWxseS4nOecgemdkuWym-W4guadjuayp-WMuiIsInVzZXJOYW1lIjoi5byg5pmT5piOIiwidXNlcklkIjoxMDAyLCJhZ2UiOjEyfQ.6Z18aIA6y52ntQkV3BwlYiVK3hL3R2WFujjTmuvimww";
int result = JwtUtils.verifyToken(token);
System.out.println(result);
Map<String, Object> map = JwtUtils.parseToken(token);
System.out.println(map);
输出结果:
0
{jti=4ec65ca4-0f5f-4e99-9295-fae27e0823c4, iat=1576492866, exp=1576496466, address=山东省青岛市李沧区, userName=张晓明, userId=1002, age=12}
签名算法
12 种签名算法
JWT 规范定义了12种标准签名算法:3种 secret key 算法和9种非对称密钥算法
HS256
: HMAC using SHA-256HS384
: HMAC using SHA-384HS512
: HMAC using SHA-512ES256
: ECDSA using P-256 and SHA-256ES384
: ECDSA using P-384 and SHA-384ES512
: ECDSA using P-521 and SHA-512RS256
: RSASSA-PKCS-v1_5 using SHA-256RS384
: RSASSA-PKCS-v1_5 using SHA-384RS512
: RSASSA-PKCS-v1_5 using SHA-512PS256
: RSASSA-PSS using SHA-256 and MGF1 with SHA-256PS384
: RSASSA-PSS using SHA-384 and MGF1 with SHA-384PS512
: RSASSA-PSS using SHA-512 and MGF1 with SHA-512
根据算法名称可分为四类:HSxxx(secret key 算法)、ESxxx、RSxxx、PSxxx
HSxxx、ESxxx 中的 xxx 表示算法 key 最小 Bit 长度
RSxxx、PSxxx 中的 xxx 表示算法 key 最小 Byte 长度
规定key的最小长度是为了避免因 key 过短生成弱密钥
生成密钥
jjwt 生成 secret key 两种方法
String key = "1234567890_1234567890_1234567890";
// 1. 根据key生成密钥(会根据字节参数长度自动选择相应的 HMAC 算法)
SecretKey secretKey1 = Keys.hmacShaKeyFor(key.getBytes());
// 2. 根据随机数生成密钥
SecretKey secretKey2 = Keys.secretKeyFor(SignatureAlgorithm.HS256);
- 方法 Keys.hmacShaKeyFor(byte[]) 内部也是 new SecretKeySpec(bytes, alg.getJcaName()) 来生成密钥的
- 方法 Keys.secretKeyFor(SignatureAlgorithm) 内部使用 KeyGenerator.generateKey() 生成密钥
jjwt 也提供了非对称密钥对的生成方法
// 1. 使用jjwt提供的方法生成
KeyPair keyPair = Keys.keyPairFor(SignatureAlgorithm.RS256); //or RS384, RS512, PS256, PS384, PS512, ES256, ES384, ES512
// 2. 手动生成
int keySize = 1024;
// RSA算法要求有一个可信任的随机数源
SecureRandom secureRandom = new SecureRandom();
// 为RSA算法创建一个KeyPairGenerator对象
KeyPairGenerator keyPairGenerator = null;
try {
keyPairGenerator = KeyPairGenerator.getInstance("RSA");
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
// 利用上面的随机数据源初始化这个KeyPairGenerator对象
keyPairGenerator.initialize(keySize, secureRandom);
// 生成密钥对
KeyPair keyPair2 = keyPairGenerator.generateKeyPair();
- Keys.keyPairFor(SignatureAlgorithm) 会根据算法自动生成相应长度的
- signWith(secretKey) 会根据密钥长度自动选择相应算法,也可以指定任意算法(指定的算法不受密钥长度限制,可任意选择,即用 RS256生成的密钥,可以 signWith(secretKey, SignatureAlgorithm.RS512),但是 JJWT 并不建议这么做)
- 在加密时使用 keyPair.getPrivate() ,解密时使用 keyPair.getPublic()
不同密钥生成token
以上都是使用同一密钥签名生成所有的token,下面我们使用不同的密钥
这一个特性可以应用于不同用户/角色使用不同的密钥生成的 token,帮助你更好的构建权限系统
首先在 Header(或 claims)中设置一个 keyId
定义一个类,继承 SigningKeyResolverAdapter,并重写 resolveSigningKey() 或 resolveSigningKeyBytes() 方法
public class MySigningKeyResolver extends SigningKeyResolverAdapter {
@Override
public Key resolveSigningKey(JwsHeader header, Claims claims) {
// 除了从 header 中获取 keyId 外,也可以从 claims 中获取(前提是在 claims 中设置了 keyId 声明)
String keyId = header.getKeyId();
// 根据 keyId 查找相应的 key
Key key = lookupVerificationKey(keyId);
return key;
} public Key lookupVerificationKey(String keyId) {
// TODO 根据 keyId 获取 key,比如从数据库中获取
// 下面语句仅做演示用,绝对不可用于实际开发中!!!
String key = "qwertyuiopasdfghjklzxcvbnm2019_" + keyId;
return Keys.hmacShaKeyFor(key.getBytes());
}
}
解析时,不再调用 setSigningKey(SecretKey) ,而是调用 setSigningKeyResolver(SigningKeyResolver)
// 生成密钥
// TODO 此处 keyId 仅做演示用,实际开发中可以使用 UserId、RoleId 等作为 keyId
String keyId = new Long(System.currentTimeMillis()).toString();
System.out.println("keyId=" + keyId); String key = "qwertyuiopasdfghjklzxcvbnm2019_" + keyId;
SecretKey secretKey = new SecretKeySpec(key.getBytes(), SignatureAlgorithm.HS256.getJcaName()); // 1. 生成 token
String token = Jwts.builder()
.setHeaderParam(JwsHeader.KEY_ID, keyId) // 设置 keyId(当然也可以在 claims 中设置)
.setSubject("JSON Web Token")
.signWith(secretKey)
.compact();
System.out.println("token=" + token); // 2. 验证token
// token 使用了不同的密钥生成签名,在解析时就不用调用 setSigningKey(SecretKey) 了
// 而是调用 setSigningKeyResolver(SigningKeyResolver)
try {
Jwts.parser()
.setSigningKeyResolver(new MySigningKeyResolver())
.parseClaimsJws(token);
// OK, we can trust this token
System.out.println("token验证成功");
} catch (JwtException e) {
//don't trust the token!
System.out.println("token验证失败");
}
安全加密
敬请期待 .....
JWT(二):使用 Java 实现 JWT的更多相关文章
- Java验证jwt token
https://jwt.io/ RS256加密JWT生成.验证 https://blog.csdn.net/u011411069/article/details/79966226 How to loa ...
- 二.3.token认证,jwt认证,前端框架
一.token: 铺垫: 之前用的是通过最基本的用户名密码登录我的运维平台http://127.0.0.1:8000/---这种用的是form表单,但是这种对于前后端分离的不适合.前后端分离,应该通过 ...
- drf认证组件、权限组件、jwt认证、签发、jwt框架使用
目录 一.注册接口 urls.py views.py serializers.py 二.登录接口 三.用户中心接口(权限校验) urls.py views.py serializers.py 四.图书 ...
- drf认证组件(介绍)、权限组件(介绍)、jwt认证、签发、jwt框架使用
目录 一.注册接口 urls.py views.py serializers.py 二.登录接口 三.用户中心接口(权限校验) urls.py views.py serializers.py 四.图书 ...
- 20145212《Java程序设计》实验报告二 《 Java面向对象程序设计》
20145212 实验二< Java面向对象程序设计> 实验内容 单元测试 三种代码 伪代码 百分制转五分制: 如果成绩小于60,转成"不及格" 如果成绩在60与70之 ...
- 20145221 《Java程序设计》实验报告二:Java面向对象程序设计
20145221 <Java程序设计>实验报告二:Java面向对象程序设计 实验要求 初步掌握单元测试和TDD 理解并掌握面向对象三要素:封装.继承.多态 初步掌握UML建模 熟悉S.O. ...
- Java实验报告二:Java面向对象程序设计
Java实验报告二:Java面向对象程序设计 ...
- 错题集锦(二) -- Java专项
错题集锦(二) -- Java专项 标签(空格分隔): 找工作 JVM的内存模型 线程共享: 堆(Heap):主要存放一些对象实例 方法区(Method Area / Non-Heap):用于存储已被 ...
- Java语言基础(二) Java关键字
Java语言基础(二) Java关键字 Java关键字比较多,我就不列举出来了,只记录一些常用的小知识点: ①Java的关键字只有小写. ②then.sizeof都不是Java的关键字,熟悉C++的程 ...
随机推荐
- H3C ISDN BRI和PRI
- ip2long之后有什么好处?
ip2long需要bigint来存储,而且在32位和64位系统中存储方式还有区别: 而保存成字符串,只需要char20即可. 那么,ip2long好处在哪? 做投票项目的时候,将ip地址处理后用int ...
- 2016年NOIP普及组复赛题解
题目涉及算法: 买铅笔:入门题: 回文日期:枚举: 海港:双指针: 魔法阵:数学推理. 买铅笔 题目链接:https://www.luogu.org/problem/P1909 设至少要买 \(num ...
- Laravel 中config的用法
Laravel的config下一般存放配置信息,可以通过config('key')方法获取指定的数据. 设置值可通过「点」式语法读取,其中包含要访问的文件名以及选项名称. 现在想读取\config\a ...
- servicemix-3.2.1 内置的服务引擎和绑定组件
服务引擎: servicemix-bean servicemix-camel servicemix-cxf-se servicemix-drools servicemix-eip servicemix ...
- Vue 语法的一些小问题
设置 sty行内样式 :style="{width:mapWidth,height:mapHeight}" This指向 axios 使用axios 的时候 ,在生命周期函数 ...
- P1091 剧院广场
题目描述 柏林首都的剧院广场呈长方形,面积为 \(n \times m\) 平方米.在这座城市的周年纪念日之际,人们决定用方形花岗岩石板铺设广场.每块石板的大小都是 \(a \times a\) . ...
- Linux 基础(一)stat函数
Header file: #include <sys/types.h> #include <sys/stat.h> #include <unistd.h> DEFI ...
- H3CSTP、RSTP的问题
- linux ioctl 接口
大部分驱动需要 -- 除了读写设备的能力 -- 通过设备驱动进行各种硬件控制的能力. 大 部分设备可进行超出简单的数据传输之外的操作; 用户空间必须常常能够请求, 例如, 设 备锁上它的门, 弹出它的 ...