Redis6:短信登录

欢迎来到"雪碧聊技术"CSDN博客!

在这里,您将踏入一个专注于Java开发技术的知识殿堂。无论您是Java编程的初学者,还是具有一定经验的开发者,相信我的博客都能为您提供宝贵的学习资源和实用技巧。作为您的技术向导,我将不断探索Java的深邃世界,分享最新的技术动态、实战经验以及项目心得。

让我们一同在Java的广阔天地中遨游,携手提升技术能力,共创美好未来!感谢您的关注与支持,期待在"雪碧聊技术"与您共同成长!

目录

一、短信登录3-集群的session共享问题

1、什么是集群的session共享问题?

2、如何解决上述问题?答:使用redis代替session。

二、短信登录4-基于Redis实现共享session登录

1、使用Redis,存储验证码

①用户输入手机号,请求获取验证码

②用户输入手机号、验证码,请求短信验证登录。后端从redis中取出正确的验证码,与用户输入的验证码核对。

2、如何将用户信息存入Redis中(关键※)

①如果验证该手机号、验证码都没毛病,证明应该让人家登录。此时就需要为该用户生成一个token(唯一的),并以token为key,用户信息为value,存入redis中。

②最关键的一步:把上面生成的token,返回到客户端,客户端再将该token保存到本地,作为以后登录、访问其他后端接口的令牌!

③以后客户端访问任何后端接口,都要携带token,经过后端拦截器的许可后,才能得到后端服务。

3、根据1和2的逻辑,完成具体的代码开发

①完成验证码存储到redis中

②使用redis,完成登录验证功能

③如果验证码一致,则校验登录成功。下面就需要为用户生成jwt令牌(此处用UUID来生成),然后将jwt令牌(作为键)和用户信息(作为值)存入redis中。

④别忘了将该token返回给客户端,让客户端存起来,以后每次请求都要携带该token,来通过后端拦截器的拦截,从而获取后端服务。

⑤重新编写拦截器,从redis中获取token,看看前端是否拥有token令牌,从而决定是否应该拦截该请求

注意:在拦截器中,无法注入stringRedisTemplate对象,因为这个拦截器类是我们自定义的,因此无法交给Spring管理,因此只能通过构造器的方式来获取stringRedisTemplate对象。那么谁来给这个拦截器类的构造器传入一个stringRedisTemplate对象呢?那就是配置类,因为配置类需要使用拦截器对象,如下:

⑥重启后端项目,查看运行效果

⑦解决stringRedisTemplate的类型转换异常

⑧解决上述异常后,再次启动项目,查看运行效果:

4、优化登录状态刷新问题

①什么是登录状态刷新?

②那目前的登录状态刷新有什么缺点?

③如何优化这个问题?

④重新启动项目,查看运行效果


一、短信登录3-集群的session共享问题

1、什么是集群的session共享问题?

当项目足够大时,一台Tomcat服务器的压力比较大,因此就会水平拓展出很多台Tomcat服务器,然后nginx服务器再进行负载均衡,将前端的请求合理地分配给后端的多台Tomcat服务器。

此时就会出现Session共享问题:多台Tomcat服务器之间,并不共享session存储空间,当前端请求被nginx服务器分配到不同的tomcat服务器时会导致数据丢失。

举例:当我填写手机号,要求后端生成一个验证码时,此时后端就会生成一个验证码,并存放到session中。

然后用户的手机短信,会收到刚刚后端生成的验证码"043018",然后会输入到页面上,在请求验证码登录:

然后后端会拿出刚刚在session中存入的验证码,和用户现在输入的验证码进行对比,如果一致,说明确实是该手机号的主人,如果不一致,则说明不是本人。

那此时如果该请求被nginx服务器,分配到了其他Tomcat服务器,此时由于不同的Tomcat服务器之间是不共享session的,所以后端此时就无法拿到刚刚存入session中的正确验证码,因此就无法验证登录。这就叫"集群的session共享问题"。

2、如何解决上述问题?答:使用redis代替session。

此时我们用redis来存储用户信息,如:验证码。

此时就能解决不同的Tomcat的session共享问题了。

二、短信登录4-基于Redis实现共享session登录

1、使用Redis,存储验证码

①用户输入手机号,请求获取验证码

然后后端将该验证码,存入redis中,以手机号为键,验证码为值,如下:

②用户输入手机号、验证码,请求短信验证登录。后端从redis中取出正确的验证码,与用户输入的验证码核对。

2、如何将用户信息存入Redis中(关键※)

思路:

①如果验证该手机号、验证码都没毛病,证明应该让人家登录。此时就需要为该用户生成一个token(唯一的),并以token为key,用户信息为value,存入redis中。

②最关键的一步:把上面生成的token,返回到客户端,客户端再将该token保存到本地,作为以后登录、访问其他后端接口的令牌!

举例:

③以后客户端访问任何后端接口,都要携带token,经过后端拦截器的许可后,才能得到后端服务。

3、根据1和2的逻辑,完成具体的代码开发

①完成验证码存储到redis中

验证功能:

后端生成验证码947864

去redis中查看,是否存入了该验证码?

此时就完成了验证码存储到redis中。

②使用redis,完成登录验证功能

③如果验证码一致,则校验登录成功。下面就需要为用户生成jwt令牌(此处用UUID来生成),然后将jwt令牌(作为键)和用户信息(作为值)存入redis中。

上面设置了token的有效期,时长为30分钟。也就是说,你登录成功并进入内部页面,30分钟回来以后,会显示你登录过期,请重新登陆,很多应用都是这么做的。

④别忘了将该token返回给客户端,让客户端存起来,以后每次请求都要携带该token,来通过后端拦截器的拦截,从而获取后端服务。

⑤重新编写拦截器,从redis中获取token,看看前端是否拥有token令牌,从而决定是否应该拦截该请求

java 复制代码
public class LoginInterceptor implements HandlerInterceptor {

    private StringRedisTemplate stringRedisTemplate;
    public LoginInterceptor(StringRedisTemplate stringRedisTemplate){
        this.stringRedisTemplate = stringRedisTemplate;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //1、获取session(废弃)
        //HttpSession session = request.getSession();
        //1、获取请求头中的token(前端携带过来的)
        String token = request.getHeader("authorization");
        if(StrUtil.isBlank(token)){//判断token是否为空
            //token为空,拦截,返回401状态码
            response.setStatus(401);

        }
        //2、根据该token,获取redis中的用户
        String key = RedisConstants.LOGIN_USER_KEY + token;
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash()
                .entries(key);
        //3、判断用户是否为空
        if(userMap.isEmpty()){
            //4、不存在,拦截,返回401状态码
            response.setStatus(401);
            return false;
        }

        //5、将从redis查询到的hash数据,转为UserDTO对象
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);

        //6、存在,保存用户信息到ThreadLocal
        UserHolder.saveUser(userDTO);

        //7、刷新token的有效期
        stringRedisTemplate.expire(key, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
        //8、放行
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        //从ThreadLocal中,移除用户
        //这里是避免内存泄漏:防止ThreadLocal数据明明不再使用,却一直占用JVM内存,这就叫内存泄露
        UserHolder.removeUser();
    }
}
注意:在拦截器中,无法注入stringRedisTemplate对象,因为这个拦截器类是我们自定义的,因此无法交给Spring管理,因此只能通过构造器的方式来获取stringRedisTemplate对象。那么谁来给这个拦截器类的构造器传入一个stringRedisTemplate对象呢?那就是配置类,因为配置类需要使用拦截器对象,如下:

⑥重启后端项目,查看运行效果

查看后端报错:

⑦解决stringRedisTemplate的类型转换异常

这里的userMap在本质上,就是一个userDTO对象。

查看UserDTO类的构成:

因为我们的StringRedisTemplate有一个特点,就是往redis中存数据时,要求key和value都得是String类型的。下面的源码:

可见上述就是Long类型的id,转为String类型的过程中出现了异常。

解决方案:若还想继续使用StringRedisTemplate,就必须自己手动转,如下:

⑧解决上述异常后,再次启动项目,查看运行效果:

可见此时就登入进来了,下面查看一下redis中是否有该用户的token和用户信息:

4、优化登录状态刷新问题

①什么是登录状态刷新?

就是每次请求时,都要刷新redis中的token的有效期。

如果不刷新,那么redis中的有效期,就会一直是在登录时给的那30分钟,若这期间我经常访问该项目,证明我一直在,那么就应该刷新这个token的有效期。

②那目前的登录状态刷新有什么缺点?

没有被拦截的请求路径,如下:

此时这些请求路径,由于根本不执行拦截器的代码,因此根本没机会执行拦截器中刷新redis的token有效期的代码,因此是不合理的。

比如:我一直在访问首页,而这个首页就不是拦截器拦截的范围,因此不走拦截器,于是就不会刷新token的有效期。

③如何优化这个问题?

在原有的拦截器前面,再加一个拦截器,专门用来拦截全部的请求,并刷新token的有效期,此时就完美了,如下:

具体操作如下:

编写代码:

java 复制代码
public class RefreshTokenInterceptor implements HandlerInterceptor {
    private StringRedisTemplate stringRedisTemplate;
    public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate){
        this.stringRedisTemplate = stringRedisTemplate;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //1、获取session(废弃)
        //HttpSession session = request.getSession();
        //1、获取请求头中的token(前端携带过来的)
        String token = request.getHeader("authorization");
        if(StrUtil.isBlank(token)){//判断token是否为空
            return true;//此处不拦截,第二个拦截器也会拦截下来,因为还没往ThreadLocal中存用户信息呢!

        }
        //2、根据该token,获取redis中的用户
        String key = RedisConstants.LOGIN_USER_KEY + token;
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash()
                .entries(key);
        //3、判断用户是否为空
        if(userMap.isEmpty()){

            return true;//此处不拦截,第二个拦截器也会拦截下来,因为还没往ThreadLocal中存用户信息呢!
        }

        //5、将从redis查询到的hash数据,转为UserDTO对象
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);

        //6、存在,保存用户信息到ThreadLocal
        UserHolder.saveUser(userDTO);

        //7、刷新token的有效期
        stringRedisTemplate.expire(key, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
        //8、放行
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        //从ThreadLocal中,移除用户
        //这里是避免内存泄漏:防止ThreadLocal数据明明不再使用,却一直占用JVM内存,这就叫内存泄露
        UserHolder.removeUser();
    }
}

修改原来的拦截器:

java 复制代码
public class LoginInterceptor implements HandlerInterceptor {


    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //1、判断是否需要拦截(依据是ThreadLocal中是否有用户信息)
        if(UserHolder.getUser() == null){
            //ThreadLocal中没有用户信息,需要拦截
            response.setStatus(401);
            return false;

        }
        //由用户,则放行
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        //从ThreadLocal中,移除用户
        //这里是避免内存泄漏:防止ThreadLocal数据明明不再使用,却一直占用JVM内存,这就叫内存泄露
        UserHolder.removeUser();
    }
}

然后再修改下配置类,配置一下这两个拦截器的拦截范围、执行先后顺序等:

java 复制代码
@Configuration
public class MvcConfig implements WebMvcConfigurer {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //登录拦截器(后执行)
        registry.addInterceptor(new LoginInterceptor())
                .excludePathPatterns(//访问下面声明的接口前,都不走拦截器,用的排除法
                        "/shop/**",
                        "/voucher/**",
                        "/shop-type/**",
                        "/upload/**",
                        "/blog/hot",
                        "/user/code",
                        "/user/login"
                ).order(1);
        //用于token刷新的拦截器(先执行)
        registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);
    }
}

其中的order,用来指定该拦截器的执行顺序,数字越小,越先执行。

addPathPatterns("/**")方法,表示新添加的拦截器,要拦截全部的请求,从而进行redis中的token有效期的刷新操作。

④重新启动项目,查看运行效果

我们登录成功后,查看此时redis中的token有效期是多少:

下面我们访问一下前端那些没有被拦截的路径,如:首页,看看这个token的有效期会不会刷新:

此时,我们的登录状态刷新就得到了优化,即:访问原先没有被拦截的请求路径,也会刷新redis中的token有效期。

以上就是本篇文章的全部内容,如果感兴趣,请关注本博主吧~~

相关推荐
ac.char4 小时前
在 Ubuntu 上安装 Redis 并为其设置登录密码
linux·redis·ubuntu
极客星辰7 小时前
Linux redis-6.2.6安装
linux·运维·redis
NiNg_1_2348 小时前
SpringBoot集成Redis详解
spring boot·redis·bootstrap
看山还是山,看水还是。9 小时前
Redis 命令
前端·数据库·redis·bootstrap
极客先躯13 小时前
高级java每日一道面试题-2024年11月02日-Redis篇-Redis6之后为什么开始支持多线程?
java·redis·多线程·优势·多核处理
水月梦镜花14 小时前
redis:RDB和AOF机制
数据库·redis·bootstrap
ktkiko1114 小时前
Redis中的缓存设计
数据库·redis·缓存
ascarl201020 小时前
系统启动时将自动加载环境变量,并后台启动 MinIO、Nacos 和 Redis 服务
数据库·redis·缓存
LightOfNight20 小时前
Redis设计与实现第9章 -- 数据库 总结(键空间 过期策略 过期键的影响)
数据库·redis·后端·缓存·中间件·架构
FIN技术铺1 天前
Redis集群模式之Redis Sentinel vs. Redis Cluster
数据库·redis·sentinel