• Redis实战之共享session + jwt 实现登录拦截、刷新token


    共享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文件

     <dependency>
        <groupId>org.springframework.bootgroupId>
          <artifactId>spring-boot-starter-data-redisartifactId>
      dependency>
      <dependency>
          <groupId>org.apache.commonsgroupId>
          <artifactId>commons-pool2artifactId>
      dependency>
    
     <dependency>
        <groupId>cn.hutoolgroupId>
         <artifactId>hutool-allartifactId>
         <version>5.7.17version>
     dependency>
     <dependency>
         <groupId>io.jsonwebtokengroupId>
         <artifactId>jjwtartifactId>
         <version>0.9.0version>
     dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    jwt 工具类

    /**
     * jwt工具类
     */
    public class JwtUtils {
    
        //加密 解密时的密钥(盐) 用来生成key
        public 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();//设置需要解析的jwt
            return 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);
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78

    controller

    @RestController
    @RequestMapping("/user")
    public class UserController {
    
        @Autowired
        private UserServiceImpl userService;
    
    
        //刷新token普通请求
        @GetMapping("/hello")
        public String hello() {
            return "hello";
        }
    
        //登录
        @PostMapping("/login")
        public Result login(@RequestBody User user) {
            return userService.login(user);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    service

    @Service
    public class UserServiceImpl implements UserService {
    
        @Resource
        private UserMapper userMapper;
    
        @Autowired
        private StringRedisTemplate redisTemplate;
    
        public Result login(User user) {
            if (user==null || user.getUsername()==null){
                return Result.fail("账号为空");
            }
            LambdaQueryWrapper<User> 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("密码错误");
            }
            //根据用户账号生成token
            String token = JwtUtils.createJWT(one.getUsername(), 24 * 3600);
            //将用户信息转为Map
            Map<String, Object> userMap = BeanUtil.beanToMap(one,
                    new HashMap<>(),
                    CopyOptions.create()
                            .setIgnoreNullValue(true)
                            .setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));
            //将(token,用户信息)存入redis
            redisTemplate.opsForHash().putAll("user:token:"+token,userMap);
            //设置过期时间
            redisTemplate.expire("user:token:"+token, Duration.ofMinutes(30));
            //返回token,登录成功
            return Result.ok(token);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    ThreadLocal

    public class UserHolder {
        private static final ThreadLocal<User> 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();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    拦截器

    登录拦截
    /**
     * 登录拦截
     */
    public class LoginInterceptor implements HandlerInterceptor {
    
        //目标资源执行前执行
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            // 判断是否需要拦截(ThreadLocal中是否有用户)
            if (UserHolder.getUser() == null) {
                // 没有,需要拦截,设置状态码
                response.setStatus(401);
                // 拦截
                return false;
            }
            // 有用户,则放行
            return true;
        }
    
        //请求完成后执行
        @Override
        public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
            // 移除用户
            UserHolder.removeUser();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    请求拦截,刷新 token 有效期
    /**
     * 请求拦截,刷新 token 有效期
     */
    public class RefreshTokenInterceptor implements HandlerInterceptor {
    
        private StringRedisTemplate redisTemplate;
    
        public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {
            this.redisTemplate = stringRedisTemplate;
        }
    
    
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            // 1.获取请求头中的token
            String token = request.getHeader("authorization");
            if (StrUtil.isBlank(token)) {
                return true;
            }
            // 2.基于token获取redis中的用户
            String key  = "user:token:" + token;
            Map<Object, Object> userMap = redisTemplate.opsForHash().entries(key);
            // 3.判断用户是否存在
            if (userMap.isEmpty()) {
                return true;
            }
            // 4.将查询到的hash数据转为User
            User user = BeanUtil.fillBeanWithMap(userMap, new User(), false);
            // 5.存在,保存用户信息到 ThreadLocal
            UserHolder.saveUser(user);
            // 6.刷新token有效期
            redisTemplate.expire(key, Duration.ofMinutes(30));
            // 7.放行
            return true;
        }
    
        @Override
        public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
            // 移除用户
            UserHolder.removeUser();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    拦截器配置
    /**
     * 拦截器配置
     */
    @Configuration
    public class MvcConfig implements WebMvcConfigurer {
    
        @Autowired
        private StringRedisTemplate redisTemplate;
    
        @Override
        public 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); //拦截器优先级,值越小优先级越高
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    结果图

    登录,并返回token

    在这里插入图片描述

    刷新token普通请求

    在这里插入图片描述

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

    在这里插入图片描述

  • 相关阅读:
    加密安全-openssh服务
    Java基础之《网站跨域问题》
    22 VueComponent 响应式处理
    Node.js | express 获取请求参数 | 客户端渲染 | 服务端渲染
    腾讯云轻量2核4G5M服务器双11优惠价166元一年可选三年
    C++-STL-map:map插入元素的几种方式【用数组方式插入数据】【用insert函数插入pair数据】【用insert函数插入value_type数据】
    Blocking waiting for file lock on the registry index 问题解决
    家庭房产(PTA)
    华中科技大学计算机组成原理慕课答案-第六章-中央处理器(二)
    SwiftUI 4.0:两种方式实现子视图导航功能
  • 原文地址:https://blog.csdn.net/qq_54429571/article/details/128075765