当前位置: 首页 > news >正文

Redis实战篇Day01(短信登录篇)

前言:

在学习苍穹外卖时,我们引入了redis,了解了下它在Java中的应用。而从今天开始,我们将开始真正以项目的方式(黑马点评)深入学习redis,以及如何使用它完成各项接口功能

今日所学:

  • 导入黑马点评项目
  • session短信登录
  • redis代替session短信登录

目录

1. 导入黑马点评项目

1.1 导入数据库

1.2 导入项目代码

1.3 修改项目配置(端口)

1.4 导入前端代码(配置端口)

2. session短信登录

需求分析

代码实现

2.1 发送短信验证码

2.2 短信验证码登录以及注册

2.3 校验登录状态(拦截器)

思考

3. redis替代session进行短信登录

需求分析:

代码实现:

3.1 发送验证码代码修改

3.2 登录注册代码修改

3.3 校验登录状态(拦截器优化)

问题:

最后:


1. 导入黑马点评项目

1.1 导入数据库

打开黑马学习资料,在mysql中导入hmdp.sql文件

导入完后hmdp数据库一共有11个表

1.2 导入项目代码

复制hm-dianping文件夹,给他放在一个无中文的目录下,我这给它放置在D:/software文件下

使用你亲爱的ideal打开这个文件,打开项目结构,给你的JDK版本调低一点,这里调成18的即可

修改完不要忘了点击应用,再点击确定

之后我们可以使用maven编译(compile)下,正常不报错就是项目导入成功

1.3 修改项目配置(端口)

更改数据库和redis的连接配置

一般来说把这四样东西修改成自己的就行了(如果你没有修改redis端口的话)

运行项目,如果项目报这个错误

那就是你的server所需的8081端口被占用了

这里注意千万不要去随便修改server端口,比如说什么改成8082,这样后续前端界面是显示不出来的。

保持8081端口,打开你的cmd,输入以下指令,查看是哪个应用占据了你的8081端口(这里主要找到PID)

netstat -ano | findstr :"8081"

接着Ctril+shift+ESC打开你的任务管理器

输入相应的PID,找到该应用,右键点击结束任务

重新运行,后端项目是可以正常启动的。

这里可以打开你的浏览器,输入localhost:8080/shop-type/list,如果能正常返回结果,即代表着运行成功

1.4 导入前端代码(配置端口)

复制黑马学习资料的前端文件(nginx -1.18),同样给它放置在无中文结构的目录下,这里我依旧放在D:/software/)

打开该文件,双击启动(或者该目录下cmd中输入start nginx.exe)

打开浏览器,输入localhost:8080,正常就可以打开项目了(记得给后端项目打开)

如果打不开,可以看下是否是nginx端口冲突的问题

找到error.log文件

翻到最后,查看错误日志,可以看到输出了以下问题

扔给AI,说的是nginx端口冲突

打开cmd,输入指令查看该端口

在任务管理器中,输入PID

可以看到还是nginx,exe(我估摸着是苍穹外卖的nginx忘关了),右键点击结束任务。

重新启动项目

可以看到以成功输出出来了

2. session短信登录

需求分析

业务规则:

基于Session实现短信验证码登录,注册以及校验

逻辑流程图

代码实现

再讲代码实现之前,我们先讲下service层的这个继承关系。这个继承关系是Java中常见的Service层实现方式,结合了Mybatis-Plus框架的特性。其中ServiceImpI是Mybatis-plus提供的通用服务实现基类内置了大量常用的CRUD方法,通过继承他,就能轻易的去调用这些方法,不需要再格外写mapper层

再加上Controller层基本格式已经写好了(请求路径,请求方式),因此我们只需要关注于service层的代码逻辑实现就好了

2.1 发送短信验证码

具体逻辑思路(依照上面所展示的逻辑流程图):

1.对前端传来的手机号进行校验(使用正则表达式,已经封装到RegexUtils类中)

2.不合格直接返回校验信息

3.合格的话随机生成六位验证码

4.并将校验码保存到session中(方便后续的登录校验)【这里存储到redis不用管,是后面的内容,下面功能也一样】

5.控制台输出验证码

@Override
public Result sendCode(String phone, HttpSession session) {// 1.校验手机号if(RegexUtils.isPhoneInvalid(phone)){// 2.如果不符合,返回校验信息return Result.fail("手机号格式错误");}// 3. 符合,生成校验码String code = RandomUtil.randomNumbers(6);// 4.保存验证码到sessionsession.setAttribute("code", code);// 保存到redis// stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);// 5.发送验证码log.info("发送短信验证码成功,验证码;{}",code);// 6.返回okreturn Result.ok(code);
}

2.2 短信验证码登录以及注册

具体逻辑思路(依据上述逻辑流程图):

1.获得前端所传的手机号,进行格式校验

2.获取前端的验证码,和保存在session的验证码,进行equals()校验

3.如果手机号无问题,验证码一致的话,查询数据库,判断用户是否存在

这里直接调用继承于ServiceImpl类的查询方法,query()相当于是select * from user,eq()相当于where phone = ?, one()指的是查询一条记录。

4.如果用户不存在,则创建

5.最后添加用户信息到session(校验登录状态时要用)

@Override

    public Result login(LoginFormDTO loginForm, HttpSession session) {

        // 1.校验手机号

        String phone = loginForm.getPhone();

        if (RegexUtils.isPhoneInvalid(phone)) {

            // 2.如果不符合,返回错误信息

            return Result.fail("手机号格式错误!");

        }

        // 3.校验验证码

        Object cacheCode = session.getAttribute("code");

        String code = loginForm.getCode();

        if(cacheCode == null || !cacheCode.toString().equals(code)){

             //3.不一致,报错

            return Result.fail("验证码错误");

        }

        //一致,根据手机号查询用户

        User user = query().eq("phone", phone).one();

        //5.判断用户是否存在

        if(user == null){

            //不存在,则创建

            user =  createUserWithPhone(phone);

        }

        //7.保存用户信息到session中

        session.setAttribute("user",user);

        return Result.ok();

    }

2.3 校验登录状态(拦截器)

具体执行逻辑(依据逻辑流程图):

1.配置拦截器代码

  • 获取前端请求携带的session
  • 获取其中的用户
  • 判断用户是否存在,不存在返回401
  • 存在,则将用户信息保存在ThreadLocal,方便获取

2.配置拦截器,使其生效

  • 在MVCConfig类中,ait+insert重写addInterceptors()方法
  • excludePathPatterns排除一些不需要登录就能查看的功能
  • 其他功能查看前都要进行拦截做一个登录校验

拦截器代码

public class LoginInterceptor implements HandlerInterceptor {

    @Override

    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

       //1.获取session

        HttpSession session = request.getSession();

        //2.获取session中的用户

        Object user = session.getAttribute("user");

        //3.判断用户是否存在

        if(user == null){

              //4.不存在,拦截,返回401状态码

              response.setStatus(401);

              return false;

        }

        //5.存在,保存用户信息到Threadlocal

        UserHolder.saveUser((User)user);

        //6.放行

        return true;

    }

}

让拦截器生效

```java

@Configuration

public class MvcConfig implements WebMvcConfigurer {

    @Resource

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

    }

}

思考

和苍穹外卖登录校验的比较:

一个是将数据储存在JWT令牌中,一个是储存在session,除此之外,并无什么区别

3. redis替代session进行短信登录

需求分析:

集群的session具有数据共享问题

多台Tomcat并不共享session存储空间,当请求切换到不同tomcat服务时导致数据丢失的问题。

因此集群时我们需要使用另一种东西去替代session,这个东西应当具有

1.数据共享

2. 内存储存

3.key value结构

显而易见,redis集群很好的满足了这一点

代码实现:

3.1 发送验证码代码修改

基本逻辑保持不变,在保存验证码那给验证码保存到redis中就行,这里记得保存时设置有效期,实现一个数据自动清理的效果

@Override
public Result sendCode(String phone, HttpSession session) {// 1.校验手机号if(RegexUtils.isPhoneInvalid(phone)){// 2.如果不符合,返回校验信息return Result.fail("手机号格式错误");}// 3. 符合,生成校验码String code = RandomUtil.randomNumbers(6);// 4.保存验证码到session//  session.setAttribute("code", code);// 保存到redisstringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);
// 5.发送验证码log.info("发送短信验证码成功,验证码;{}",code);// 6.返回okreturn Result.ok(code);
}

3.2 登录注册代码修改

基本逻辑保持不变,主要的修改处有两点:

  • 从session中获取验证码改为从redis中获取验证码
  • 将数据保存在redis中,并给前端返回token

第二点修改中,我们使用hash结构去储存数据,随机生成一个token作为key值,将UserDTO作为一个(filed-value)值,那么为什么使用token作为key值呢,使用phone不行吗(也是唯一的),技术上是可以的,但是将phone这样一个敏感数据作为key值并返回给前端页面终究是不合适的

此外,在将UserDTO进行map转换的时候不要换了给value值从Long类型转换成string类型,不然在储存数据到redis时会有一个类型不兼容的问题

最后,不要忘了给储存数据设置有效期(跟JWT令牌的token是一样的),以便当用户超出一段时间没有访问网站时,会自动登出。后续要访问,得重新登录

@Override

public Result login(LoginFormDTO loginForm, HttpSession session) {

    // 1.校验手机号

    String phone = loginForm.getPhone();

    if (RegexUtils.isPhoneInvalid(phone)) {

        // 2.如果不符合,返回错误信息

        return Result.fail("手机号格式错误!");

    }

    // 3.从redis获取验证码并校验

    String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);

    String code = loginForm.getCode();

    if (cacheCode == null || !cacheCode.equals(code)) {

        // 不一致,报错

        return Result.fail("验证码错误");

    }

    // 4.一致,根据手机号查询用户 select * from tb_user where phone = ?

    User user = query().eq("phone", phone).one();

    // 5.判断用户是否存在

    if (user == null) {

        // 6.不存在,创建新用户并保存

        user = createUserWithPhone(phone);

    }

    // 7.保存用户信息到 redis中

    // 7.1.随机生成token,作为登录令牌

    String token = UUID.randomUUID().toString(true);

    // 7.2.将User对象转为HashMap存储

    UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);

    Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),

            CopyOptions.create()

                    .setIgnoreNullValue(true)

                    .setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));

    // 7.3.存储

    String tokenKey = LOGIN_USER_KEY + token;

    stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);

    // 7.4.设置token有效期

    stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);

    // 8.返回token

    return Result.ok(token);

}

3.3 校验登录状态(拦截器优化)

在上面我们讲了,设置token有效期时为了超出一段时间没有访问的话则自动登出。但是这里有个问题是我们并没有给他设置时间刷新机制,也就是说,不管用户有没有持续访问,时间到了都会自动登出,这显然是不行的。那么如何设置时间刷新机制呢,我们可以很自然想到拦截器,设置一个拦截器,对每个功能进行拦截,只要访问,就刷新token有效期。

这里我们重新定义一个拦截器(拦截所有功能),跟前面定义的登录拦截器区分开,在这个拦截器中,如果没有查到token或者用户,return true放行(说明是未登录下查看首页等功能,这个不用刷新有效期,也不用return 401啥的)。如果查到了用户,则刷新。最后将用户信息保存在ThreadLocal中

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.获取请求头中的token

        String token = request.getHeader("authorization");

        if (StrUtil.isBlank(token)) {

            return true;

        }

        // 2.基于TOKEN获取redis中的用户

        String key  = LOGIN_USER_KEY + token;

        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);

        // 3.判断用户是否存在

        if (userMap.isEmpty()) {

            return true;

        }

        // 5.将查询到的hash数据转为UserDTO

        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);

        // 6.存在,保存用户信息到 ThreadLocal

        UserHolder.saveUser(userDTO);

        // 7.刷新token有效期

        stringRedisTemplate.expire(key, LOGIN_USER_TTL, TimeUnit.MINUTES);

        // 8.放行

        return true;

    }

    @Override

    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

        // 移除用户

        UserHolder.removeUser();

    }

}

在原先的这个拦截器中,只要使用LocalThread尝试获取用户信息,如果为null,输出401

**LoginInterceptor**

public class LoginInterceptor implements HandlerInterceptor {

    @Override

    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        // 1.判断是否需要拦截(ThreadLocal中是否有用户)

        if (UserHolder.getUser() == null) {

            // 没有,需要拦截,设置状态码

            response.setStatus(401);

            // 拦截

            return false;

        }

        // 有用户,则放行

        return true;

    }

}

最后把拦截器配置改下

使用order配置拦截顺序(低值优先)

@Configuration
public class MvcConfig implements WebMvcConfigurer {@Resourceprivate StringRedisTemplate stringRedisTemplate;@Overridepublic void addInterceptors(InterceptorRegistry registry) {// 这里LoginInterceptor是手动new的,不受IOC容器去管理registry.addInterceptor(new LoginInterceptor()).excludePathPatterns("/shop/**","/voucher/**","/shop-type/**","/upload/**","/blog/hot","/user/code","/user/login").order(1);registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);}
}

问题:

在配置拦截器的时候,我遇到了一个空指针异常问题

原因是因为我给LoginInterceptor交给IOC容器去管理了

使用@Resource注入stringRedisTemplate,让它可以在config类中不用传stringredistemplate就可以实现拦截

但是这样是不行的,因为我们拦截器是new 出来的,并不受IOC容器去管理

最后:

今天的分享就到这里。如果我的内容对你有帮助,请点赞评论收藏。创作不易,大家的支持就是我坚持下去的动力!(๑`・ᴗ・´๑)

相关文章:

  • 谷歌medgemma-27b-text-it医疗大模型论文速读:多语言大型语言模型医学问答基准测试MedExpQA
  • PyTorch可视化工具——使用Visdom进行深度学习可视化
  • java 基础知识巩固
  • 论文阅读笔记——PixArt-α,PixArt-δ
  • [Harmony]网络请求
  • 【COMPUTEX 2025观察】NVIDIA开放NVLink:一场重构AI算力版图的“阳谋“
  • 应用案例 | 集成Docker,解锁 HMI/网关的定制化应用
  • 数据库基础面试题(回答思路和面试建议)
  • 力扣第450场周赛
  • upload-labs靶场通关详解:第14关
  • python:基础爬虫、搭建简易网站
  • Intel oneAPI 入门
  • 浙江大学python程序设计(陈春晖、翁恺、季江民)习题答案-第八章
  • RAG系统实战:文档切割与转换核心技术解析
  • Postgresql14+Repmgr部署
  • sentinel滑动时间窗口算法详解
  • 性能测试场景题
  • leetcode hot100刷题日记——10.螺旋矩阵
  • Day 0015:Metasploit 基础解析
  • ollama接口配合chrome插件实现商品标题和描述生成
  • 网站和web系统的区别/昆山网站建设
  • 网站建设公司主营业务/百度怎么推广
  • 网站建设要什么知识/seo点击软件手机
  • 网络营销的特点包括哪些/广州网站优化外包
  • 诗词门户网站/我想接app注册推广单
  • 全球网站域名/外包公司到底值不值得去