JSON Web Token(JWT)
1 . 简介
JSON Web Token(缩写JWT)是目前最流行的跨域认证解决方案。
对于传统的用户认证方案为:
- 用户向服务器发送用户名和密码。
- 服务器验证通过后,在当前**对话(session)**里面保存相关数据,比如用户角色、登录时间等等。
- 服务器向用户返回一个 session_id,写入用户的 Cookie。
- 用户随后的每一次请求,都会通过 Cookie将 session_id 传回服务器。
- 服务器收到 session_id,找到对应的session并获取前期保存的数据,由此得知用户的身份。
这种传统的通过session的方式适用于前后端不分离的情况,因为session是保存在服务器端,因此对于跨域或服务器集群的情况很不友好。
为了解决传统用户认证的问题,就出现了JWT这一种方案。可以简单的理解为session变为了保存在客户端而不是保存在服务器端了,用户每次请求都会携其内容。因此也就解决了跨域问题。
2. 原理
- 客户端初次访问,服务器端在进行认证之后会生成一个包含用户数据的JSON对象。为防止该JSON对对象被篡改,服务器会在生成该对象的时候对其进行签名,这个加密后的数据就是token。
- 服务器将token发送给客户端。
- 客户端将token存放在本地。
- 在以后的访问中,客户端每次通信,都会在请求头中携带该JSON对象,服务器端通过该JSON认定用户身份。
- 因此,服务器端不再保存session数据,变成了无状态的情况。从而比较容易实现扩展。
3. 特点
-
jwt基于json,数据处理方便。
-
可以在令牌(token)中自定义内容,容易扩展。
-
使用非对称加密和签名技术,安全性高。
-
资源服务使用JWT,可不依赖认证服务即可完成授权。
JWT令牌较长,占用存储空间较大。
3.2 缺点
4. 组成结构
JWT由3部分组成,分别头部header、荷载payload和签名signature。如下图:
。
4.1 Header
Header 部分是一个 JSON 对象,描述 JWT 的元数据(签名算法、token类型)。如上图中的红色部分。
- alg:(algorithm)表示签名的算法。
- typ:(type)表示这个token(令牌)的类型,JWT 令牌统一写为JWT。 最后,将头部进行base64加密(该加密是可对称解密的),就构成了第一部分。
4.2 PayLoad
载荷就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分:
- **标准中注册的声明,该部分建议但不强制使用
- iss: jwt签发者
- sub: jwt所面向的用户
- aud: 接收jwt的一方
- exp: jwt的过期时间,这个过期时间必须要大于签发时间
- nbf: 定义在什么时间之前,该jwt都是不可用的.
- iat: jwt的签发时间
- jti: jwt的唯一id身份标识,主要用来作为一次性token,从而回避重放攻击。
- 公共的声明: 公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.
- 私有的声明 私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息
4.3 Signature
Jwt的第三部分是一个签证信息,这个签证信息由三部分组成:
- header (base64后的)
- payload (base64后的)
- secret
这个部分需要base64加密后的header和base64加密后的payload使用
.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。
注意:secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。 一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。
5. 使用Java操作JWT
JWT Maven坐标:
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
5.1 生成JWT
JwtBuilder jwtBuilder = Jwts.builder()
// 设置jti
.setId("123")
// 设置用户sub
.setSubject("andy")
// 设置签发时间ita
.setIssuedAt(new Date())
// 设置token过期时间 1分钟
.setExpiration(new Date(60*1000))
// 设置签名算法和盐
.signWith(SignatureAlgorithm.HS256,"xxxx");
// 获取jwt token
String token = jwtBuilder.compact();
System.out.println("token:"+token);
// 解密
String[] strings = token.split("\\.");
String header = Base64Codec.BASE64.decodeToString(strings[0]);
String payload = Base64Codec.BASE64.decodeToString(strings[1]);
String signature = Base64Codec.BASE64.decodeToString(strings[2]);
System.out.println("header:"+header);
System.out.println("payload:"+payload);
System.out.println("signature:"+signature);
通过Jwts创建创建jwtBuilder对象。通过jwtsbuilder配置token的相关信息,然后通过compact方法生成token。
-
setId():设置荷载中的jti,即jwt中的唯一身份标识。
-
setSubject():设置荷载中的sub,即jwt所面向的用户。
-
setIssuedAt():设置j荷载中的jat,即jwt的签发时间。
-
setExpration():设置荷载中的exp,即jwt的过期时间。jwt过期后无法进行解析。
-
cliams():自定义声明,参数以key-value即可。也可以使用
.addClaims(Map)的方式自定义声明。 -
signWith():第一个参数为设置header中的签名算法,第二个参数为singature中的secret。
运行程序,查看输出结果如下。其中signature部分输出为乱码:
token:eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxMjMiLCJzdWIiOiJhbmR5IiwiaWF0IjoxNjQ5OTMwMTY5fQ.sg15LmBWd6g_TAeUxU9s5tKs6L77UakgSBK_4fFpHkI
header:{"alg":"HS256"}
payload:{"jti":"123","sub":"andy","iat":1649930169
y.`Vw��1S�9��:/��jH���
5.2 解析JWT
通过Jwts的parsr()方法可以解析token。通过getXxx()来获取对应的荷载属性。
String token = "eyJhbGcNiJ9.eyJqOioxfQ.sg15LK_4fFpHkI";
//解析token ,需要指定解析的“盐”
Jwt parse = Jwts.parser()
.setSigningKey("xxxx")
.parse(token);
// 获取token的荷载部分
Claims body = (Claims) parse.getBody();
System.out.println("id:"+body.getId());
System.out.println("sub:"+body.getSubject());
System.out.println("iat:"+body.getIssuedAt());
立即执行,输出结果为:
id:123
sub:andy
iat:Thu Apr 14 17:56:09 CST 2022
等待一分钟后再次运行该部分代码,会抛出ExpiredJwtException异常,表示该token已经过期,不能再进行解析:
io.jsonwebtoken.ExpiredJwtException:
JWT expired at 2022-04-14T20:54:56Z. Current time: 2022-04-14T20:55:09Z,
a difference of 13355 milliseconds. Allowed clock skew: 0 milliseconds.
JWT 工具类
通常会将常用的jwt的方法封装到类中,减少不必要的代码重复,以下为结合spring security构成的工具类,同样适用于非security框架。
package top.xcyxiaoxiang.server.utils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* @author: xcy.小相
* @date: 2022/3/3 15:02
* @Description: Jwt 的工具类。
* 该工具类具有以下功能:
* 1. 根据用户信息生成token
* 2. 从token中获取用户名
* 3. 查看token是否有效
* 4. token是否到期
* 5. 刷新token时间
*/
@Component
public class JwtTokenUtil {
/**
* 自定义的荷载的key
*/ // jwt的面向的用户名
private static final String ClAIM_KEY_USERNAME = "sub";
/**
* JWT的加密密钥
*/
@Value("${jwt.secret}")
private String secret;
/**
* jwt失效时间增量
*/
@Value("${jwt.expiration}")
private long expiration;
/**
* 通过userDetails 自定义荷载,并通过荷载生成 token
* * @param userDetails
* @return
*/
public String generateTokenByUserDeatials(UserDetails userDetails) {
// 定义一个荷载,用于存放 生成token的相关信息
HashMap<String, Object> claims = new HashMap<>();
// 用户名称
claims.put(ClAIM_KEY_USERNAME, userDetails.getUsername());
return generateTokenWithClaims(claims);
}
/**
* 从token中获取用户名
*
* @param token
* @return
*/
public String getUserNameFromToken(String token) {
String userName;
try {
Claims claims = this.getClaimsFromToken(token);
// 从荷载中获取用户名
userName = claims.getSubject();
} catch (Exception e) {
userName = null;
}
return userName;
}
/**
* 验证当前用户的token是否有效(username和exp均有效)
* - 用户名不匹配
* - 过期
*
* @param token
* @param userDetails
* @return 有效 true,无效 false
*/ public boolean validateToken(String token, UserDetails userDetails) {
String userName = this.getUserNameFromToken(token);
return userName.equals(userDetails.getUsername()) && !this.isTokenExpired(token);
}
/**
* 刷新token时间
*
* @param token
* @return
*/
public String refreshToken(String token) {
Claims claims = this.getClaimsFromToken(token);
return this.generateTokenWithClaims(claims);
}
/**
* 判断token是否过期失效
*
* @param token
* @return 过期 false 未过期 true
*/ private boolean isTokenExpired(String token) {
Claims claims = this.getClaimsFromToken(token);
Date expireDate = claims.getExpiration();
return expireDate.before(new Date());
}
/**
* 从token中获取荷载
*
* @param token
* @return
*/
private Claims getClaimsFromToken(String token) {
Claims claims;
try {
claims = Jwts.parser()
.setSigningKey(secret) // 指定加密时候的secret
.parseClaimsJws(token) // 指定解密的token
.getBody(); // 获取荷载(payload)部分
} catch (Exception e) {
claims = null;
e.printStackTrace();
}
return claims;
}
/**
* 根据 荷载生成 token
* * @param claims map形式的自定义的荷载
* @return jwt对象
*/
private String generateTokenWithClaims(Map<String, Object> claims) {
return Jwts.builder()
// 放入荷载
.setClaims(claims)
//设置签发时间
.setIssuedAt(new Date())
// 设置荷载的失效时间
.setExpiration(new Date(System.currentTimeMillis() + this.expiration))
// 设置签名的加密方法
.signWith(SignatureAlgorithm.HS512, this.secret)
.compact();
}
}