jwt java_JWT(二):使用 Java 实现 JWT

介绍

原理在上篇《JWT(一):认识 JSON Web Token》已经说过了,实现起来并不难,你可以自己写一个 jwt 工具类(如果你有兴趣的话)

当然了,重复造轮子不是程序员的风格,我们主张拿来主义!

3a5a3afcb355e89693c6a063fd4fe9a3.png

JWT 官网提供了多种语言的 JWT 库,详情可以参考 https://jwt.io/#debugger 页面下半部分

jjwt 版本 0.10.7,它和 0.9.x 有很大的区别,一定要注意!!!

本文分5部分

第1部分:以简单例子演示生成、验证、解析 jwt 过程

第2部分:介绍 jjwt 的常用方法

第3部分:封装一个常用的 jwt 工具类

如果只是拿来主义,看到这里就可以了

第4部分:介绍 jjwt 的各种签名算法

第5部分:对 jwt 进行安全加密

简单例子

引入 MAVN 依赖

io.jsonwebtoken

jjwt-api

0.10.7

io.jsonwebtoken

jjwt-impl

0.10.7

runtime

io.jsonwebtoken

jjwt-jackson

0.10.7

runtime

一个例子

// 生成密钥

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 header): JwtBuilder

setHeaderParams(Map params): JwtBuilder

setHeaderParam(String name, Object value): JwtBuilder

参数 Header 对象 可通过 Jwts.header(); 创建,它简单得就像一个 map (把它当做 map 使用即可)

Payload

至少设置一个 claims,否则在生成签名时会抛出异常

setClaims(Claims claims): JwtBuilder

setClaims(Map claims): JwtBuilder

addClaims(Map 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 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 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 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 map = new HashMap();

//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 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,帮助你更好的构建权限系统

首先在 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验证失败");

}

安全加密

敬请期待 .....

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值