Redis实战之共享session + jwt 实现登录拦截、刷新token
创始人
2024-02-23 14:14:04
0

共享session问题

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

  • 早期的方案是session拷贝,就是说虽然每个tomcat上都有不同的session,但是每当任意一台服务器的session修改时,都会同步给其他的Tomcat服务器的session,这样的话,就可以实现session的共享了。
  • 问题1:每台服务器中都有完整的一份session数据,服务器压力过大。
  • 问题2:session拷贝数据时,可能会出现延迟
  • 解决:redis天然满足共享session的条件

在这里插入图片描述

设计key的结构

使用哪种结构呢?

  • 由于存入的数据比较简单,我们可以考虑使用String,或者是使用Hash
  • 如果使用String,value 多占用一点空间
  • 如果使用Hash,value中只会存储他数据本身,如果不是特别在意内存,其实使用String就可以。

设计key的具体细节

共享session是每个用户都有自己的session,所以要满足:

  • key要具有唯一性
  • key要方便携带

我们在后台使用 jwt 生成一个字符串 token,然后让前端在 Header 带来这个token就能完成我们的整体逻辑了。

整体访问流程

解决状态登录刷新问题

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

在这里插入图片描述

实例代码

pom文件

 org.springframework.bootspring-boot-starter-data-redisorg.apache.commonscommons-pool2
cn.hutoolhutool-all5.7.17io.jsonwebtokenjjwt0.9.0

jwt 工具类

/*** 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);}}

controller

@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

@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);}
}

ThreadLocal

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 有效期
/*** 请求拦截,刷新 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 userMap = redisTemplate.opsForHash().entries(key);// 3.判断用户是否存在if (userMap.isEmpty()) {return true;}// 4.将查询到的hash数据转为UserUser user = BeanUtil.fillBeanWithMap(userMap, new User(), false);// 5.存在,保存用户信息到 ThreadLocalUserHolder.saveUser(user);// 6.刷新token有效期redisTemplate.expire(key, Duration.ofMinutes(30));// 7.放行return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {// 移除用户UserHolder.removeUser();}
}
拦截器配置
/*** 拦截器配置*/
@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); //拦截器优先级,值越小优先级越高}
}

结果图

登录,并返回token

在这里插入图片描述

刷新token普通请求

在这里插入图片描述

redis,token及用户信息,只要有请求就会刷新 TTL存活时间

在这里插入图片描述

相关内容

热门资讯

【PdgCntEditor】解... 一、问题背景 大部分的图书对应的PDF,目录中的页码并非PDF中直接索引的页码...
监控摄像头接入GB28181平... 流程简介将监控摄像头的视频在网站和APP中直播,要解决的几个问题是:1&...
在Word、WPS中插入AxM... 引言 我最近需要写一些文章,在排版时发现AxMath插入的公式竟然会导致行间距异常&#...
protocol buffer... 目录 目录 什么是protocol buffer 1.protobuf 1.1安装  1.2使用...
修复 爱普生 EPSON L4... L4151 L4153 L4156 L4158 L4163 L4165 L4166 L4168 L4...
Windows10添加群晖磁盘... 在使用群晖NAS时,我们需要通过本地映射的方式把NAS映射成本地的一块磁盘使用。 通过...
Fluent中创建监测点 1 概述某些仿真问题,需要创建监测点,用于获取空间定点的数据࿰...
ChatGPT 怎么用最新详细... ChatGPT 以其强大的信息整合和对话能力惊艳了全球,在自然语言处理上面表现出了惊人...
educoder数据结构与算法...                                                   ...
MySQL下载和安装(Wind... 前言:刚换了一台电脑,里面所有东西都需要重新配置,习惯了所...