Redis替代Session实现用户短信登录(超级详细解释)

 我们可以对比之间学过的Session实现短信登录学习

基于Session实现短信登录_我爱布朗熊的博客-CSDN博客这个地方为什么需要session?因为我们需要把验证码保存在session当中。https://blog.csdn.net/weixin_51351637/article/details/127519489?spm=1001.2014.3001.5501

目录

一、 基于Redis实现用户短信登录

二、代码编写

2.1 发送短信验证码

2.1.1 业务流程图:

2.1.2 Controller层代码

2.1.3 Service层代码

2.1.4  发送验证码演示

  2.2 短信验证码登录、注册

2.2.1 业务流程图

2.2.2  Controller层

2.2.3 Service层代码

2.2.4  拦截器的注册器             

        至于为什么在这里注入   StringRedisTemplate   ,在2.2.5我们会进行讲解

2.2.5自定义拦截器更新token有效期(注入对象有点巧妙)

2.2.6 最终结果演示


一、 基于Redis实现用户短信登录

 

 那么接下来我们就有这么一个疑问:

         以后我们的每个请求都要携带Token,它是怎么做到这一点的呢?

我们可以看一下下面这段代码,其中{data}就是后端传输过来的token信息,并将其存储到浏览器中的本地存储当中(关闭再打开就没有了)

那我们接下来就看看到底在请求的时候怎么携带Token:  给axios设置了一个拦截器,将Token作为请求头放进去了,名字叫“authorization”,这样就能确保每次axios发起请求都会携带“authorization”这个头(也就是token),这样一来在服务端我们就能获取到这个请求到拿到token从而实现登录的严重

这也是为什么我们token的值不选择手机号当做key,如果是选择手机号当做key很可能会发生信息泄露(因为如果token是手机号,最终token是要保存在前端浏览器的本地存储当中的)

如果有学过Vue并且感兴趣的小伙伴可以看一下下面这个登录表单验证,这里面从标题六及其以后都会有涉及到Token的应用,可以直接看

Element-UI+vue实现登录表单_我爱布朗熊的博客-CSDN博客_vue登录表单

二、代码编写

我们之前是将短信验证码保存到了session,现在做出改变,放入到Redis之中

   2.1 发送短信验证码

      2.1.1 业务流程图:

   2.1.2 Controller层代码

相比于session方式,这个地方并没有修改

    /**
     * 发送手机验证码
     */
    @PostMapping("code")
    public Result sendCode(@RequestParam("phone") String phone,HttpSession session) {
        // TODO 发送短信验证码并保存验证码、
   
//        return Result.fail("功能未完成");
        return userService.sendCode(phone,session);
    }

  2.1.3 Service层代码

     相对于session方式,在存储code的时候有所不同,以前是存储到session,现在是存储到Redis

@Autowired 与@Resource的区别(通俗易懂)_行者彡的博客-CSDN博客_@resource和@autowired的区别@Autowired 与@Resource的区别:1、@Autowired与@Resource都可以用来装配bean. 都可以写在字段上,或写在setter方法上。2、@Autowired默认按类型装配(这个注解是属业spring的),默认情况下必须要求依赖对象必须存在,如果要允许null值,可以设置它的required属性为false,如:@Autowired(required=false) ,如果我们想使用名称装配可以结合@Qualifier注解进行使用3、@Resource(这个...https://blog.csdn.net/sinat_21843047/article/details/108639610?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522166704547616800180627901%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=166704547616800180627901&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~top_positive~default-1-108639610-null-null.142%5Ev62%5Ejs_top,201%5Ev3%5Eadd_ask,213%5Ev1%5Et3_esquery_v3&utm_term=%40resource%E5%92%8C%40autowire%E7%9A%84%E5%8C%BA%E5%88%AB&spm=1018.2226.3001.4187

     首先我们注入StringRedisTemplate对象,至于为什么用@Resource注解的,可以看上面这个博主的文章,在此提出感谢

    @Resource
    private StringRedisTemplate stringRedisTemplate;

     如果有朋友对StringRedisTemplate对象不熟悉的话,可以看下面这个博客

Redis的Java客户端——SpringDataRedis、RedisTemplate、StringRedisTemplate_我爱布朗熊的博客-CSDN博客

    /**
     * 发送手机验证码
     * @param phone   电话号码
     * @param session
     * @return
     */
    @Override
    public Result sendCode(String phone, HttpSession session) {
//      1.校验手机号
        if(RegexUtils.isPhoneInvalid(phone)){
//              说明:RegexUtils使我们封装的一个类   isCodeInvalid是里面的静态方法,在这个静态方法里面又调用了另外一个静态方法得以实现
//      2.如果不符合,返回错误信息
            return Result.fail("手机号格式错误");
        }

//      3.符合,生成验证码    6代表生成的验证码的长度  RandomUtil使用这个工具类生成
        String code =  RandomUtil.randomNumbers(6);
//      4.保存验证码到Redis  在采用Redis中采用String的数据结构  "login:code:"+phone这样更加有层次感
//              2, TimeUnit.MINUTES有效期是两分钟  类似set key value ex(有效期,单位是秒)
        stringRedisTemplate.opsForValue().set("login:code:"+phone,code,2, TimeUnit.MINUTES);

        //5.发送验证码
//        实现起来比较麻烦 我们使用日志假装发送
        log.debug("发送短信验证码成功,验证码:"+code);
        return Result.ok();
    }

如果大家有对Redis命令有所忘记的话,可以看下面这一篇文章

Redis命令——通用命令、String类型、Key层级结构、Hash类型、List类型、Set类型、SortedSet类型_我爱布朗熊的博客-CSDN博客

2.1.4  发送验证码演示

  2.2 短信验证码登录、注册

    2.2.1 业务流程图

  

   2.2.2  Controller层

  与session并没有区别

    /**
     * 登录功能
     * @param loginForm 登录参数,包含手机号、验证码;或者手机号、密码
     */
    @PostMapping("/login")
    public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){
        // TODO 实现登录功能
        return userService.login(loginForm,session);
    }

   

   2.2.3 Service层代码

 这个地方就比较复杂,改动的地方也比较多

    /**
     * 实现用户登录
     * @param loginForm  登录的参数
     * @param session
     * @return
     */
    @Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
// 1.校验手机号
        if(RegexUtils.isPhoneInvalid(loginForm.getPhone())){
//              说明:RegexUtils使我们封装的一个类   isCodeInvalid是里面的静态方法,在这个静态方法里面又调用了另外一个静态方法得以实现
//         1.2.如果不符合,返回错误信息
            return Result.fail("手机号格式错误");
        }
// 2.校验验证码
//      2.1 从Redis中获取真正正确的验证码
        String cacheCode = stringRedisTemplate.opsForValue().get("login:code:"+loginForm.getPhone());
//      2.2 获取用户输入的code
        String code = loginForm.getCode();
        if(cacheCode ==null || !cacheCode.equals(code)){
//3.不一致,报错
            return Result.fail("验证码错误");
        }

//4.一致,根据手机号查询用户  .one()代表查询一个  list()代表着查询多个
        User user =query().eq("phone",loginForm.getPhone()).one();

//5.判断用户是否存在
        if(user ==null){
//6.不存在,创建新用户并保存
             user = createUserWithPhone(loginForm.getPhone());
        }

//7.保存用户信息到Redis中
//      7.1 随机生成Token,作为登录令牌  我们使用import cn.hutool.core.lang.UUID;
//             true代表着不加中划线
        String token =  UUID.randomUUID().toString(true);
//      7.2 将User对象转为HashMap存储
//             BeanUtil.copyProperties(user, UserDTO.class))  会自动的将user中的属性拷贝到UserDTO当中而且也创建出一个UserDTO对象
//             将User转化为UserDTO是为了提高数据的保密性,User中有各种信息都会返回到前端,这样很不友好
        UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
//             转化为Map集合的时候,确保每一个都是String,否则就会出现异常!!!!!!!
//        new HashMap<>() 是一个空的map集合
//        setIgnoreNullValue(true) 忽略空值   setFieldValueEditor这个就是我们所需要的设置字段值,把每一个字段值都设置成Value属性
        Map<String, Object> userMap = BeanUtil.beanToMap(userDTO,new HashMap<>(),
                                                    CopyOptions.create().setIgnoreNullValue(true)
                                                                        .setFieldValueEditor((fieldName,fieldValue)->fieldValue.toString()));
//      7.3 存储并设置有效期(session有效期是30分钟,超过30分钟不访问便被剔除,这里我们也可以设置30分钟)
//          put是一个一个的加不是很适合,这里使用putAll,一次性存入
        stringRedisTemplate.opsForHash().putAll("login:token:"+token,userMap);
//          这个地方存储的时候是不能设置有效期的,我们只能先存储再设置有效期
//      7.4  设置有效期(但是这个地方是到了30分钟就剔除,但是session不是这个样子的,剩下的操作我们需要在拦截器中设置)
        stringRedisTemplate.expire("login:token:"+token,30,TimeUnit.MINUTES);

//      8. 返回token
        return Result.ok(token);
    }

     

2.2.4  拦截器的注册器             

  至于为什么在这里注入   StringRedisTemplate   ,在2.2.5我们会进行讲解

@Configuration
public class MvcConfig implements WebMvcConfigurer {

    @Resource
    private StringRedisTemplate stringRedisTemplate;
//  拦截器的注册器
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor( stringRedisTemplate))
                .excludePathPatterns(
                        "/user/code",
                        "/user/login",
                        "/shop/**",
                        "/blog/hot",
                        "/shop-type/**",
                        "upload/**",
                        "voucher/**"
                );
    }
}

 2.2.5自定义拦截器更新token有效期(注入对象有点巧妙)

 当我们将数据放入token的时候是30分钟的有效期,也就是说当我们把数据放入Redis就开始30分钟的倒计时,这样是非常不合理的。加入用户正在操作着,突然没有访问权限了

  所有我们要设置一下拦截器,当用户还在操作的时候,就会更新token的有效期

  当顾客三十分钟没有操作之后才将token剔除

//          HandlerInterceptor 这是拦截器
public class LoginInterceptor implements HandlerInterceptor {

//  这个类LoginInterceptor 我们并没有给他添加注解,所以这个类并没有交给Spring管理,所以这个地方也没有自动装配的注解
//  这个类的对象使我们自己new出来的(不是Spring帮我们创建的,所以我们不能使用那些注解了)
    private StringRedisTemplate stringRedisTemplate;
//  所以我们使用构造函数注入  很重要哦
//      那我们利用构造函数注入,那谁帮我们注入呢?谁用了它,谁就帮我们注入
    public LoginInterceptor(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    //  前置拦截   在进入controller之前我们进行登录校验
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//      1.获取请求头中的token     authorization使我们前端定义的一个请求头
        String token = request.getHeader("authorization");
        if(StrUtil.isBlank(token)){
//            运行到这里说明token是空
            //不存在,拦截
            response.setStatus(401);  //返回401状态码
            return false;
        }
//      2.基于Token获取Redis中的用户 这里不用get,get只能获取一个字段的值
//                 entries返回值是一个map
        Map<Object,Object> userMap= stringRedisTemplate.opsForHash()
                                    .entries("login:token:"+token);

//      3.判断用户是否存在
//         这个地方不用担心空指针异常,因为如果是空的话entries返回的就是一个什么也没有的map集合而已
        if(userMap.isEmpty()){
            // 4.不存在,拦截
            response.setStatus(401);  //返回401状态码
            return false;
        }
//      5.将查询到的hash数据转化为UserDTO对象
//            false 表示不忽略转换过程中的错误
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap,new UserDTO(),false);
//      6.存在,保存用户信息到ThreadLocal  保存在当前线程里面的
        UserHolder.saveUser(userDTO);
//      7.刷新token有效期
        stringRedisTemplate.expire("login:token:"+token,30, TimeUnit.MINUTES);
//      8.放行
        return true;
    }
//  在controller执行之后拦截  这个我们在这里不需要
//    @Override
//    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
//        HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
//    }

//  渲染之后,返回给用户之前   用户业务执行完毕我们要销毁维护信息,避免泄露
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//      移除用户
        UserHolder.removeUser();
    }
}

2.2.6 最终结果演示

验证码:

token令牌:存储的是User对象的部分内容(存储部分主要内容省内存空间)

我们也可以看一下前端的请求Header

携带了authorization,这样后台才能验证token,很完美!

三、解决状态登录刷新的问题

这是我们之前所用到的登陆拦截器的样子:

但是上面那个不是很友好,加入我们的用户在半个小时内在操作,但是并没有访问需要登录的路径,那这样一来我们半个小时后还是token过期,这样显然是不符合要求的

接下来我们就进行优化,我们在原先拦截器的基础上再增加一个拦截器,目的是拦截一切请求,如下图所示:

3.1  新增RefreshTokenInterceptor拦截器实现大部分功能

//          HandlerInterceptor 这是拦截器
public class RefreshTokenInterceptor implements HandlerInterceptor {

//  这个类LoginInterceptor 我们并没有给他添加注解,所以这个类并没有交给Spring管理,所以这个地方也没有自动装配的注解
//  这个类的对象使我们自己new出来的(不是Spring帮我们创建的,所以我们不能使用那些注解了)
    private StringRedisTemplate stringRedisTemplate;
//  所以我们使用构造函数注入  很重要哦
//      那我们利用构造函数注入,那谁帮我们注入呢?谁用了它,谁就帮我们注入
    public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    //  前置拦截   在进入controller之前我们进行登录校验
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//      1.获取请求头中的token     authorization使我们前端定义的一个请求头
        String token = request.getHeader("authorization");
        if(StrUtil.isBlank(token)){
//            不拦截,直接放行
            return true;
        }
//      2.基于Token获取Redis中的用户 这里不用get,get只能获取一个字段的值
//                 entries返回值是一个map
        Map<Object,Object> userMap= stringRedisTemplate.opsForHash()
                                    .entries("login:token:"+token);

//      3.判断用户是否存在
//         这个地方不用担心空指针异常,因为如果是空的话entries返回的就是一个什么也没有的map集合而已
        if(userMap.isEmpty()){
//          不拦截,直接放行
            return true;
        }
//      5.将查询到的hash数据转化为UserDTO对象
//            false 表示不忽略转换过程中的错误
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap,new UserDTO(),false);
//      6.存在,保存用户信息到ThreadLocal  保存在当前线程里面的
        UserHolder.saveUser(userDTO);
//      7.刷新token有效期
        stringRedisTemplate.expire("login:token:"+token,30, TimeUnit.MINUTES);
//      8.放行
        return true;
    }
//  在controller执行之后拦截  这个我们在这里不需要
//    @Override
//    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
//        HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
//    }

//  渲染之后,返回给用户之前   用户业务执行完毕我们要销毁维护信息,避免泄露
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//      移除用户
        UserHolder.removeUser();
    }
}

3.2 修改LoginInterceptor拦截器

此时这个类只用来判断是否有用户

//          HandlerInterceptor 这是拦截器
public class LoginInterceptor implements HandlerInterceptor {

    //  前置拦截   在进入controller之前我们进行登录校验
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//      判断是否需要拦截(ThreadLocal中是否有用户)
        if(UserHolder.getUser() ==null){
//            没有的话进行拦截
            response.setStatus(401);
            return  false;
        }
//      有用户则放行
        return true;
    }
//  在controller执行之后拦截  这个我们在这里不需要
//    @Override
//    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
//        HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
//    }

//  渲染之后,返回给用户之前   用户业务执行完毕我们要销毁维护信息,避免泄露
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//      移除用户
        UserHolder.removeUser();
    }
}

3.3 配置两个拦截器


@Configuration
public class MvcConfig implements WebMvcConfigurer {

    @Resource
    private StringRedisTemplate stringRedisTemplate;
//  拦截器的注册器
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
//     登录拦截器
        registry.addInterceptor(new LoginInterceptor( ))
                .excludePathPatterns(
                        "/user/code",
                        "/user/login",
                        "/shop/**",
                        "/blog/hot",
                        "/shop-type/**",
                        "upload/**",
                        "voucher/**"
                ).order(1);
//      刷新token拦截器  
//      registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate));  这两种方式都是可以的
//      这个拦截器需要先执行  .order(0) 就说明是先执行,从0开始,默认就是0  如果都没有写order的话,就按照添加拦截器的顺序执行拦截器
        registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);
    }
}

  • 4
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我爱布朗熊

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值