前言
在前后端分离的开发架构中,当用户成功登录后,后端服务会生成一个JWT(JSON Web Tokens)token,并将其返回给前端。前端(如Vue应用)接收到此token后,通常会将其存储在LocalStorage中以方便后续请求时使用。每次向后端发送请求时,前端会将这个token作为请求头的一部分发送给后端,以便后端通过自定义的过滤器或中间件进行身份验证。
考虑到JWT token中可能包含敏感的用户信息,并且出于安全考虑,这些token的过期时间往往被设置得相对较短。然而,较短的过期时间可能会带来用户体验上的问题,尤其是在用户进行长时间操作(如填写复杂表单)时,token可能会意外过期,导致用户需要重新登录,进而丢失已填写的数据,这对用户体验是极为不利的。
一、实现原理
在登录流程中,当用户成功认证后,我们可以将生成的JWT token作为键(key)和值(value)存储到缓存系统中,此时键和值相同是为了便于管理和查找。将缓存的有效期设置为JWT token有效期的两倍,这样可以提供一个缓冲期,用于在用户实际token过期后的一段时间内仍然能够验证其身份并可能自动续期。
后端系统通过JWT Filter来拦截每个请求,并验证前端发送的token是否有效。如果token无效,说明是非法请求,Filter将直接抛出异常或返回错误响应。
在JWT Filter中,除了验证前端发送的token之外,还会根据规则去缓存中尝试获取对应的cache token(即之前存储的、与前端token相同的token):
cache token 不存在:
这种情况表明自用户上次活动以来,缓存中的token已经过期或被清除,可能由于用户长时间未操作或系统缓存策略导致。此时,可以认为用户账户处于空闲超时状态,Filter应返回“用户信息已失效,请重新登录”的提示,并引导用户重新进行登录操作。cache token 存在:
如果缓存中存在对应的token,则需要进一步使用JWT工具类来验证这个cache token是否已过期。
未过期:如果cache token未过期,说明用户当前是活跃的,无需进行额外处理,继续处理请求即可。
已过期:如果cache token已过期,但考虑到用户可能一直在进行操作只是token本身失效了,后端程序可以执行以下操作:首先,根据当前用户信息(可能需要从数据库或其他认证服务中获取)重新生成一个新的JWT token;然后,使用这个新的token覆盖缓存中原有的token值;最后,更新该缓存项的生命周期,重新开始计算。这样,用户在无需重新登录的情况下,可以继续他们的操作。
通过这种机制,我们能够在保证JWT token安全性的同时,提升用户体验,减少因token过期导致的频繁登录需求。
三、代码实现(伪码)
登录成功后给用户签发token,并设置token的有效期
...
SysUser sysUser = userService.getUser(username,password);
if(null !== sysUser){
String token = JwtUtil.sign(sysUser.getUsername(),
sysUser.getPassword());
}
...
public static String sign(String username, String secret) {
//设置token有效期为30分钟
Date date = new Date(System.currentTimeMillis() + 30 * 60 * 1000);
//使用HS256生成token,密钥则是用户的密码
Algorithm algorithm = Algorithm.HMAC256(secret);
// 附带username信息
return JWT.create().withClaim("username", username).withExpiresAt(date).sign(algorithm);
}将token存入redis,并设定过期时间,将redis的过期时间设置成token过期时间的两倍
Sting tokenKey = "sys:user:token" + token;
redisUtil.set(tokenKey, token);
redisUtil.expire(tokenKey, 30 * 60 * 2);过滤器校验token,校验token有效性
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
//从header中获取token
String token = httpServletRequest.getHeader("token")
if(null == token){
throw new RuntimeException("illegal request,token is necessary!")
}
//解析token获取用户名
String username = JwtUtil.getUsername(token);
//根据用户名获取用户实体,在实际开发中从redis取
User user = userService.findByUser(username);
if(null == user){
throw new RuntimeException("illegal request,token is Invalid!")
}
//校验token是否失效,自动续期
if(!refreshToken(token,username,user.getPassword())){
throw new RuntimeException("illegal request,token is expired!")
}
...
}实现token的自动续期
public boolean refreshToken(String token, String userName, String passWord) {
Sting tokenKey = "sys:user:token" + token ;
String cacheToken = String.valueOf(redisUtil.get(tokenKey));
if (StringUtils.isNotEmpty(cacheToken)) {
// 校验token有效性,注意需要校验的是缓存中的token
if (!JwtUtil.verify(cacheToken, userName, passWord)) {
String newToken = JwtUtil.sign(userName, passWord);
// 设置超时时间
redisUtil.set(tokenKey, newToken) ;
redisUtil.expire(tokenKey, 30 * 60 * 2);
}
return true;
}
return false;
}
...
public static boolean verify(String token, String username, String secret) {
try {
// 根据密码生成JWT效验器
Algorithm algorithm = Algorithm.HMAC256(secret);
JWTVerifier verifier = JWT.require(algorithm).withClaim("username", username).build();
// 效验TOKEN
DecodedJWT jwt = verifier.verify(token);
return true;
} catch (Exception exception) {
return false;
}
}