声明:这只是一个入门级的JWT示例教学,欢迎各位朋友踊跃发言,一起探讨进步。
声明:本文不介绍JWT相关概念,也不比较JWT的各个类库,对相关概念、JWT各个类库感兴趣的朋友可
自行查阅相关资料;本文直接演示示例使用入门级JWT。
提示:本人较懒,不想什么基本的东西都自己写,所以这里使用了JWT众多类库中的nimbus-jose-jwt类库。
本文中涉及到的JwtUtil工具类,其实是本人对nimbus-jose-jwt提供的基本功能的一个简单封装。
软硬件环境说明:Windows10、IntelliJ IDEA、SpringBoot 2.1.6.RELEASE。
本人为了及快速开发、为了示例,还引入了其他的一些不是关键的依赖文件。给出完整pom.xml文字版:
4.0.0
org.springframework.boot
spring-boot-starter-parent
2.1.6.RELEASE
com.szlzcl
jwt-token
0.0.1-SNAPSHOT
jwt-token
测试使用json web token
1.8
org.springframework.boot
spring-boot-starter-web
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-starter-test
test
com.nimbusds
nimbus-jose-jwt
7.1
com.alibaba
fastjson
1.2.56
org.springframework.boot
spring-boot-maven-plugin
JwtUtil:
import com.nimbusds.jose.*;
import com.nimbusds.jose.crypto.MACSigner;
import com.nimbusds.jose.crypto.MACVerifier;
import com.nimbusds.jose.crypto.RSASSASigner;
import com.nimbusds.jose.crypto.RSASSAVerifier;
import com.nimbusds.jose.jwk.RSAKey;
import java.text.ParseException;
/**
* 对nimbus-jose-jwt类库提供的基本功能 再进行工具类封装
*
* @author JustryDeng
* @date 2019/7/21 13:46
*/
@SuppressWarnings("unused")
public class JwtUtil {
/**
* 生成token -- 采用【对称加密】算法HS256验证签名
*
* 注:此方法生成的jwt的Header部分为默认的
* {
* "alg": "HS256",
* "typ": "JWT"
* }
*
* @param payloadJsonString
* 有效负载JSON字符串
*
* @param secret
* 对称加密/解密密钥
* 注意:secret.getBytes().length 必须 >=32
*
* @return token
* @throws JOSEException
* 以下情况会抛出此异常:
* 1、密钥长度 secret.getBytes().length < 32时,会抛出此异常
* 2、JWS已签名
* 3、JWS无法使用指定的签名器
* @date 2019/7/21 13:54
*/
public static String generateToken(String payloadJsonString, String secret)
throws JOSEException {
// 创建Header(设置以JWSAlgorithm.HS256算法进行签名认证)
JWSHeader jwsHeader = new JWSHeader(JWSAlgorithm.HS256);
// 建立Payload
Payload payload = new Payload(payloadJsonString);
/// Signature相关
// 根据Header以及Payload创建JSON Web Signature (JWS)对象
JWSObject jwsObject = new JWSObject(jwsHeader, payload);
// 使用给的对称加密密钥,创建一个加密器
JWSSigner jwsSigner = new MACSigner(secret);
// 将该签名器 与 JSON Web Signature (JWS)对象进行关联
// 即: 指定JWS使用该签名器进行签名
jwsObject.sign(jwsSigner);
// 使用JWS生成JWT(即:使用JWS生成token)
return jwsObject.serialize();
}
/**
* 校验token是否被篡改,并返回有效负载JSON字符串 -- 采用【对称加密】算法HS256验证签名
*
* @param secret
* 对称加密/解密密钥
* 注意:secret.getBytes().length 必须 >=32
*
* @return 有效负载JSON字符串
* @throws JOSEException,ParseException,JwtSignatureVerifyException 异常信息
* @date 2019/7/21 14:08
*/
public static String verifySignature(String token, String secret)
throws JOSEException, ParseException, JwtSignatureVerifyException {
// 解析token,将token转换为JWSObject对象
JWSObject jwsObject = JWSObject.parse(token);
// 创建一个JSON Web Signature (JWS) verifier.用于校验签名(即:校验token是否被篡改)
JWSVerifier jwsVerifier = new MACVerifier(secret);
// 如果校验到token被篡改(即:签名认证失败),那么抛出异常
if(!jwsObject.verify(jwsVerifier)) {
throw new JwtSignatureVerifyException("Signature verification result is fail!");
}
// 获取有效负载
Payload payload = jwsObject.getPayload();
// 返回 有效负载JSON字符串
return payload.toString();
}
/**
* 生成token -- 采用【非对称加密】算法RS256验证签名
*
* @param payloadJsonString
* 有效负载JSON字符串
*
* @param rsaKey
* 非对称加密密钥对
* 提示:RSAKey示例,可以这么获得
* RSAKeyGenerator rsaKeyGenerator = new RSAKeyGenerator(1024 * 3);
* RSAKey rsaKey = rsaKeyGenerator.generate();
*
* @return token
* @throws JOSEException 异常信息
* @date 2019/7/21 13:54
*/
public static String generateTokenByAsymmetric(String payloadJsonString, RSAKey rsaKey)
throws JOSEException {
// Header
JWSHeader jwsHeader = new JWSHeader.Builder(JWSAlgorithm.RS256)
.keyID(rsaKey.getKeyID())
.build();
// Payload
Payload payload= new Payload(payloadJsonString);
/// Signature相关
// 根据Header以及Payload创建JSON Web Signature (JWS)对象
JWSObject jwsObject = new JWSObject(jwsHeader, payload);
// 使用给的对称加密密钥,创建一个加密器
JWSSigner signer = new RSASSASigner(rsaKey);
// 将该签名器 与 JSON Web Signature (JWS)对象进行关联
// 即: 指定JWS使用该签名器进行签名
jwsObject.sign(signer);
// 使用JWS生成JWT(即:使用JWS生成token)
return jwsObject.serialize();
}
/**
* 校验token是否被篡改,并返回有效负载JSON字符串 -- 采用【非对称加密】算法RS256验证签名
*
* @param rsaKey
* 非对称加密密钥对
*
* @return 有效负载JSON字符串
* @throws JOSEException,ParseException,JwtSignatureVerifyException 异常信息
* @date 2019/7/21 14:08
*/
public static String verifySignatureByAsymmetric(String token, RSAKey rsaKey)
throws JOSEException, ParseException, JwtSignatureVerifyException {
// 根据token获得JSON Web Signature (JWS)对象
JWSObject jwsObject = JWSObject.parse(token);
// 获取到公钥
RSAKey publicRsaKey = rsaKey.toPublicJWK();
// 根据公钥 获取 Signature验证器
JWSVerifier jwsVerifier = new RSASSAVerifier(publicRsaKey);
// 如果校验到token被篡改(即:签名认证失败),那么抛出异常
if(!jwsObject.verify(jwsVerifier)) {
throw new JwtSignatureVerifyException("Signature verification result is fail!");
}
// 获取有效负载
Payload payload = jwsObject.getPayload();
// 返回 有效负载JSON字符串
return payload.toString();
}
// /**
// * main方法测试
// *
// * --------------------------------------下面的为测试代码--------------------------------------
// *
// */
// public static void main(String[] args)
// throws JOSEException, ParseException, JwtSignatureVerifyException {
// // 测试对称加密算法 验证签名的JWT
// testSymmetric();
// // 测试非对称加密算法 验证签名的JWT
// testAsymmetric();
// }
//
// /**
// * 测试对称加密算法的token生成与 签名检验
// */
// private static void testSymmetric()
// throws JOSEException, ParseException, JwtSignatureVerifyException {
// String secret = "adsgfiaughofashdofhjasodhfoasdafisd";
// String token = JwtUtil.generateToken("{\"name\":\"张三\"}", secret);
// System.out.println(token);
// String payloadJsonString = JwtUtil.verifySignature(token, secret);
// System.out.println(payloadJsonString);
// }
//
// /**
// * 测试对称加密算法的token生成与 签名检验
// */
// private static void testAsymmetric()
// throws JOSEException, ParseException, JwtSignatureVerifyException {
// RSAKeyGenerator rsaKeyGenerator = new RSAKeyGenerator(1024 * 3);
// RSAKey rsaKey = rsaKeyGenerator.generate();
// String token =
// JwtUtil.generateTokenByAsymmetric("{\"name\":\"张三\"}", rsaKey);
// System.out.println(token);
// String payloadJsonString = JwtUtil.verifySignatureByAsymmetric(token, rsaKey);
// System.out.println(payloadJsonString);
// }
}
JwtSignatureVerifyException:
/**
* 签名校验失败异常
* 即:token被篡改异常
*
* @author JustryDeng
* @date 2019/7/21 14:23
*/
@SuppressWarnings("unused")
public class JwtSignatureVerifyException extends Exception {
private static final long serialVersionUID = -861994790728930634L;
/**
* Creates a new JwtSignatureVerifyException with the specified message.
*
* @param message The exception message.
*/
public JwtSignatureVerifyException(String message) {
super(message);
}
/**
* Creates a new JwtSignatureVerifyException with the specified message and cause.
*
* @param message The exception message.
* @param cause The exception cause.
*/
public JwtSignatureVerifyException(String message, Throwable cause) {
super(message, cause);
}
}
PayloadDTO:
import lombok.*;
import java.io.Serializable;
/**
* JWT中间部分 有效负载 数据模型
*
* @author JustryDeng
* @date 2019/7/21 15:31
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class PayloadDTO implements Serializable {
private static final long serialVersionUID = 5988619597830511341L;
/**
* ------------------------------------这部分为 注册声明------------------------------------
* 这个jwt的身份id
*/
private String jti;
/** 签发人 */
private String iss;
/** 过期时长(单位ms) */
private Long exp;
/** 主题 */
private String sub;
/** 受众 */
private String aud;
/** 生效时间(1970年1月1日到现在的偏移量) */
private Long nbf;
/** 签发时间(1970年1月1日到现在的偏移量) */
private Long iat;
/**
* ------------------------------------这部分为 公开声明------------------------------------
* 姓名
*/
private String name;
/**
* 性别
*/
private String gender;
/**
* 出生日期
*/
private String birthday;
/**
* ------------------------------------这部分为 私有声明------------------------------------
* 是否是管理员
*/
private Boolean isAdmin;
}
application.properties:
# 验证签名的(对称加密)算法的密钥
my.jwt.signature.algorithm.secret=abcdefghijklmnopqrstuvwxyz123456789
# 单位ms
my.jwt.expiration.time=60000
DemoController:
import com.alibaba.fastjson.JSON;
import com.nimbusds.jose.JOSEException;
import com.szlzcl.jwttoken.model.PayloadDTO;
import com.szlzcl.jwttoken.util.JwtSignatureVerifyException;
import com.szlzcl.jwttoken.util.JwtUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.text.ParseException;
/**
* controller
*
* @author JustryDeng
* @date 2019/7/21 11:03
*/
@Slf4j
@RestController
public class DemoController {
@Value("${my.jwt.signature.algorithm.secret}")
private String signatureAlgorithmSecret;
@Value("${my.jwt.expiration.time}")
private Long tokenExpirationTime;
/**
* 用户登录, 并返回token信息
*
* 注:真正使用时,往往除了token,还会返回一些其他的相关信息。
* 注:真正使用时,token往往放在响应头里面进行返回。
* 注:真正使用时,用户登录不应用get方法,而是用post方法。
*
* @param name 用户名
* @param password mima
*
* @return 返回jwt
* @date 2019/7/21 11:05
*/
@GetMapping("/login")
@SuppressWarnings("all")
public String login(@RequestParam("name") String name,
@RequestParam("password") String password){
// 模拟 用户名密码 均正确
if("邓沙利文".equals(name) && "123xyz".equals(password)) {
/*
* 模拟获得有效负载信息
* 注:根据自己业务的不同,往往这一步会有非常大的不同
*/
PayloadDTO payloadDTO = getPayload(name);
// 生成token
String payloadJsonString = JSON.toJSONString(payloadDTO);
String token;
try {
token = JwtUtil.generateToken(payloadJsonString, signatureAlgorithmSecret);
} catch (JOSEException e) {
log.error("生成token失败!", e);
return "生成token失败";
}
return token;
}
return "用户名密码有误!";
}
/**
* 验证请求中的token是否被篡改、是否过期(是否有效)。
* 如果有效那么返回响应的业务信息。
*
* 注:真正使用时,token的验证往往在Filter或AOP中进行。
* 注:真正使用时,一般将token放在请求头中,以Authorization作为key,以Bearer 作为值。
*
* @param token json web token(JWT)
*
* @return 返回相应信息
* @date 2019/7/21 11:05
*/
@GetMapping("/test")
public String logis(@RequestHeader("Authorization") String token){
String payloadJsonString;
// 验证token是否被篡改
try {
payloadJsonString = JwtUtil.verifySignature(token, signatureAlgorithmSecret);
} catch (JwtSignatureVerifyException e) {
log.error("token签名验证失败, token已经被篡改!", e);
return "token签名验证失败, token已经被篡改!";
} catch (JOSEException | ParseException e) {
log.error("验证token签名时,系统异常!", e);
return "验证token签名时,系统异常!";
}
/// 验证 有效负载中的其他信息 (如:过期时间、权限信息 等等)
PayloadDTO payloadDTO =JSON.parseObject(payloadJsonString, PayloadDTO.class);
log.info("token中存放的用户信息是 -> {}", payloadDTO);
// token生效时间
long nbf = payloadDTO.getNbf();
log.info("token的生效时间是 -> {}", nbf);
// token有效时长
long exp = payloadDTO.getExp();
log.info("token的生效时长是 -> {}", exp);
// 当前时间
long nowTime = System.currentTimeMillis();
log.info("当前时间是 -> {}", nowTime);
boolean isAuthorized = (nowTime - nbf) >= 0 && exp >= (nowTime - nbf);
log.info("token -> 【{}】是否有效? {}", token, isAuthorized);
if (isAuthorized) {
return "token认证通过!";
}
return "token已过期,请重新登录";
}
/**
* 模拟生成 有效负载信息
*
* 注:根据自己业务情况的不同,可能会往有效负载中放入不同的信息;
* 此步骤的逻辑也可能会非常复杂。
*
* @date 2019/7/21 16:00
*/
private PayloadDTO getPayload(String name) {
return PayloadDTO.builder()
// 放置过期时长
.exp(tokenExpirationTime)
// 放置生效时间
.nbf(System.currentTimeMillis())
// 放置用户信息
.name(name)
.birthday("1994-02-05")
.isAdmin(true)
.build();
}
}
第一步:启动项目,并访问localhost:8080/login?name=邓沙利文&password=123xyz:
第二步:以Authorization为key,以将第一步拿到的token为value,放入请求Header里面,并访问localhost:8080/test:
此时,我们不妨观察一下IDEA的控制台输出:
token中存放的用户信息是 -> PayloadDTO(jti=null, iss=null, exp=60000, sub=null, aud=null, nbf=1563700723248, iat=null, name=邓沙利文, gender=null, birthday=1994-02-05, isAdmin=true)
token的生效时间是 -> 1563700723248
token的生效时长是 -> 60000
当前时间是 -> 1563700766988
token -> 【eyJhbGciOiJIUzI1NiJ9.eyJiaXJ0aGRheSI6IjE5OTQtMDItMDUiLCJleHAiOjYwMDAwLCJpc0FkbWluIjp0cnVlLCJuYW1lIjoi6YKT5rKZ5Yip5paHIiwibmJmIjoxNTYzNzAwNzIzMjQ4fQ.0nCrMAC8QO6r-dVnvQkzsSDlIWu3dxNwxOLnVs74saU】是否有效? true
第三步:等一分钟,让时间超过我们设置的有效时长(本人设置的是60000毫秒),再按照第二步的方式进行访问:
此时,我们不妨再观察一下IDEA的控制台输出:
token中存放的用户信息是 -> PayloadDTO(jti=null, iss=null, exp=60000, sub=null, aud=null, nbf=1563700723248, iat=null, name=邓沙利文, gender=null, birthday=1994-02-05, isAdmin=true)
token的生效时间是 -> 1563700723248
token的生效时长是 -> 60000
当前时间是 -> 1563701012872
token -> 【eyJhbGciOiJIUzI1NiJ9.eyJiaXJ0aGRheSI6IjE5OTQtMDItMDUiLCJleHAiOjYwMDAwLCJpc0FkbWluIjp0cnVlLCJuYW1lIjoi6YKT5rKZ5Yip5paHIiwibmJmIjoxNTYzNzAwNzIzMjQ4fQ.0nCrMAC8QO6r-dVnvQkzsSDlIWu3dxNwxOLnVs74saU】是否有效? false
由此可见,入门级JWT使用并示例成功!