2022黑马Redis跟学笔记.实战篇(二)

2022黑马Redis跟学笔记.实战篇 二

  • 实战篇Redis
    • 开篇导读
    • 4.1短信登录
      • 4.1.1. 搭建黑马点评项目
        • 一、导入黑马点评项目
        • 二、导入SQL
        • 三、有关当前模型
        • 四、导入后端项目
          • 相关依赖
          • 配置redis和mysql连接
          • 项目组成概述
          • 关闭Linux防火墙
        • 五、导入前端工程
        • 六、 运行前端项目
      • 4.1.2. 基于Session实现登录流程
        • 1.实现发送短信验证码功能
        • 2. 实现登录拦截和校验功能
        • 3. 隐藏用户敏感信息
      • 4.1.3. session共享问题
        • 集群的session共享问题
      • 4.1.4. Redis实现共享session
        • 1.设计key的结构
        • 2. 设计key的具体细节
        • 3. 整体访问流程
        • 4. 基于Redis实现短信登录
          • (1).修改发送短信验证码
          • (2).修改短信验证码登录、注册
      • 4.1.5. Redis实现session的刷新问题
        • 1. 初始方案思路总结:
        • 2. 优化方案
        • 3. 代码

实战篇Redis

开篇导读

亲爱的小伙伴们大家好,马上咱们就开始实战篇的内容了,相信通过本章的学习,小伙伴们就能理解各种redis的使用啦,接下来咱们来一起看看实战篇我们要学习一些什么样的内容。

2022黑马Redis跟学笔记.实战篇(二)_第1张图片

  • 短信登录

这一块我们会使用redis共享session来实现。

  • 商户查询缓存

通过本章节,我们会理解缓存击穿,缓存穿透,缓存雪崩等问题,让小伙伴的对于这些概念的理解不仅仅是停留在概念上,更是能在代码中看到对应的内容。

  • 优惠卷秒杀

通过本章节,我们可以学会Redis的计数器功能, 结合Lua完成高性能的redis操作,同时学会Redis分布式锁的原理,包括Redis的三种消息队列。

  • 附近的商户

我们利用Redis的GEOHash来完成对于地理坐标的操作。

  • UV统计

主要是使用Redis来完成统计功能。

  • 用户签到

使用Redis的BitMap数据统计功能。

  • 好友关注

基于Set集合的关注、取消关注,共同关注等等功能,这一块知识咱们之前就讲过,这次我们在项目中来使用一下。

  • 达人探店

基于List来完成点赞列表的操作,同时基于SortedSet来完成点赞的排行榜功能。

以上这些内容咱们统统都会给小伙伴们讲解清楚,让大家充分理解如何使用Redis。

4.1短信登录

2022黑马Redis跟学笔记.实战篇(二)_第2张图片

4.1.1. 搭建黑马点评项目

一、导入黑马点评项目

二、导入SQL

2022黑马Redis跟学笔记.实战篇(二)_第3张图片
其中的表有:
●tb_user: 用户表
●tb_user_info: 用户详情表
●tb_shop:商户信息表
●tb_shop_ type: 商户类型表
●tb_blog: 用户日记表(达人探店日记)
●tb_follow: 用户关注表
●tb_voucher:优惠券表
●tb_voucher_order: 优惠券的订单表

三、有关当前模型

手机或者app端发起请求,请求我们的nginx服务器,nginx基于七层模型走的事HTTP协议,可以实现基于Lua直接绕开tomcat访问redis,也可以作为静态资源服务器,轻松扛下上万并发, 负载均衡到下游tomcat服务器,打散流量,我们都知道一台4核8G的tomcat,在优化和处理简单业务的加持下,大不了就处理1000左右的并发, 经过nginx的负载均衡分流后,利用集群支撑起整个项目,同时nginx在部署了前端项目后,更是可以做到动静分离,进一步降低tomcat服务的压力,这些功能都得靠nginx起作用,所以nginx是整个项目中重要的一环。

在tomcat支撑起并发流量后,我们如果让tomcat直接去访问Mysql,根据经验Mysql企业级服务器只要上点并发,一般是16或32 核心cpu,32 或64G内存,像企业级mysql加上固态硬盘能够支撑的并发,大概就是4000起~7000左右,上万并发, 瞬间就会让Mysql服务器的cpu,硬盘全部打满,容易崩溃,所以我们在高并发场景下,会选择使用mysql集群,同时为了进一步降低Mysql的压力,同时增加访问的性能,我们也会加入Redis,同时使用Redis集群使得Redis对外提供更好的服务。
2022黑马Redis跟学笔记.实战篇(二)_第4张图片

四、导入后端项目

在资料中提供了一个项目源码:
2022黑马Redis跟学笔记.实战篇(二)_第5张图片
打开项目
2022黑马Redis跟学笔记.实战篇(二)_第6张图片
设置编码
2022黑马Redis跟学笔记.实战篇(二)_第7张图片
配置Maven
2022黑马Redis跟学笔记.实战篇(二)_第8张图片
2022黑马Redis跟学笔记.实战篇(二)_第9张图片
配置Maven的下载路径

-DarchetypeCatalog=internal

2022黑马Redis跟学笔记.实战篇(二)_第10张图片
如果pom.xml中的2.3.12.RELEASE报红,可以采取这个方法
2022黑马Redis跟学笔记.实战篇(二)_第11张图片
点击重启,即可
2022黑马Redis跟学笔记.实战篇(二)_第12张图片

相关依赖

简单看一下pom.xml的依赖
2022黑马Redis跟学笔记.实战篇(二)_第13张图片
2022黑马Redis跟学笔记.实战篇(二)_第14张图片

配置redis和mysql连接

2022黑马Redis跟学笔记.实战篇(二)_第15张图片

项目组成概述

2022黑马Redis跟学笔记.实战篇(二)_第16张图片
2022黑马Redis跟学笔记.实战篇(二)_第17张图片
打开service窗口
2022黑马Redis跟学笔记.实战篇(二)_第18张图片
选择spring boot
2022黑马Redis跟学笔记.实战篇(二)_第19张图片
2022黑马Redis跟学笔记.实战篇(二)_第20张图片
点击运行,就可以启动该项目了。
2022黑马Redis跟学笔记.实战篇(二)_第21张图片

项目启动成功
2022黑马Redis跟学笔记.实战篇(二)_第22张图片

关闭Linux防火墙

如果是Linux上的Redis,那么还需要关闭防火墙
在Linux命令行中

查看防火墙状态
2022黑马Redis跟学笔记.实战篇(二)_第23张图片

systemctl status firewalld 

说明防火墙启动的,要关闭防火墙
关闭防火墙

systemctl stop firewalld.service

关闭开机自启防火墙

systemctl disable firewalld.service

此刻查看防火墙状态是
2022黑马Redis跟学笔记.实战篇(二)_第24张图片
先关闭redis服务

systemctl stop redis

然后找到redis.conf关闭保护模式
2022黑马Redis跟学笔记.实战篇(二)_第25张图片
找到95行,设置为no

protected-mode no 

2022黑马Redis跟学笔记.实战篇(二)_第26张图片
重启redis服务

systemctl start redis

查看redis服务状态

systemctl status redis

登录:http://localhost:8081/shop-type/list可以查看相关数据
在这里插入图片描述
有数据的原因是后台ShopTypeController.java写好了逻辑地址
2022黑马Redis跟学笔记.实战篇(二)_第27张图片

五、导入前端工程

在资料中提供了一个nginx文件夹
2022黑马Redis跟学笔记.实战篇(二)_第28张图片

将其复制到任意目录,要确保该目录不包含中文、特殊字符和空格,例如:
2022黑马Redis跟学笔记.实战篇(二)_第29张图片

六、 运行前端项目

在nginx所在目录下打开一个CMD窗口,输入命令:

start nginx.exe

2022黑马Redis跟学笔记.实战篇(二)_第30张图片

打开chrome浏览器,在空白页面点击鼠标右键,选择检查,即可打开开发者工具:
2022黑马Redis跟学笔记.实战篇(二)_第31张图片
切换为手机模式
2022黑马Redis跟学笔记.实战篇(二)_第32张图片
选择具体手机型号
2022黑马Redis跟学笔记.实战篇(二)_第33张图片

然后访问: http://127.0.0.1:8080,即可看到页面:
注意,此时是启动spring boot的,否则界面里没图片
未启动Spring Boot
2022黑马Redis跟学笔记.实战篇(二)_第34张图片
启动Spring Boot
2022黑马Redis跟学笔记.实战篇(二)_第35张图片

4.1.2. 基于Session实现登录流程

2022黑马Redis跟学笔记.实战篇(二)_第36张图片

发送短信验证码:
2022黑马Redis跟学笔记.实战篇(二)_第37张图片

用户在提交手机号后,会校验手机号是否合法,如果不合法,则要求用户重新输入手机号。

如果手机号合法,后台此时生成对应的验证码,同时将验证码进行保存,然后再通过短信的方式将验证码发送给用户。

短信验证码登录、注册:
2022黑马Redis跟学笔记.实战篇(二)_第38张图片

用户将验证码和手机号进行输入,后台从session中拿到当前验证码,然后和用户输入的验证码进行校验,如果不一致,则无法通过校验,如果一致,则后台根据手机号查询用户,如果用户不存在,则为用户创建账号信息,保存到数据库,无论是否存在,都会将用户信息保存到session中,方便后续获得当前登录信息。

校验登录状态:
2022黑马Redis跟学笔记.实战篇(二)_第39张图片

用户在请求时候,会从cookie中携带者JsessionId到后台,后台通过JsessionId从session中拿到用户信息,如果没有session信息,则进行拦截,如果有session信息,则将用户信息保存到threadLocal中,并且放行。

1.实现发送短信验证码功能

页面流程

2022黑马Redis跟学笔记.实战篇(二)_第40张图片
点击我的之后,点击发送验证码,报错,但是接收到了POST请求
2022黑马Redis跟学笔记.实战篇(二)_第41张图片

具体代码如下
2022黑马Redis跟学笔记.实战篇(二)_第42张图片

贴心小提示:

具体逻辑上文已经分析,我们仅仅只需要按照提示的逻辑写出代码即可。
修改UserController.java

  • 发送验证码

UserController.java

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

修改IUserService.java,添加

Result sendCode(String phone, HttpSession session);

UserServiceImpl.java,添加

@Slf4j
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {

    @Override
    public Result sendCode(String phone, HttpSession session) {
        // 1.校验手机号(是否符合手机号的规范)
        if (RegexUtils.isPhoneInvalid(phone)) {
            // 1.1 如果不符合,返回错误信息
            /**
             * 注意该方法是return !str.matches(regex);
             所以true是验证不通过
             */
            return Result.fail("手机号格式错误,请检查!");
        }

        // 1.2 如果符合,生成验证码(使用hutool提供的工具类)
        String code = RandomUtil.randomNumbers(6);

        // 2. 保存验证码到session
        session.setAttribute("code", code);

        // 3. 发送验证码
        log.debug("发送短信验证码成功,验证码是:" + code);

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

注意代码写完之后,要重启之后才生效
2022黑马Redis跟学笔记.实战篇(二)_第43张图片
重启后点击发送验证码,前台开发者工具-网络-预览 显示成功
2022黑马Redis跟学笔记.实战篇(二)_第44张图片
再看一下IDEA的控制台
在这里插入图片描述

  • 登录

填写账号和密码,勾选已经阅读协议,发现报错
2022黑马Redis跟学笔记.实战篇(二)_第45张图片
查看标头,请求URL中没有跟用户信息的参数
2022黑马Redis跟学笔记.实战篇(二)_第46张图片
再去看负载,发现是json格式的
2022黑马Redis跟学笔记.实战篇(二)_第47张图片
短信验证登录
2022黑马Redis跟学笔记.实战篇(二)_第48张图片

看UserController.java

        @PostMapping("/login")
    public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session) {
        // TODO 实现登录功能
        return userService.login(loginForm, session);
    }

修改IUserService.java,添加抽象方法

       Result login(LoginFormDTO loginForm, HttpSession session);

修改UserServiceImpl.java
这里注意,使用了两种方式二选一,Mybatisplus提供了mapper接口的方法和service接口的方法。
2022黑马Redis跟学笔记.实战篇(二)_第49张图片
2022黑马Redis跟学笔记.实战篇(二)_第50张图片

UserServiceImpl.java

@Slf4j
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
    @Autowired
    private UserMapper userMapper;

    @Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
        // 1. 校验手机号和验证码
        String phone = loginForm.getPhone();
        if (RegexUtils.isPhoneInvalid(phone)) {
            return Result.fail("手机号格式错误,请检查!");
        }

        // 2. 校验验证码是否正确
        Object o = session.getAttribute("code");
        String code_Session = (String) o;
        String code_loginForm = loginForm.getCode();

        // 2.1 验证码错误,报错
        if (null == code_Session || "".equals(code_Session)) {
            return Result.fail("验证码过期,请重新生成!");
        }
        if (null == code_loginForm || "".equals(code_loginForm)) {
            return Result.fail("验证码为空,请重新输入!");
        }

        if (!code_Session.equals(code_loginForm)) {
            // 验证码核验不一致,报错
            return Result.fail("验证码错误,请检查!");
        }

        // 2.2 验证码正确,根据手机号查询用户
        // SELECT * FROM comment.tb_user WHERE phone = ?

        // 以下方式一和方式二,二选一即可
        // 方式一:根据Mapper查询
        LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(User::getPhone, phone);
        User user = userMapper.selectOne(wrapper);
        // 方式二:在Service中查询
        //user = query().eq("phone",phone).one();

        // 3.判断用户是否存在
        if (null == user) {
            // 3.1 不存在就创建新用户并保存
            user = createUserWithPhone(phone);
        }

        // 4.保存用户到session
        session.setAttribute("user", user);
        return Result.ok();
    }

    /**
     * @param
     * @return void
     * @description //根据手机号创建用户并且保存
     * @param: phone
     * @date 2023/2/11 13:30
     * @author wty
     **/
    private User createUserWithPhone(String phone) {
        // 1.创建新用户
        User user = new User();
        user.setPhone(phone);
        // 随机生成的用户名:"user_" + 随机10位
        String nickName = SystemConstants.USER_NICK_NAME_PREFIX + RandomUtil.randomString(10);
        user.setNickName(nickName);
        // 2.保存用户 insert into tb_user values (?,?,?,?,?,?,?)
        // 以下方式二选一即可
        // 方式一: 用Mapper接口
        userMapper.insert(user);
        // 方式二: 用Service接口
        //save(user);
        return user;
    }
}

如果用Mapper接口的话,需要加上注解
UserMapper.java

@Mapper
public interface UserMapper extends BaseMapper<User> {

}

最后运行项目,点击登录
2022黑马Redis跟学笔记.实战篇(二)_第51张图片
登录后,数据在mysql中插入成功,但是前台界面一闪而过
2022黑马Redis跟学笔记.实战篇(二)_第52张图片
一闪而过的原因是还没有做登录校验。

2. 实现登录拦截和校验功能

2022黑马Redis跟学笔记.实战篇(二)_第53张图片

温馨小贴士:tomcat的运行原理
2022黑马Redis跟学笔记.实战篇(二)_第54张图片
当用户发起请求时,会访问我们像tomcat注册的端口,任何程序想要运行,都需要有一个线程对当前端口号进行监听,tomcat也不例外,当监听线程知道用户想要和tomcat进行连接时,会由监听线程创建socket连接,socket都是成对出现的,用户通过socket互相传递数据,当tomcat端的socket接受到数据后,此时监听线程会从tomcat的线程池中取出一个线程执行用户请求,在我们的服务部署到tomcat后,线程会找到用户想要访问的工程,然后用这个线程转发到工程中的controller,service,dao中,并且访问对应的DB,在用户执行完请求后,再统一返回,再找到tomcat端的socket,再将数据写回到用户端的socket,完成请求和响应。

通过以上讲解,我们可以得知每个用户其实对应都是去找tomcat线程池中的一个线程来完成工作的, 使用完成后再进行回收,既然每个请求都是独立的,所以在每个用户去访问我们的工程时,我们可以使用Threadlocal来做到线程隔离,每个线程操作自己的一份数据。

温馨小贴士:关于Threadlocal

如果小伙伴们看过ThreadLocal的源码,你会发现在ThreadLocal中,无论是他的put方法和他的get方法, 都是先从获得当前用户的线程,然后从线程中取出线程的成员变量map,只要线程不一样,map就不一样,所以可以通过这种方式来做到线程隔离。
2022黑马Redis跟学笔记.实战篇(二)_第55张图片
拦截器代码
新建LoginInterceptor.java
2022黑马Redis跟学笔记.实战篇(二)_第56张图片

LoginInterceptor.java

public class LoginInterceptor implements HandlerInterceptor {
	@Autowired
    private UserMapper userMapper;

    /**
     * @param
     * @return boolean
     * @description //前置拦截器
     * @param: request
     * @param: response
     * @param: handler
     * @date 2023/2/11 14:13
     * @author wty
     **/
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1.获取session
        HttpSession session = request.getSession();

        // 2.获取sessionh中的用户
        Object o = session.getAttribute("user");
        User user = (User) o;

        // 3.判断用户是否存在
        if (null == user) {
            // 3.2 不存在就拦截,返回状态码401(未授权)
            response.setStatus(401);
            return false;
        }
        // 3.1 存在就保存到ThreadLocal中
        UserHolder.saveUser(user);

        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 移除用户
        // 这里注意ThreadLocal中key是弱引用,可能被回收
        // 而value 是强引用不会被回收,所以user对象没被回收
        UserHolder.removeUser();
    }
}

这里保存user对象用到了Threadlocal
2022黑马Redis跟学笔记.实战篇(二)_第57张图片
存储如下:
2022黑马Redis跟学笔记.实战篇(二)_第58张图片
这里注意User.java类要简单修改,继承UserDTO,相当于进行了扩写。
这里用UserDTO的原因是,session中不用存储全部的用户信息
2022黑马Redis跟学笔记.实战篇(二)_第59张图片

让拦截器生效
新建类MvcConfig.java
注意: 这里放行的没有/user/me 如果加了请赶紧删掉,不然一点击登录就会跑到首页,再点击我的,又跑到登录上了。

@Configuration
public class MvcConfig implements WebMvcConfigurer {
    /**
     * @param
     * @return void
     * @description //添加拦截器
     * @param: registry
     * @date 2023/2/11 14:43
     * @author wty
     **/
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor())
                .excludePathPatterns(
                        // 以下几个都是放行的
                        "/user/code",
                        "/user/login",
                        "/blog/hot",
                        "/shop/**",
                        "/upload/**",
                        "/voucher/**",
                        "/shop-type/**"
                );// 通过排除一些不必要的路径,不用所有都拦截
    }
}

最后我们要让Controller获取到拦截器过滤后的结果。
修改UserController.java

@GetMapping("/me")
    public Result me() {
        // TODO 获取当前登录的用户并返回
        UserDTO user = UserHolder.getUser();
        return Result.ok(user);
    }

此时我们运行测试类发现报错,类不兼容
2022黑马Redis跟学笔记.实战篇(二)_第60张图片
明白了,我们在第一次UserServiceImpl.java
login方法需要修改成userDTO对象
2022黑马Redis跟学笔记.实战篇(二)_第61张图片
UserServiceImpl.java修改代码如下

    @Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
        // 1. 校验手机号和验证码
        String phone = loginForm.getPhone();
        if (RegexUtils.isPhoneInvalid(phone)) {
            return Result.fail("手机号格式错误,请检查!");
        }

        // 2. 校验验证码是否正确
        Object o = session.getAttribute("code");
        String code_Session = (String) o;
        String code_loginForm = loginForm.getCode();

        // 2.1 验证码错误,报错
        if (null == code_Session || "".equals(code_Session)) {
            return Result.fail("验证码过期,请重新生成!");
        }
        if (null == code_loginForm || "".equals(code_loginForm)) {
            return Result.fail("验证码为空,请重新输入!");
        }

        if (!code_Session.equals(code_loginForm)) {
            // 验证码核验不一致,报错
            return Result.fail("验证码错误,请检查!");
        }

        // 2.2 验证码正确,根据手机号查询用户
        // SELECT * FROM comment.tb_user WHERE phone = ?

        // 以下方式一和方式二,二选一即可
        // 方式一:根据Mapper查询
        LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(User::getPhone, phone);
        User user = userMapper.selectOne(wrapper);
        // 方式二:在Service中查询
        //user = query().eq("phone",phone).one();

        // 3.判断用户是否存在
        if (null == user) {
            // 3.1 不存在就创建新用户并保存
            user = createUserWithPhone(phone);
        }

        // 这里要注意session里不宜保存User的全部信息,容易泄密个人信息,这个放一部分即可
        // 需要把User转成UserDTO
        UserDTO userDTO = new UserDTO();
        userDTO = BeanUtil.copyProperties(user, UserDTO.class);

        // 4.保存用户到session
        session.setAttribute("user", userDTO);
        return Result.ok();
    }

LoginInterceptor.java也更改成UserDTO
2022黑马Redis跟学笔记.实战篇(二)_第62张图片
LoginInterceptor.java代码如下

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1.获取session
        HttpSession session = request.getSession();

        // 2.获取sessionh中的用户
        Object o = session.getAttribute("user");
        UserDTO user = (UserDTO) o;

        // 3.判断用户是否存在
        if (null == user) {
            // 3.2 不存在就拦截,返回状态码401(未授权)
            response.setStatus(401);
            return false;
        }
        // 3.1 存在就保存到ThreadLocal中
        UserHolder.saveUser(user);

        return true;
    }

修改User.java,把extends UserDTO 去掉
2022黑马Redis跟学笔记.实战篇(二)_第63张图片
配置完后重新启动,登录
2022黑马Redis跟学笔记.实战篇(二)_第64张图片
跳转了主页
这里点击我的即可
2022黑马Redis跟学笔记.实战篇(二)_第65张图片
补充以下,如果想跳转到和老师一样的个人详情页,需要更改前端代码。更改login.html
2022黑马Redis跟学笔记.实战篇(二)_第66张图片
更改L87行
2022黑马Redis跟学笔记.实战篇(二)_第67张图片

再看一下开发者工具中的数据
2022黑马Redis跟学笔记.实战篇(二)_第68张图片

3. 隐藏用户敏感信息

我们通过浏览器观察到此时用户的全部信息都在,这样极为不靠谱,所以我们应当在返回用户信息之前,将用户的敏感信息进行隐藏,采用的核心思路就是书写一个UserDto对象,这个UserDto对象就没有敏感信息了,我们在返回前,将有用户敏感信息的User对象转化成没有敏感信息的UserDto对象,那么就能够避免这个尴尬的问题了

在登录方法处修改
见上

在拦截器处:
见上

在UserHolder处:将user对象换成UserDTO
新版资料中已经更改了,无需修改
UserHolder.java

public class UserHolder {
    private static final ThreadLocal<UserDTO> tl = new ThreadLocal<>();

    public static void saveUser(UserDTO user){
        tl.set(user);
    }

    public static UserDTO getUser(){
        return tl.get();
    }

    public static void removeUser(){
        tl.remove();
    }
}

4.1.3. session共享问题

集群的session共享问题

核心思路分析:

每个tomcat中都有一份属于自己的session,假设用户第一次访问第一台tomcat,并且把自己的信息存放到第一台服务器的session中,但是第二次这个用户访问到了第二台tomcat,那么在第二台服务器上,肯定没有第一台服务器存放的session,所以此时整个登录拦截功能就会出现问题。
2022黑马Redis跟学笔记.实战篇(二)_第69张图片

我们能如何解决这个问题呢?
早期的方案是session拷贝,就是说虽然每个tomcat上都有不同的session,但是每当任意一台服务器的session修改时,都会同步给其他的Tomcat服务器的session,这样的话,就可以实现session的共享了。

但是这种方案具有两个大问题

1、每台服务器中都有完整的一份session数据,服务器压力过大。
2、session拷贝数据时,可能会出现延迟。

所以咱们后来采用的方案都是基于redis来完成,我们把session换成redis,redis数据本身就是共享的,就可以避免session共享的问题了

2022黑马Redis跟学笔记.实战篇(二)_第70张图片

4.1.4. Redis实现共享session

1.设计key的结构

首先我们要思考一下利用redis来存储数据,那么到底使用哪种结构呢?由于存入的数据比较简单,我们可以考虑使用String,或者是使用哈希,如下图,如果使用String,同学们注意他的value,用多占用一点空间,如果使用哈希,则他的value中只会存储他数据本身,如果不是特别在意内存,其实使用String就可以啦。

2022黑马Redis跟学笔记.实战篇(二)_第71张图片

2. 设计key的具体细节

所以保存验证码我们可以使用String结构,保存用户信息我们可以使用Hash,进行key,field,value的存取,但是关于key的处理,session他是每个用户都有自己的session,但是redis的key是共享的,咱们就不能使用code作为key了。

在设计这个key的时候,我们之前讲过需要满足两点

1、key要具有唯一性
2、key要方便携带

如果我们采用phone:手机号来存储当然是可以的。
2022黑马Redis跟学笔记.实战篇(二)_第72张图片
2022黑马Redis跟学笔记.实战篇(二)_第73张图片

但是如果把手机号这样的敏感数据存储到redis中并且从页面中带过来毕竟不太合适,所以我们在后台生成一个随机串token,然后让前端带来这个token就能完成我们的整体逻辑了。

3. 整体访问流程

当注册完成后,用户去登录会去校验用户提交的手机号和验证码,是否一致,如果一致,则根据手机号查询用户信息,不存在则新建,最后将用户数据保存到redis,并且生成token作为redis的key,当我们校验用户是否登录时,会去携带着token进行访问,从redis中取出token对应的value,判断是否存在这个数据,如果没有则拦截,如果存在则将其保存到threadLocal中,并且放行。
2022黑马Redis跟学笔记.实战篇(二)_第74张图片

4. 基于Redis实现短信登录

这里具体逻辑就不分析了,之前咱们已经重点分析过这个逻辑啦。

(1).修改发送短信验证码

要修改的逻辑如下:

  1. 保存验证码到session → 保存验证码到redis(String)
  2. redis存储的时候,key是手机号
    2022黑马Redis跟学笔记.实战篇(二)_第75张图片
    修改UserServiceImpl.java的sendCode(手机发送验证码方法)
 @Override
    public Result sendCode(String phone, HttpSession session) {
        // 1.校验手机号(是否符合手机号的规范)
        if (RegexUtils.isPhoneInvalid(phone)) {
            // 1.1 如果不符合,返回错误信息
            /**
             * 注意该方法是return !str.matches(regex);
             所以true是验证不通过
             */
            return Result.fail("手机号格式错误,请检查!");
        }

        // 1.2 如果符合,生成验证码(使用hutool提供的工具类)
        String code = RandomUtil.randomNumbers(6);

        // 2. 保存验证码到session → 保存验证码到redis 使用String的形式存取
        // 一般key都设置为  业务前缀:属性名:key  加以区分
		stringRedisTemplate.opsForValue().set(RedisConstants.LOGIN_CODE_KEY + phone, code);
        // 设置有效期,时间一到自动销毁,比如设置1分钟 最好用工具类提供的静态属性来定义数字和固定值
        // 方式一:set的重载方法
        //stringRedisTemplate.opsForValue().set(RedisConstants.LOGIN_CODE_KEY + phone, code, RedisConstants.LOGIN_CODE_TTL, TimeUnit.MINUTES);
        // stringRedisTemplate.expire("login:code" + phone, 1, TimeUnit.MINUTES);
        // 方式二: expire的重载方法
        stringRedisTemplate.expire(RedisConstants.LOGIN_CODE_KEY + phone, RedisConstants.LOGIN_CODE_TTL, TimeUnit.MINUTES);
        session.setAttribute("code", code);

        // 3. 发送验证码
        log.debug("发送短信验证码成功,验证码是:" + code);

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

RedisConstants.java增加常量

public class RedisConstants {
    public static final String LOGIN_CODE_KEY = "login:code:";
    public static final Long LOGIN_CODE_TTL = 1L;
}
(2).修改短信验证码登录、注册

紧接着修改UserServiceImpl.java的login方法
2022黑马Redis跟学笔记.实战篇(二)_第76张图片
UserServiceImpl.java

    @Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
        // 1. 校验手机号和验证码
        String phone = loginForm.getPhone();
        if (RegexUtils.isPhoneInvalid(phone)) {
            return Result.fail("手机号格式错误,请检查!");
        }

        // 2. 从session中获取校验验证码,并校验是否正确
        // TODO  从redis中获取校验验证码,并校验是否正确
        String code_Redis = stringRedisTemplate.opsForValue().get(RedisConstants.LOGIN_CODE_KEY + phone);
        //Object o = session.getAttribute("code");
        //String code_Session = (String) o;
        String code_loginForm = loginForm.getCode();

        // 2.1 验证码错误,报错
       /* if (null == code_Session || "".equals(code_Session)) {
            return Result.fail("验证码过期,请重新生成!");
        }*/
        if (null == code_Redis || "".equals(code_Redis)) {
            return Result.fail("验证码过期,请重新生成!");
        }

        if (null == code_loginForm || "".equals(code_loginForm)) {
            return Result.fail("验证码为空,请重新输入!");
        }

        /*if (!code_Session.equals(code_loginForm)) {
            // 验证码核验不一致,报错
            return Result.fail("验证码错误,请检查!");
        }*/
        if (!code_Redis.equals(code_loginForm)) {
            // 验证码核验不一致,报错
            return Result.fail("验证码错误,请检查!");
        }

        // 2.2 验证码正确,根据手机号查询用户
        // SELECT * FROM comment.tb_user WHERE phone = ?

        // 以下方式一和方式二,二选一即可
        // 方式一:根据Mapper查询
        LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(User::getPhone, phone);
        User user = userMapper.selectOne(wrapper);
        // 方式二:在Service中查询
        //user = query().eq("phone",phone).one();

        // 3.判断用户是否存在
        if (null == user) {
            // 3.1 不存在就创建新用户并保存
            user = createUserWithPhone(phone);
        }

        // 这里要注意session里不宜保存User的全部信息,容易泄密个人信息,这个放一部分即可
        // 需要把User转成UserDTO
        UserDTO userDTO = new UserDTO();
        userDTO = BeanUtil.copyProperties(user, UserDTO.class);

        // 4.保存用户到session → Redis
        //session.setAttribute("user", userDTO);

        // 5.随机生成token作为登录令牌
        String token = UUID.randomUUID().toString(true);

        // 将 UserDTO转换为Map
        Map<String, Object> map = BeanUtil.beanToMap(userDTO);
        // 6.将UserDTO的Map对象转为Hash存储
        // "login:token:" + token存储
        stringRedisTemplate.opsForHash().putAll(RedisConstants.LOGIN_USER_KEY + token, map);

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

考虑一下有效期问题,于是我们增加代码
2022黑马Redis跟学笔记.实战篇(二)_第77张图片
UserServiceImpl.java

 // 7.设置有效期30分钟:这个30分钟指的是从用户登录开始计算30分钟
        stringRedisTemplate.expire(RedisConstants.LOGIN_USER_KEY + token, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);

RedisConstants.java增加常量

public class RedisConstants {
    public static final String LOGIN_USER_KEY = "login:token:";
    public static final Long LOGIN_USER_TTL = 30L;
}

但是现在存在问题,目前有效期是指,从登录开始往后30分钟,就失效,这期间无论用户是登录还是未登录,之后都会失效,这明显是不对的,应该是用户下线后30分钟再失效。
我们的思路是,当我们登录触发登录校验的拦截器后,就会更新token的有效期。
修改LoginInterceptor.java,修改之前,解决一个问题。
2022黑马Redis跟学笔记.实战篇(二)_第78张图片
更改MvcConfig.java
2022黑马Redis跟学笔记.实战篇(二)_第79张图片
看前台代码login.html,引入common.js
2022黑马Redis跟学笔记.实战篇(二)_第80张图片
common.js中前台通过拦截器拿到token进行保存
2022黑马Redis跟学笔记.实战篇(二)_第81张图片
所以token在request请求的头部,名称是authorization

修改LoginInterceptor.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 → 获取请求头中的token
        HttpSession session = request.getSession();

        // 这里请求头的名称和common.js中 L10 一致
        String token = request.getHeader("authorization");

        // 判断token是否为空,时空就没必要取了
        if (StrUtil.isBlank(token)) {
            response.setStatus(401);
            return false;
        }

        // 2.获取sessionh中的用户 → 获取redis中token对应的用户
        //Object o = session.getAttribute("user");
        //UserDTO user = (UserDTO) o;

        Map<Object, Object> map = stringRedisTemplate.opsForHash().entries(RedisConstants.LOGIN_USER_KEY + token);

        // 3.判断用户是否存在
        // 如果是null,entries会返回空的map,所以判断是否为空即可
       /* if (null == user) {
            // 3.2 不存在就拦截,返回状态码401(未授权)
            response.setStatus(401);
            return false;
        }*/

        if (map.isEmpty()) {
            response.setStatus(401);
            return false;
        }


        // 将查询到的Hash数据转为UserDTO对象
        UserDTO userDTO = BeanUtil.fillBeanWithMap(map, new UserDTO(), false);


        // 3.1 存在就保存到ThreadLocal中
        UserHolder.saveUser(userDTO);

        // 4.刷新token有效期
        stringRedisTemplate.expire(RedisConstants.LOGIN_USER_KEY + token, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);

        // 5.放行
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 移除用户
        // 这里注意ThreadLocal中key是弱引用,可能被回收
        // 而value 是强引用不会被回收,所以user对象没被回收
        UserHolder.removeUser();
    }
}

重启程序,点击发送验证码
2022黑马Redis跟学笔记.实战篇(二)_第82张图片
验证码如下
在这里插入图片描述
看一下Redis的图形界面
2022黑马Redis跟学笔记.实战篇(二)_第83张图片
直接登录的话,发现报错
2022黑马Redis跟学笔记.实战篇(二)_第84张图片
网页控制台输出错误信息
2022黑马Redis跟学笔记.实战篇(二)_第85张图片
IDEA控制台输出错误信息
2022黑马Redis跟学笔记.实战篇(二)_第86张图片
原因很简单StringRedisTemplate要求键和值都是String,而UserDTO类中id是Long类型的,所以会有异常。
2022黑马Redis跟学笔记.实战篇(二)_第87张图片
修改UserServiceImpl.java

// 将 UserDTO转换为Map方式一:用自定义转换
        //public static Map beanToMap(Object bean, Map targetMap, CopyOptions copyOptions){}
        // setIgnoreNullValue忽略空值
        // setFieldValueEditor函数式接口
        Map<String, Object> map = BeanUtil.beanToMap(userDTO, new HashMap<>(), CopyOptions.create().setIgnoreNullValue(true)
                .setFieldValueEditor((fileName, fileValue) -> fileValue.toString()));

        // 方式二:自己创建map然后put
        /*HashMap hashMap = new HashMap<>();
        hashMap.put("id", userDTO.getId().toString());
        hashMap.put("nickName", userDTO.getNickName());
        hashMap.put("icon", userDTO.getIcon());*/

如图所示
2022黑马Redis跟学笔记.实战篇(二)_第88张图片
再重启然后登录,发现登录成功了
2022黑马Redis跟学笔记.实战篇(二)_第89张图片
登录成功后查看Redis图形界面
2022黑马Redis跟学笔记.实战篇(二)_第90张图片
查看前台控制台,确实携带了authorization
2022黑马Redis跟学笔记.实战篇(二)_第91张图片
总结
Redis代替session需要考虑的问题:
◆选择合适的数据结构
◆选择合适的key
◆选择合适的存储粒度

4.1.5. Redis实现session的刷新问题

1. 初始方案思路总结:

在这个方案中,他确实可以使用对应路径的拦截,同时刷新登录token令牌的存活时间,但是现在这个拦截器他只是拦截需要被拦截的路径,假设当前用户访问了一些不需要拦截的路径,那么这个拦截器就不会生效(比如放行列表中的这些)。
在这里插入图片描述
所以此时token令牌刷新的动作实际上就不会执行,所以这个方案他是存在问题的。
2022黑马Redis跟学笔记.实战篇(二)_第92张图片

2. 优化方案

既然之前的拦截器无法对不需要拦截的路径生效,那么我们可以添加一个拦截器,在第一个拦截器中拦截所有的路径,把第二个拦截器做的事情放入到第一个拦截器中,同时刷新token,因为第一个拦截器有了threadLocal的数据,所以此时第二个拦截器只需要判断拦截器中的user对象是否存在即可,完成整体刷新功能。

  • 第一个拦截器的任务

  • (1). 拦截一切路径

  • (2). 刷新token

  • (3).查询Redis的用户信息,能查询到就放到ThreadLocal中,查询不到就放行,让下一个拦截器处理。

  • 第二个拦截器的任务

  • (1).获取ThreadLocal中的用户信息,用户存在就放行,不存在就拦截
    2022黑马Redis跟学笔记.实战篇(二)_第93张图片

3. 代码

新建第一个拦截器,拦截所有路径。
2022黑马Redis跟学笔记.实战篇(二)_第94张图片

RefreshTokenInterceptor.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 → 获取请求头中的token
        HttpSession session = request.getSession();

        // 这里请求头的名称和common.js中 L10 一致
        String token = request.getHeader("authorization");

        // 判断token是否为空,是空就直接放行即可
        if (StrUtil.isBlank(token)) {
            //response.setStatus(401);
            return true;
        }

        // 2.获取sessionh中的用户 → 获取redis中token对应的用户
        //Object o = session.getAttribute("user");
        //UserDTO user = (UserDTO) o;

        Map<Object, Object> map = stringRedisTemplate.opsForHash().entries(RedisConstants.LOGIN_USER_KEY + token);

        // 3.判断用户是否存在
        // 如果是null,entries会返回空的map,所以判断是否为空即可,空就放行即可
       /* if (null == user) {
            // 3.2 不存在就拦截,返回状态码401(未授权)
            response.setStatus(401);
            return false;
        }*/

        if (map.isEmpty()) {
            //response.setStatus(401);
            return true;
        }


        // 将查询到的Hash数据转为UserDTO对象
        UserDTO userDTO = BeanUtil.fillBeanWithMap(map, new UserDTO(), false);


        // 3.1 存在就保存到ThreadLocal中
        UserHolder.saveUser(userDTO);

        // 4.刷新token有效期
        stringRedisTemplate.expire(RedisConstants.LOGIN_USER_KEY + token, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);

        // 5.放行
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 移除用户
        // 这里注意ThreadLocal中key是弱引用,可能被回收
        // 而value 是强引用不会被回收,所以user对象没被回收
        UserHolder.removeUser();
    }
}	

LoginInterceptor.java

public class LoginInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1. 判断是否需要拦截(依据ThreadLocal中是否有用户,如果没有就拦截,有就放行)
        UserDTO userDTO = UserHolder.getUser();
        if (null == userDTO) {
            response.setStatus(401);
            return false;
        }
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 移除用户
        // 这里注意ThreadLocal中key是弱引用,可能被回收
        // 而value 是强引用不会被回收,所以user对象没被回收
        UserHolder.removeUser();
    }
}

修改 MvcConfig.java

@Configuration
public class MvcConfig implements WebMvcConfigurer {
    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 第1个拦截器,用来拦截所有
        registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate));

        // 第2个拦截器,用来判断ThreadLocal中是否有UserDTO对象,有就放行
        registry.addInterceptor(new LoginInterceptor())
                .excludePathPatterns(
                        // 以下几个都是放行的
                        "/user/code",
                        "/user/login",
                        "/blog/hot",
                        "/shop/**",
                        "/upload/**",
                        "/voucher/**",
                        "/shop-type/**"
                );// 通过排除一些不必要的路径,不用所有都拦截
    }
}

这样写并不能保证拦截器的执行顺序,用到注解,给第一个拦截器RefreshTokenInterceptor添加注解。
2022黑马Redis跟学笔记.实战篇(二)_第95张图片
看一下执行顺序,先跑的Refresh拦截器,后跑的Login拦截器
2022黑马Redis跟学笔记.实战篇(二)_第96张图片

最后测试一下
等待一会儿点击我的,会重置token的TTL,点击首页也会重置token
在这里插入图片描述

你可能感兴趣的:(redis,redis,短信验证)