关于JWT
前言
什么是JWT,JSON Web Token (JWT)是一个开放标准(RFC 7519) ,它定义了一种紧凑和自包含的方式,用于作为 JSON 对象在各方之间安全地传输信息。该信息经过数字签名,可以进行验证和信任,签名技术可以保证信息不被篡改,但是不能保证传输的安全。官网地址:jwt.io/introductio…
JWT的结构
在其紧凑的形式中,JWT由以点(.)分隔的三个部分组成,它们是:
- Header
- Payload
- Signature
并且你可以通过官网jwt.io/#debugger-i… 可使用 JWT.io Debugger 来解码、验证和生成 JWT )。
(1) Header
报头通常由两部分组成: Token的类型(即 JWT)和所使用的签名算法(如 HMAC SHA256或 RSA)。
eg:
{
"alg": "HS256",
"typ": "JWT"
}
(2) Payload
Token的第二部分是有效负载,其中包含声明。声明是关于实体(通常是用户)和其他数据的语句
eg:
{
"sub": "1234567890",// 注册声明
"name": "John Doe",// 公共声明
"admin": true // 私有声明
}
(3) Signature
要创建Signature,您必须获取编码的标头(header)、编码的有效载荷(payload)、secret、标头中指定的算法,并对其进行签名。
例如,如果您想使用 HMAC SHA256算法,签名将按以下方式创建:
eg:
HMACSHA256(
base64UrlEncode(header)+"."+base64UrlEncode(payload),
secret
)
JWT入门案例实现:
通过Java代码实现JWT的生成( 使用的是JJWT框架 )
先导入JJWT的依赖(JJWT是JWT的框架)
<!--JWT(Json Web Token)登录支持-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
测试代码如下:
public class JjwtTest {
@Test
public void generateToken() {
// JWT头部分信息【Header】
Map<String, Object> header = new HashMap<>();
header.put("alg", "HS256");
header.put("typ", "JWT");
// 载核【Payload】
Map<String, Object> payload = new HashMap<>();
payload.put("sub", "1234567890");
payload.put("name","John Doe");
payload.put("admin",true);
// 声明Token失效时间
Calendar instance = Calendar.getInstance();
instance.add(Calendar.SECOND,300);// 300s
// 生成Token
String token = Jwts.builder()
.setHeader(header)// 设置Header
.setClaims(payload) // 设置载核
.setExpiration(instance.getTime())// 设置生效时间
.signWith(SignatureAlgorithm.HS256,"secret") // 签名,这里采用私钥进行签名,不要泄露了自己的私钥信息
.compact(); // 压缩生成xxx.xxx.xxx
System.out.println(token);
}
}
运行结果:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImV4cCI6MTY5NTUyNDYwNX0.DOpq5VaXA-0ga4Q40i1CINaj0t2QSZLWmeH5qLT-nFY
通过Java代码实现JWT的解码( 使用的是JJWT框架 )
测试代码如下:
@Test
public void getInfoByJwt() {
// 生成的token
String token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImV4cCI6MTY2MzI5NzQzMX0.Ju5EzKBpUnuIRhDG1SU0NwMGsd9Jl_8YBcMM6PB2C20";
// 解析head信息
JwsHeader jwsHeader = Jwts
.parser()
.setSigningKey("secret")
.parseClaimsJws(token)
.getHeader();
System.out.println(jwsHeader); // {typ=JWT, alg=HS256}
System.out.println("typ:"+jwsHeader.get("typ"));
// 解析Payload
Claims claims = Jwts
.parser()
.setSigningKey("secret")
.parseClaimsJws(token)
.getBody();
System.out.println(claims);// {sub=1234567890, name=John Doe, admin=true, exp=1663297431}
System.out.println("admin:"+claims.get("admin"));
// 解析Signature
String signature = Jwts
.parser()
.setSigningKey("secret")
.parseClaimsJws(token)
.getSignature();
System.out.println(signature); // Ju5EzKBpUnuIRhDG1SU0NwMGsd9Jl_8YBcMM6PB2C20
}
运行结果:
{typ=JWT, alg=HS256}
typ:JWT
{sub=1234567890, name=John Doe, admin=true, exp=1695524886}
admin:true
o4k7rJamDt-K8Jqa2KsJzaUsEtgSjksu1PVOHWNFyPk
Process finished with exit code 0
JWT工具类
在实际的项目中,一般都会将上面的操作封装成工具类来使用。
应用配置文件application.yaml中加入如下配置:
jwt:
tokenHeader: Authorization #JWT存储的请求头
secret: mall-admin-secret #JWT加解密使用的密钥【私钥】
expiration: 604800 #JWT的超期限时间(60*60*24*7)
tokenHead: 'Bearer ' #JWT负载中拿到开头
工具类代码如下:
package com.dudu.mall.utils;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* JwtToken生成的工具类
* JWT token的格式:header.payload.signature
* header的格式(算法、token的类型):
* {"alg": "HS512","typ": "JWT"}
* payload的格式(用户名、创建时间、生成时间):
* {"sub":"wang","created":1489079981393,"exp":1489684781}
* signature的生成算法:
* HMACSHA512(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret)
*/
public class JwtTokenUtil {
private static final Logger LOGGER = LoggerFactory.getLogger(JwtTokenUtil.class);
private static final String CLAIM_KEY_USERNAME = "sub";
private static final String CLAIM_KEY_CREATED = "created";
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration}")
private Long expiration;
@Value("${jwt.tokenHead}")
private String tokenHead;
/**
* 根据负责生成JWT的token
*/
private String generateToken(Map<String, Object> claims) {
return Jwts.builder()
.setClaims(claims)
.setExpiration(generateExpirationDate())
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
/**
* 从token中获取JWT中的负载
*/
private Claims getClaimsFromToken(String token) {
Claims claims = null;
try {
claims = Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
} catch (Exception e) {
LOGGER.info("JWT格式验证失败:{}", token);
}
return claims;
}
/**
* 生成token的过期时间
*/
private Date generateExpirationDate() {
return new Date(System.currentTimeMillis() + expiration * 1000);
}
/**
* 从token中获取登录用户名
*/
public String getUserNameFromToken(String token) {
String username;
try {
Claims claims = getClaimsFromToken(token);
username = claims.getSubject();
} catch (Exception e) {
username = null;
}
return username;
}
/**
* 验证token是否还有效
*
* @param token 客户端传入的token
* @param userDetails 从数据库中查询出来的用户信息
*/
public boolean validateToken(String token, UserDetails userDetails) {
String username = getUserNameFromToken(token);
return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
}
/**
* 判断token是否已经失效
*/
private boolean isTokenExpired(String token) {
Date expiredDate = getExpiredDateFromToken(token);
return expiredDate.before(new Date());
}
/**
* 从token中获取过期时间
*/
private Date getExpiredDateFromToken(String token) {
Claims claims = getClaimsFromToken(token);
return claims.getExpiration();
}
/**
* 根据用户信息生成token
*/
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
claims.put(CLAIM_KEY_USERNAME, userDetails.getUsername());
claims.put(CLAIM_KEY_CREATED, new Date());
return generateToken(claims);
}
/**
* 当原来的token没过期时是可以刷新的
*
* @param oldToken 带tokenHead的token
*/
public String refreshHeadToken(String oldToken) {
if(StrUtil.isEmpty(oldToken)){
return null;
}
String token = oldToken.substring(tokenHead.length());
if(StrUtil.isEmpty(token)){
return null;
}
//token校验不通过
Claims claims = getClaimsFromToken(token);
if(claims==null){
return null;
}
//如果token已经过期,不支持刷新
if(isTokenExpired(token)){
return null;
}
//如果token在30分钟之内刚刷新过,返回原token
if(tokenRefreshJustBefore(token,30*60)){
return token;
}else{
claims.put(CLAIM_KEY_CREATED, new Date());
return generateToken(claims);
}
}
/**
* 判断token在指定时间内是否刚刚刷新过
* @param token 原token
* @param time 指定时间(秒)
*/
private boolean tokenRefreshJustBefore(String token, int time) {
Claims claims = getClaimsFromToken(token);
Date created = claims.get(CLAIM_KEY_CREATED, Date.class);
Date refreshDate = new Date();
//刷新时间在创建时间的指定时间内
if(refreshDate.after(created)&&refreshDate.before(DateUtil.offsetSecond(created,time))){
return true;
}
return false;
}
}
JWT是如何工作的?
Jwt的工作流程:
- 应用程序或客户端向授权服务器请求授权。
- 授予授权后,授权服务器将向应用程序返回访问令牌。
- 应用程序使用访问令牌访问受保护的资源(如 API)。