1. 202230616学习记录
目标
- 了解jwt
- jwt的使用
- jwt的应用
1.1. jwt
以前我们使用session解决http的无状态特性,用来作会话跟踪,单机部署的情况下能够很好解决问题;
分布式部署的情况下,服务器只保存自己的session,而请求会被分发到不同服务器上;
因此需要进行session同步,session的同步会随着session数量的增多,部署服务器的增加,同步的网络消耗和服务器的内存存储消耗都会增加;
或者session集中存储,服务器向session服务器存取数据,但是服务器需要额外的网络读写,还有session服务器出问题影响所有服务器。
1.1.1. 简介
JWT:JSON Web Token,定义了一种简洁的、自包含的格式,用于在通信双方以json数据格式安全的传输信息。并且使用数字签名保证这些信息是可靠的。
JWT由三部分组成:
-
第一部分:Header(头),记录令牌类型、签名算法等。 例如:{“alg”:“HS256”,“type”:“JWT”}
-
第二部分:Payload(有效载荷),携带一些自定义信息、默认信息等。 例如:{“id”:“1”,“username”:“Tom”}
-
第三部分:Signature(签名),防止Token被篡改、确保安全性。将header、payload,并加入指定秘钥,通过指定签名算法计算而来。
第一部分
和第二部分
的json串,会使用Base64进行编码,第三部分是使用指定的加密算法和指定的密钥将第一第二部分的Base64码加密得到的
数字签名的存在,让收到信息的一方能够确认传输的信息是否有效,是否被篡改
1.1.2. 使用
1.1.2.1. 引入依赖
<!-- JWT依赖-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
1.1.2.2. 使用提供的工具类:Jwts
- 生成jwt
Map<String, Object> claims = new HashMap<>();
claims.put("id", 1);
claims.put("username", "Tom");
String myjwt = Jwts.builder()
.setClaims(claims) // 添加载荷 还有claim(String var1, Object var2)的方法
.signWith(SignatureAlgorithm.HS512, "myjwt")
.setExpiration(new Date(System.currentTimeMillis() + 1000 * 60))
.compact();
- 解析jwt
//解析
Object o = Jwts.parser()
.setSigningKey("myjwt")//指定签名密钥(必须保证和生成令牌时使用相同的签名密钥)
.parseClaimsJws(myjwt)//解析token
.getBody()//获取解析后的载荷部分
.get("username");//获取键对应的值
System.out.println(o);
1.1.3. 应用
登录认证
登录成功返回jwt给客户端,下次访问从缓存中查询是否有对应token,来判断是否是已登录用户
登录验证时使用上布隆过滤器和缓存
1.1.3.1. 缓存预热
做个定时任务
将用户名和密码放入缓存(密码有所改进,数据库存储的是加密后的密码只存),采用hash数据类型。
@Slf4j
public class UserPreHotJob extends QuartzJobBean {
@Autowired
private IUserDao userDao;
@Autowired
private StringRedisTemplate redisTemplate;
@Override
protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
log.debug("缓存预热开始");
//查询数据库,将用户名密码全部存入redis key="user:username" 的hash类型
List<User> allUsers = userDao.selectList(null);
allUsers.forEach(u -> {
//缓存存储
redisTemplate.opsForHash().put("user:"+u.getUsername(),"user:username",u.getUsername());
redisTemplate.opsForHash().put("user:"+u.getUsername(),"user:password",u.getPassword());
//存入布隆过滤器组成的白名单
// WhiteListUtil.add(u.getUsername());
});
log.debug("缓存预热结束");
}
}
1.1.3.2. server层
查询缓存中是否有username对应的password(key是用username拼接的,只要看password能不能取出来,就能验证username存不存在缓存里)
@Override
public User login(String username, String password) {
//查询缓存
String rPassword = (String) redisTemplate.opsForHash().get("user:" + username, "user:password");
if(StringUtils.hasLength(rPassword)&&rPassword.equals(password)){
//查询到了密码且和输入密码相同,返回用户对象
User user = new User();
user.setUsername(username);
user.setPassword(rPassword);
return user;
}else {
//查询不到
throw new BusinessException(ResultCode.USER_LOGIN_ERROR,"用户名或密码错误");
}
}
1.1.3.3. controller层
登录成功,创建一个jwt并放在响应头信息里
@PostMapping("/login")
public RespData login(String username, String password, HttpServletResponse response){
try {
//没有异常则登录成功,生成jwt
User user = userService.login(username, password);
//载荷内容
Map<String, Object> map = new HashMap<>();
map.put("username",user.getUsername());
//生成jwt
String jwt = MyJwtUtil.buildJwt(map);
//添加响应头
response.addHeader("token",jwt);
return RespData.success(ResultCode.USER_LOGIN_SUCCESS,user);
} catch (BusinessException e) {
//有异常,用户名密码有问题
return RespData.fail(e.getCode(),e.getMessage(),null);
}
}
封装的工具类
@Slf4j
public class MyJwtUtil {
private static String signKey = "f67a4e806a5a7097"; //密钥
private static long expira = 1000L*60; //过期时间 毫秒
//创建jwt
public static String buildJwt(Map<String, Object> map) {
String jwt = Jwts.builder()
.setClaims(map)
.setExpiration(new Date(System.currentTimeMillis() + expira))
.signWith(SignatureAlgorithm.HS512, signKey)
.compact();
return jwt;
}
//验证jwt
public static Claims parseJwt(String jwt){
Claims body = Jwts.parser()
.setSigningKey(signKey)
.parseClaimsJws(jwt)
.getBody();
return body;
}
//用于获取载荷的方法
public static Object getPayload(String jwt,String name){
return JWTUtil.parseToken(jwt).getPayload("username");
}
}
1.1.3.4. 拦截器中验证
验证其余访问请求是否携带token,未携带token、token失效或有异常,不放行
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//获取token头信息
String jwt = request.getHeader("token");
if(!StringUtils.hasLength(jwt)){
//不存在
//创建返回对象的json串
String jsonStr = JSONUtil.toJsonStr(RespData.fail(ResultCode.TOKEN_CHECK_ERROR, "无token", null));
//设置响应头(告知浏览器:响应的数据类型为json、响应的数据编码表为utf-8)
response.setContentType("application/json;charset=utf-8");
//响应
response.getWriter().write(jsonStr);
//拦截器不放行
return false;
}else {
//存在
//解析token
try {
Claims claims = MyJwtUtil.parseJwt(jwt);
//未出异常,放行
return true;
}catch (ExpiredJwtException e){
//token过期
String jsonStr = JSONUtil.toJsonStr(RespData.fail(ResultCode.TOKEN_CHECK_ERROR, "token已过期", null));
response.setContentType("application/json;charset=utf-8");
response.getWriter().write(jsonStr);
return false;
}catch (Exception e) {
//解析token有异常
String jsonStr = JSONUtil.toJsonStr(RespData.fail(ResultCode.TOKEN_CHECK_ERROR, "token异常,禁止登录", null));
response.setContentType("application/json;charset=utf-8");
response.getWriter().write(jsonStr);
return false;
}
}
}
1.1.3.5. token续期
某些场景,用户在页面操作时间太久,导致token已过期,此时用户在页面的某个操作向后端发起了请求,此时判断tokne过期,跳转登录页面,用户的体验不好,对于这类场景,后端要给token续期。
token一旦生成,无法修改其的失效时间,只能新产生一个token,然而也不能检验token一过期就续期,一过期就续期,还是需要一个上限来约束,假如失效好几天,还给人续期,那就不安全且失去了续期的意义,直接token不过期也就不要续期了。
上限的约束方案:使用redis,登录创建token是,往redis存一个key为token值,value也为token值的数据,并设置过期时间为token过期时间的几倍(由自己选择),在拦截器中,携带了token的情况下,在redis中查询key=token的数据,如果不存在,token以及到达过期上限或者token非法,不放行;如果存在取出value,校验value中的token是否过期,如果过期了就先新建token,覆盖value,并且该key延长过期时间再放行;没过期直接放行;
该方案,客户端持有的token可以一直不改变,每次拿取value中token判断是否过期,是否过期达上限,可以减少该token在redis中延长过期时间的次数(用新的token缓冲了一段时间),比客户端持有token一过期,就延长redis的过期时间次数少。
//获取token头信息
String jwt = request.getHeader("token");
if(!StringUtils.hasLength(jwt)){
//不存在
//创建返回对象的json串
String jsonStr = JSONUtil.toJsonStr(RespData.fail(ResultCode.TOKEN_CHECK_ERROR, "无token", null));
//设置响应头(告知浏览器:响应的数据类型为json、响应的数据编码表为utf-8)
response.setContentType("application/json;charset=utf-8");
//响应
response.getWriter().write(jsonStr);
//拦截器不放行
return false;
}else {
//token头信息存在
//从redis中读取token是否存在
if(redisTemplate.hasKey("token:"+jwt)){
//存在,说明redis的过期还没到
//取出旧token验证是否过期
String token = redisTemplate.opsForValue().get("token:" + jwt);
try {
MyJwtUtil.parseJwt(token);
log.debug("token验证通过");
} catch (ExpiredJwtException e) {
//存储的token已过期,获取载荷信息用于新创建token
log.debug("token续期");
String username = (String) MyJwtUtil.getPayload(jwt, "username");
Map<String,Object> map = new HashMap<>();
map.put("username",username);
String newJwt = MyJwtUtil.buildJwt(map);
//用客户携带的token作存入redis的key,重新设置过期时间
redisTemplate.opsForValue().set("token:" + jwt,newJwt,2, TimeUnit.MINUTES);
}
return true;
}else {
//不存在,可能token异常,可能reids中也已经过期
log.debug("token异常");
String jsonStr = JSONUtil.toJsonStr(RespData.fail(ResultCode.TOKEN_CHECK_ERROR, "token异常", null));
response.setContentType("application/json;charset=utf-8");
response.getWriter().write(jsonStr);
return false;
}
}