JWT的学习
1、HTTP无状态及解决方案
HTTP一种是无状态的协议,每次请求都是一次独立的请求,一次交互之后就是陌生人。
以CSDN为例,先登录一次,然后浏览器退出,这个时候在进入CSDN,按理说服务器是不知道你已经登陆了,所以需要你重新登录,但实际却是再次点进来之后,仍然保持登录状态。
这是因为解决了HTTP无状态这个问题,解决方式有很多:
①使用Session和Cookie,将 无状态 变为 有状态
第一次请求时,服务器会自动记录一个SessionId,然后把SessionId返回给浏览器。再次发起请求时,浏览器就会携带SessionId,而服务器就可以根据SessionId,去查询会话信息(session)。
服务器可以往会话里保存用户信息
浏览器会自动保存SessionId
Session弊端:Session和Cookie适用于单机环境,默认情况下会话信息(session)是保存在服务器内存中,用户量大的话,服务器内存压力大。如果生产是集群环境,就没办法保证浏览器的请求每一次都传到有会话信息的那一台服务器。
这种也好解决,把会话信息存到Redis里,每次访问,服务器根据SessionId,去Redis拿会话信息(SpringBoot实现了这个功能,引入【spring-session-data-redis】依赖包,做一些设置,就会自动将会话信息交给Redis管理),这样做服务器的内存压力也就会见。
②使用Token
在登录时,生成一个Token放入Redis,之后把token返回给浏览器。浏览器发起其他请求时,携带这个token,服务器再去Redis看这个Token是否失效之类的。
③使用JWT
使用JWT和使用Token很像,但JWT不需要存储到Redis,就能通过验签,知道用户信息是否伪造、用户登录状态是否过期等等。
2、JWT的基本介绍
JWT官网地址:https://jwt.io/
JWT,全称JSON WEB TOKEN,是一种JSON格式的Web应用令牌,它是基于令牌去做认证。
什么是令牌,举个例子,古代调兵用的虎符,这就相当于是令牌,有了虎符,才能调兵。而令牌也是,有了令牌,才能访问后端接口。
以下是官网介绍
大致就是JWT能够通过HMAC算法(默认算法),或者通过RSA、ECDSA算法生成数字签名(Signature)。
JWT令牌的组成
JWT由三部分组成:Header(头)、Payload(负载)、Signature(签名),三部分用 "." 进行分隔
Header
两部分组成:令牌的类型是什么,签名(Signature)使用什么算法。
从官网复制的例子,表示令牌的类型是JWT,算法是 HMAC-SHA256
{ "alg": "HS256", "typ": "JWT" }
Header的JSON串经过Base64编码就转换成 "x.y.z" 的 "x"部分
Payload
负载有七个默认字段
iss:发行人
exp:到期时间
sub:主体
aud:用户
nbf:在此之前不可用
iat:发布时间
jti:JWT ID用于标识该JWT
官方样例如下:name 和 admin为自定义的字段
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
负载里面可以放自定义字段,这部分内容也是通过Base64编码变成"x.y.z"部分的"y"部分,可以通过Base64解码,拿到里面的信息。所以,官方也推荐不要往负载里存放敏感信息,比如密码之类的,除非想故意被攻击。
Signature
官方提供的例子如下,可以看见,签名是通过前两部分(Header 和 Payload)base64编码之后的字符串拼接成一个新的字符串,以及指定一个密钥(secret),并使用Header里指定的算法生成的签名。因此,密钥是一定不能暴露出去的。
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
假如攻击者对前两部分进行随意修改,并冒充前端发起请求。后端每次验签的时候,都会根据前两部分内容,加上密钥(secret),再去生成一次签名,而由于攻击者修改了内容,所以生成的签名肯定和传来的签名不匹配,这就说明被篡改了,从而验签失败。
3、JWT的优缺点
优点
1、JWT生成的令牌保存在客户端(前端),服务端不会保存令牌,减少了服务器内存的损耗。
2、易扩展,负载中可以保存自定义的信息。
3、使用强密钥生成签名时,JWT提供了很好的安全性。
4、使用JWT,HTTP仍是无状态的,和Session、Cookie的方式正好相反,JWT不需要在服务器存储会话信息,非常适合集群环境。
缺点
1、JWT 本身不支持会话管理,不能主动使令牌失效。假如用户修改了密码,这个时候肯定要重新登录,原先的JWT令牌按理就应该失效,但是,由于没有到令牌指定的过期时间,所以原先的令牌仍然是有效的。(可以将令牌存到redis,当修改密码后删除令牌,当令牌没有就强制用户去登录)
2、负载(Payload)中存放过多用户数据时,会影响性能。
4、如何使用JWT
大致流程:用户通过前端页面进行登录,业务校验通过之后,生成一个令牌(JWT),后端将JWT返回给前端,前端进行保存。之后,前端每次访问后端,都必须携带JWT,后端去验证令牌(JWT)的合法性。
①导入JWT依赖
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.0</version>
</dependency>
②编写工具类,生成令牌
前面提到了,JWT令牌由三部分组成,所以创建一个JWT令牌,只要保证有这三部分就可以了
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import java.util.Base64;
import java.util.Calendar;
import java.util.HashMap;
public class JWTUtil {
//密钥,一般是从配置文件读取
private static final String secretKey = "4008123123@#";
private static final String algorithm = Algorithm.HMAC256(secretKey).getName();
/**
* 生成JWT令牌
*
* @return
*/
public static String generateJwtToken(String userId, String secretKey) {
Calendar exp = Calendar.getInstance();
//过期时间:当前时间往后推20分钟
exp.add(Calendar.MINUTE, 20);
System.out.println("过期时间:" + exp.getTime());
// HashMap<String, Object> map = new HashMap<String, Object>();
// map.put("alg",algorithm);
// map.put("typ","JWT");
String token = JWT.create()
//header,即使不写,也会使用默认的推荐算法HS256和JWT令牌类型
// .withHeader(map)
.withExpiresAt(exp.getTime()) //默认的负载字段,设置过期时间
.withClaim("userId", userId) //负载---自定义字段
.withClaim("username", "UMR123") //负载---自定义字段
.sign(Algorithm.HMAC256(secretKey));
System.out.println(token);
return token;
}
/**
* 验签(验证JWT令牌的合法性)
*
* @param jwtToken
* @param secretKey
* @return 用来获取令牌信息
*/
public static DecodedJWT verifyToken(String jwtToken, String secretKey) {
//生成验证对象
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(secretKey)).build();
//如果验证没问题,就可以获取到负载信息,如果签名有问题(签名不一致,令牌过期),就会报错
DecodedJWT verify = jwtVerifier.verify(jwtToken);
System.out.println("负载(Payload)经过base64解码:" + new String(Base64.getDecoder().decode(verify.getPayload())));
System.out.println("userId信息:" + verify.getClaim("userId").asString());
System.out.println("username信息:" + verify.getClaim("username").asString());
System.out.println("令牌过期时间:" + verify.getExpiresAt());
return verify;
}
public static void main(String[] args) {
//生成令牌
String jwtToken = generateJwtToken("user123", secretKey);
System.out.println();
verifyToken(jwtToken, secretKey);
}
}
控制台打印结果如下:
服务端生成JWT令牌之后,客户端在请求其他接口时,请求头新增Authorization字段,放入JWT信息
Authorization: Bearer <token>
都是对数据完整性和用户身份进行校验,什么时候直接使用算法生成签名,什么时候使用JWT?
当需求没有要求过期时间,就可以使用直接使用算法,反之,用JWT令牌是最好的。