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有效期。

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

相关推荐
IT大白鼠16 小时前
Redis 系列 · 第 01 篇——认知入门:Redis 是什么
redis·nosql
彧azz16 小时前
Linux 环境下 Redis 学习总结:数据类型、持久化、锁、事务、主从与缓存问题
linux·redis·笔记·学习·面试
lbb 小魔仙18 小时前
OpenClaw + cpolar 实战:远程 NAS、分享小游戏、RDP,再配置公网 AI 入口
数据库·人工智能·redis·oracle·prometheus
夕除19 小时前
redis--018
redis
两锭子20 小时前
Redis基础
redis
烟沙九洲20 小时前
Redis 缓存穿透、缓存击穿、缓存雪崩
数据库·redis
字节探索20 小时前
Redis 只会当缓存用?这 10 大实战场景,让你的系统快到飞起
redis·后端
于樱花森上飞舞21 小时前
【Redis】哨兵详解
java·开发语言·数据库·redis
知无不研21 小时前
调用模型的API接口与Token详谈
ai·agent·token·api接口
Rain的Java大神之路1 天前
如何快速上传10G文件
java·spring boot·redis·后端·mysql·spring cloud·面试