前言:传统项目中,用户的身份认证等信息都是通过session来处理(客户端和服务端保存一个对应的sessionid,每次请求都会携带该sessionid进行逻辑处理)。但是在集群环境或者用户请求量较大的情况下,使用session会大大增加代码处理复杂度以及压力。
JWT(JSON WEB TOKEN)能解决上面涉及的问题,其实际上就是一个三部分组成的字符串,该字符串包含了头部、载荷与签名。我们可以通过JWT在客户端和服务器间进行安全可靠的信息传递。
一、三部分组成说明
(1)头部(Header)
JWT的头部是一个描述该JWT基本信息的JSON对象,比如描述其类型以及签名所用的算法等。如下,alg字段表示签名使用的算法,默认为HMAC SHA256(写为HS256);typ字段表示令牌的类型,JWT令牌统一写为JWT。
{
"typ": "JWT",
"alg": "HS256"
}
将其进行Base64编码转换为字符串,该字符串即为JWT的头部
(2)载荷(Payload)
载荷是JWT的主体内容部分,其是一个包含需要传递数据(存储)的JSON对象。
JWT默认指定了如下几个基础信息(标准定义):
iss:该JWT的签发者,也就是这个“令牌”归属于哪个用户。一般是userId
exp:过期时间,也就是这个令牌什么时候失效
sub:主题,即该JWT所面向的用户
aud:用户,即接受该JWT的一方
iat:发布时间,也就是这个令牌是什么时候创建的
jti:JWT ID,通过算法生成的一个唯一标识
另外,我们也可以将自己需要保存的信息放在这里,如:
{
"name": "test",
"role": "admin"
}
将上面涉及的JSON对象进行Base64编码转换为字符串,该字符串即为JWT的Payload(载荷)。
注意:因为Base64是一种编码而不是加密方式,其可以被翻译回原来的样子,所以不能存放隐私信息(如密码之类的)。
(3)签名
将上面生成的两个编码字符串通过.号连接起来(头部.载荷),组成一个新字符串。然后将该新字符串用HS256(头部声明的加密算法)算法进行加密,加密的过程中还要使用我们提供的密钥。加密后生成了一个新字符串,该字符串即为签名。
(4)jwt字符串
最后,我们将上面的三个字符串通过.拼接起来,即形成了我们的JWT,即头部.载荷.签名。
二、代码演示
1、引入依赖:
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
2、代码(一般我们会在用户请求获取token的时候,在生成的jwt token前面加上 "Bearer ",即 "Bearer token"。然后通过响应头返回给用户,之后用户每次调用服务的restful接口的时候,都要在请求头加上该token,即请求头一般为:"Authorization":"Bearer token",下面的代码即是以这种形式操作):
import io.jsonwebtoken.*;
import org.springframework.stereotype.Component;
import java.time.Instant;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
@Component
public class JwtUtil {
//过期时间设置,这里设置过期时间为7天
public static final long EXPIRE_TIME = 604800L;
//服务端的秘钥,一般存放在配置文件方便修改,在任何场景都不能泄露出去
static final String SECRET = "abcd";
static final String TOKEN_PREFIX = "Bearer ";
//static final String HEADER_KEY = "Authorization";
/**
* 通过该方法生成jwt对应的token
* 使用Hs256算法,密钥为常量 SECRET
* @param userId 要设置到载荷的用户数据
* @param username 签发人信息
* @return
*/
public static String createJwtToken(Integer userId, String username) {
Instant now=Instant.now();
//为payload添加各种标准声明和私有声明
JwtBuilder jwtBuilder = Jwts.builder()
//另外,如果设置的数据多,可以把一些安全的数据放到map,然后使用setClaims(map)设置到载荷中来代替多个.claim()操作
//私有声明是给builder的claim赋值,一定要先设置私有的声明,一旦写在标准的声明赋值之后,可能会覆盖标准声明
.claim("userId",userId)
//设置签发时间
.setIssuedAt(Date.from(now))
//设置过期时间
.setExpiration(Date.from(now.plusSeconds(EXPIRE_TIME)))
//设置签名使用的签名算法和秘钥
.signWith(SignatureAlgorithm.HS256, SECRET)
//代表这个JWT的拥有人
.setSubject("username");
return TOKEN_PREFIX + jwtBuilder.compact();
}
/**
* 通过该方法将jwt字符串解析
* @param token
* @return
*/
public static Claims parseJwtToken(String token){
try
{
if(Objects.isNull(token)||!token.startsWith(TOKEN_PREFIX))
{
throw new IllegalArgumentException("illggal jwt");
}
token = token.replace(TOKEN_PREFIX, "");
Claims claims = Jwts.parser()
//设置签名的秘钥
.setSigningKey(SECRET.getBytes())
.parseClaimsJws(token)
.getBody();
//可以通过如下方法从claims获取jwt token中设置的载荷信息
if(!Objects.isNull(claims))
{
claims.getSubject();
claims.get("userId",Integer.class);
}
return claims;
}
catch (ExpiredJwtException e){
// 如果jwt过期则会抛出这个异常。
throw e;
}
}
/**
* 验证该jwt token是否有效
* @param token
* @return
*/
public static boolean validateJwtToken(String token) {
if (!Objects.isNull(token)) {
// 解析token
try {
Claims claims = parseJwtToken(token);
Integer userId = claims.get("userId",Integer.class);
boolean isExpiredToken = claims.getExpiration().before(Date.from(Instant.now().plusSeconds(EXPIRE_TIME)));
if(Objects.isNull(userId) || isExpiredToken){
return false;
}
return true;
} catch (ExpiredJwtException e) {
// 如果jwt过期则会抛出这个异常。
return false;
}
}else {
return false;
}
}
}
三、问题
1、我们的信息是否会暴露
因为base64编码可逆,所以我们放置在载荷的信息是能够被获取的,因此不应该在载荷存放隐私数据。但可以存放一些用户身份验证以及权限方面的信息。
2、jwt怎么验证信息
jwt包含了签名字符串,该字符串是通过 头部.载荷 的字符串进行加密生成。所以服务器在接受到jwt字符串后,会对 头部.载荷 进行同样的加密算法计算,如果计算出来的字符串和jwt的签名字符串不一致则表示该jwt被修改过,将会被拒绝访问(401)。
3、有什么缺点
jwt在使用的时候,会涉及到过期时间的设置,该时间一旦设置则无法修改,所以jwt的时间续签问题是其致命缺点。
另外,在单点登录如果使用jwt,则需要防止该jwt token被别人获取并进行登录。