每个tomcat中都有一份属于自己的session,假设用户第一次访问第一台tomcat,并且把自己的信息存放到第一台服务器的session中,但是第二次这个用户访问到了第二台tomcat,那么在第二台服务器上,肯定没有第一台服务器存放的session,所以此时 整个登录拦截功能就会出现问题,我们能如何解决这个问题呢?

共享session是每个用户都有自己的session,所以要满足:
我们在后台使用 jwt 生成一个字符串 token,然后让前端在 Header 带来这个token就能完成我们的整体逻辑了。

第一个拦截器中拦截所有的路径,把第二个拦截器做的事情放入到第一个拦截器中,同时刷新令牌,因为第一个拦截器有了threadLocal的数据,所以此时第二个拦截器只需要判断拦截器中的user对象是否存在即可,完成整体刷新功能。

org.springframework.boot spring-boot-starter-data-redis org.apache.commons commons-pool2
cn.hutool hutool-all 5.7.17 io.jsonwebtoken jjwt 0.9.0
/*** jwt工具类*/
public class JwtUtils {//加密 解密时的密钥(盐) 用来生成keypublic static final String JWT_KEY = "campus2022";/*** 生成加密后的秘钥 secretKey** @return*/public static SecretKey generalKey() {byte[] encodedKey = Base64.getDecoder().decode(JwtUtils.JWT_KEY);SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");return key;}/*** 创建jwt密钥** @param subject 加密主体* @param ttlMillis 过期时间* @return String*/public static String createJWT(String subject, long ttlMillis) {SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; //指定签名的时候使用的签名算法,也就是header那部分,jjwt已经将这部分内容封装好了。long nowMillis = System.currentTimeMillis();//生成JWT的时间Date now = new Date(nowMillis);SecretKey key = generalKey();//生成签名的时候使用的秘钥secret,这个方法本地封装了的,一般可以从本地配置文件中读取,切记这个秘钥不能外露哦。它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。JwtBuilder builder = Jwts.builder() //这里其实就是new一个JwtBuilder,设置jwt的body
// .setClaims(claims) //如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的.setId(UUID.randomUUID().toString()) //设置jti(JWT ID):是JWT的唯一标识,根据业务需要,这个可以设置为一个不重复的值,主要用来作为一次性token,从而回避重放攻击。.setIssuedAt(now) //iat: jwt的签发时间.setSubject(subject) //sub(Subject):代表这个JWT的主体,即它的所有人,这个是一个json格式的字符串,可以存放什么userid,roldid之类的,作为什么用户的唯一标志。.signWith(signatureAlgorithm, key);//设置签名使用的签名算法和签名使用的秘钥if (ttlMillis >= 0) {long expMillis = nowMillis + ttlMillis;Date exp = new Date(expMillis);builder.setExpiration(exp); //设置过期时间}return builder.compact(); //就开始压缩为xxxxxxxxxxxxxx.xxxxxxxxxxxxxxx.xxxxxxxxxxxxx这样的jwt}/*** 解密** @param jwt* @return*/public static Claims parseJWT(String jwt) {SecretKey key = generalKey(); //签名秘钥,和生成的签名的秘钥一模一样Claims claims = Jwts.parser() //得到DefaultJwtParser.setSigningKey(key) //设置签名的秘钥.parseClaimsJws(jwt).getBody();//设置需要解析的jwtreturn claims;}/*** 测试** @param args*/public static void main(String[] args) {String userId = "1234";//加密String jwt = createJWT(userId, 3600 * 24);System.out.println("加密后:" + jwt);//解密Claims claims = parseJWT(jwt);String subject = claims.getSubject();System.out.println("解密后:" + subject);}}
@RestController
@RequestMapping("/user")
public class UserController {@Autowiredprivate UserServiceImpl userService;//刷新token普通请求@GetMapping("/hello")public String hello() {return "hello";}//登录@PostMapping("/login")public Result login(@RequestBody User user) {return userService.login(user);}
}
@Service
public class UserServiceImpl implements UserService {@Resourceprivate UserMapper userMapper;@Autowiredprivate StringRedisTemplate redisTemplate;public Result login(User user) {if (user==null || user.getUsername()==null){return Result.fail("账号为空");}LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();wrapper.eq(true, User::getUsername, user.getUsername());User one = userMapper.selectOne(wrapper);if (one==null){return Result.fail("账号未注册");}if (!one.getPassword().equals(user.getPassword())){return Result.fail("密码错误");}//根据用户账号生成tokenString token = JwtUtils.createJWT(one.getUsername(), 24 * 3600);//将用户信息转为MapMap userMap = BeanUtil.beanToMap(one,new HashMap<>(),CopyOptions.create().setIgnoreNullValue(true).setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));//将(token,用户信息)存入redisredisTemplate.opsForHash().putAll("user:token:"+token,userMap);//设置过期时间redisTemplate.expire("user:token:"+token, Duration.ofMinutes(30));//返回token,登录成功return Result.ok(token);}
}
public class UserHolder {private static final ThreadLocal tl = new ThreadLocal<>();public static void saveUser(User user){tl.set(user);}public static User getUser(){return tl.get();}public static void removeUser(){tl.remove();}
}
/*** 登录拦截*/
public class LoginInterceptor implements HandlerInterceptor {//目标资源执行前执行@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 判断是否需要拦截(ThreadLocal中是否有用户)if (UserHolder.getUser() == null) {// 没有,需要拦截,设置状态码response.setStatus(401);// 拦截return false;}// 有用户,则放行return true;}//请求完成后执行@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {// 移除用户UserHolder.removeUser();}
}
/*** 请求拦截,刷新 token 有效期*/
public class RefreshTokenInterceptor implements HandlerInterceptor {private StringRedisTemplate redisTemplate;public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {this.redisTemplate = stringRedisTemplate;}@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 1.获取请求头中的tokenString token = request.getHeader("authorization");if (StrUtil.isBlank(token)) {return true;}// 2.基于token获取redis中的用户String key = "user:token:" + token;Map
/*** 拦截器配置*/
@Configuration
public class MvcConfig implements WebMvcConfigurer {@Autowiredprivate StringRedisTemplate redisTemplate;@Overridepublic void addInterceptors(InterceptorRegistry registry) {// 登录拦截器registry.addInterceptor(new LoginInterceptor()).excludePathPatterns("/user/login","/user/hello") //排除拦截路径.order(1); //拦截器优先级,值越大优先级越低// token刷新的拦截器registry.addInterceptor(new RefreshTokenInterceptor(redisTemplate)).addPathPatterns("/**") //拦截所有路径,用于token刷新.order(0); //拦截器优先级,值越小优先级越高}
}


