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-256
  • HS384: HMAC using SHA-384
  • HS512: HMAC using SHA-512
  • ES256: ECDSA using P-256 and SHA-256
  • ES384: ECDSA using P-384 and SHA-384
  • ES512: ECDSA using P-521 and SHA-512
  • RS256: RSASSA-PKCS-v1_5 using SHA-256
  • RS384: RSASSA-PKCS-v1_5 using SHA-384
  • RS512: RSASSA-PKCS-v1_5 using SHA-512
  • PS256: RSASSA-PSS using SHA-256 and MGF1 with SHA-256
  • PS384: RSASSA-PSS using SHA-384 and MGF1 with SHA-384
  • PS512: 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,帮助你更好的构建权限系统

  1. 首先在 Header(或 claims)中设置一个 keyId

  2. 定义一个类,继承 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());
    }
    }
  3. 解析时,不再调用 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的更多相关文章

  1. Java验证jwt token

    https://jwt.io/ RS256加密JWT生成.验证 https://blog.csdn.net/u011411069/article/details/79966226 How to loa ...

  2. 二.3.token认证,jwt认证,前端框架

    一.token: 铺垫: 之前用的是通过最基本的用户名密码登录我的运维平台http://127.0.0.1:8000/---这种用的是form表单,但是这种对于前后端分离的不适合.前后端分离,应该通过 ...

  3. drf认证组件、权限组件、jwt认证、签发、jwt框架使用

    目录 一.注册接口 urls.py views.py serializers.py 二.登录接口 三.用户中心接口(权限校验) urls.py views.py serializers.py 四.图书 ...

  4. drf认证组件(介绍)、权限组件(介绍)、jwt认证、签发、jwt框架使用

    目录 一.注册接口 urls.py views.py serializers.py 二.登录接口 三.用户中心接口(权限校验) urls.py views.py serializers.py 四.图书 ...

  5. 20145212《Java程序设计》实验报告二 《 Java面向对象程序设计》

    20145212 实验二< Java面向对象程序设计> 实验内容 单元测试 三种代码 伪代码 百分制转五分制: 如果成绩小于60,转成"不及格" 如果成绩在60与70之 ...

  6. 20145221 《Java程序设计》实验报告二:Java面向对象程序设计

    20145221 <Java程序设计>实验报告二:Java面向对象程序设计 实验要求 初步掌握单元测试和TDD 理解并掌握面向对象三要素:封装.继承.多态 初步掌握UML建模 熟悉S.O. ...

  7. Java实验报告二:Java面向对象程序设计

    Java实验报告二:Java面向对象程序设计                                                                               ...

  8. 错题集锦(二) -- Java专项

    错题集锦(二) -- Java专项 标签(空格分隔): 找工作 JVM的内存模型 线程共享: 堆(Heap):主要存放一些对象实例 方法区(Method Area / Non-Heap):用于存储已被 ...

  9. Java语言基础(二) Java关键字

    Java语言基础(二) Java关键字 Java关键字比较多,我就不列举出来了,只记录一些常用的小知识点: ①Java的关键字只有小写. ②then.sizeof都不是Java的关键字,熟悉C++的程 ...

随机推荐

  1. H3C CSMA/CD冲突检测和退避

  2. Python--day21--复习

    序列化模块总结: jison格式化输出: Serialize obj to a JSON formatted str.(字符串表示的json对象) Skipkeys:默认值是False,如果dict的 ...

  3. Python--day62--Django安装,配置,web请求流程,views.py总结

    1,安装Django 2,创建Django项目: 3,配置Django项目 1.settinngs.py文件 1.templates文件夹的位置 2.静态文件 1,STATIC_URL   ----- ...

  4. P1061 最长连号

    题目描述 输入n个正整数,(1<=n<=10000),要求输出最长的连号的长度.(连号指从小到大连续自然数) 输入格式 第一行,一个数n; 第二行,n个正整数,之间用空格隔开. 输出格式 ...

  5. H3C 路由优先级

  6. linux 原子变量

    有时, 一个共享资源是一个简单的整数值. 假设你的驱动维护一个共享变量 n_op, 它告 知有多少设备操作目前未完成. 正常地, 即便一个简单的操作例如: n_op++; 可能需要加锁. 某些处理器可 ...

  7. 11-28\enum

    1.创建一个枚举对象,对象中4个属性video视频.book书----(这2个属性可以用数字表示). 2.创建一个class对象,对象中有2个属性,一个是id属性(自己设置),第二个属性是type类型 ...

  8. 关于POSTMAN做并发压测

    一开始我个人在做测试时用到了POSTMAN,用了两种方式做测试, 第一种: 测试发现这种方式是阻塞排队,我让接口睡两秒,这100次请求间隔就是2秒,是串行执行 于是想到第二种,在一个collectio ...

  9. linux一个进程如何睡眠

    如果我们深入 <linux/wait.h>, 你见到在 wait_queue_head_t 类型后面的数据结构是非 常简单的; 它包含一个自旋锁和一个链表. 这个链表是一个等待队列入口, ...

  10. css页面去除滚动条

    注:滚动条是导致页面切换标签页闪动的原因 html{ /*隐藏滚动条,当IE下溢出,仍然可以滚动*/ -ms-overflow-style:none; /*火狐下隐藏滚动条*/ overflow:-m ...