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

news2024/11/16 20:38:26

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的使用啦,接下来咱们来一起看看实战篇我们要学习一些什么样的内容。

在这里插入图片描述

  • 短信登录

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

  • 商户查询缓存

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

  • 优惠卷秒杀

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

  • 附近的商户

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

  • UV统计

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

  • 用户签到

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

  • 好友关注

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

  • 达人探店

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

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

4.1短信登录

在这里插入图片描述

4.1.1. 搭建黑马点评项目

一、导入黑马点评项目

二、导入SQL

在这里插入图片描述
其中的表有:
●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对外提供更好的服务。
在这里插入图片描述

四、导入后端项目

在资料中提供了一个项目源码:
在这里插入图片描述
打开项目
在这里插入图片描述
设置编码
在这里插入图片描述
配置Maven
在这里插入图片描述
在这里插入图片描述
配置Maven的下载路径

-DarchetypeCatalog=internal

在这里插入图片描述
如果pom.xml中的2.3.12.RELEASE报红,可以采取这个方法
在这里插入图片描述
点击重启,即可
在这里插入图片描述

相关依赖

简单看一下pom.xml的依赖
在这里插入图片描述
在这里插入图片描述

配置redis和mysql连接

在这里插入图片描述

项目组成概述

在这里插入图片描述
在这里插入图片描述
打开service窗口
在这里插入图片描述
选择spring boot
在这里插入图片描述
在这里插入图片描述
点击运行,就可以启动该项目了。
在这里插入图片描述

项目启动成功
在这里插入图片描述

关闭Linux防火墙

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

查看防火墙状态
在这里插入图片描述

systemctl status firewalld 

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

systemctl stop firewalld.service

关闭开机自启防火墙

systemctl disable firewalld.service

此刻查看防火墙状态是
在这里插入图片描述
先关闭redis服务

systemctl stop redis

然后找到redis.conf关闭保护模式
在这里插入图片描述
找到95行,设置为no

protected-mode no 

在这里插入图片描述
重启redis服务

systemctl start redis

查看redis服务状态

systemctl status redis

登录:http://localhost:8081/shop-type/list可以查看相关数据
在这里插入图片描述
有数据的原因是后台ShopTypeController.java写好了逻辑地址
在这里插入图片描述

五、导入前端工程

在资料中提供了一个nginx文件夹
在这里插入图片描述

将其复制到任意目录,要确保该目录不包含中文、特殊字符和空格,例如:
在这里插入图片描述

六、 运行前端项目

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

start nginx.exe

在这里插入图片描述

打开chrome浏览器,在空白页面点击鼠标右键,选择检查,即可打开开发者工具:
在这里插入图片描述
切换为手机模式
在这里插入图片描述
选择具体手机型号
在这里插入图片描述

然后访问: http://127.0.0.1:8080,即可看到页面:
注意,此时是启动spring boot的,否则界面里没图片
未启动Spring Boot
在这里插入图片描述
启动Spring Boot
在这里插入图片描述

4.1.2. 基于Session实现登录流程

在这里插入图片描述

发送短信验证码:
在这里插入图片描述

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

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

短信验证码登录、注册:
在这里插入图片描述

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

校验登录状态:
在这里插入图片描述

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

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

页面流程

在这里插入图片描述
点击我的之后,点击发送验证码,报错,但是接收到了POST请求
在这里插入图片描述

具体代码如下
在这里插入图片描述

贴心小提示:

具体逻辑上文已经分析,我们仅仅只需要按照提示的逻辑写出代码即可。
修改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();
    }
}

注意代码写完之后,要重启之后才生效
在这里插入图片描述
重启后点击发送验证码,前台开发者工具-网络-预览 显示成功
在这里插入图片描述
再看一下IDEA的控制台
在这里插入图片描述

  • 登录

填写账号和密码,勾选已经阅读协议,发现报错
在这里插入图片描述
查看标头,请求URL中没有跟用户信息的参数
在这里插入图片描述
再去看负载,发现是json格式的
在这里插入图片描述
短信验证登录
在这里插入图片描述

看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接口的方法。
在这里插入图片描述
在这里插入图片描述

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> {

}

最后运行项目,点击登录
在这里插入图片描述
登录后,数据在mysql中插入成功,但是前台界面一闪而过
在这里插入图片描述
一闪而过的原因是还没有做登录校验。

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

在这里插入图片描述

温馨小贴士:tomcat的运行原理
在这里插入图片描述
当用户发起请求时,会访问我们像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就不一样,所以可以通过这种方式来做到线程隔离。
在这里插入图片描述
拦截器代码
新建LoginInterceptor.java
在这里插入图片描述

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
在这里插入图片描述
存储如下:
在这里插入图片描述
这里注意User.java类要简单修改,继承UserDTO,相当于进行了扩写。
这里用UserDTO的原因是,session中不用存储全部的用户信息
在这里插入图片描述

让拦截器生效
新建类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);
    }

此时我们运行测试类发现报错,类不兼容
在这里插入图片描述
明白了,我们在第一次UserServiceImpl.java
login方法需要修改成userDTO对象
在这里插入图片描述
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
在这里插入图片描述
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 去掉
在这里插入图片描述
配置完后重新启动,登录
在这里插入图片描述
跳转了主页
这里点击我的即可
在这里插入图片描述
补充以下,如果想跳转到和老师一样的个人详情页,需要更改前端代码。更改login.html
在这里插入图片描述
更改L87行
在这里插入图片描述

再看一下开发者工具中的数据
在这里插入图片描述

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,所以此时整个登录拦截功能就会出现问题。
在这里插入图片描述

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

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

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

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

在这里插入图片描述

4.1.4. Redis实现共享session

1.设计key的结构

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

在这里插入图片描述

2. 设计key的具体细节

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

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

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

如果我们采用phone:手机号来存储当然是可以的。
在这里插入图片描述
在这里插入图片描述

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

3. 整体访问流程

当注册完成后,用户去登录会去校验用户提交的手机号和验证码,是否一致,如果一致,则根据手机号查询用户信息,不存在则新建,最后将用户数据保存到redis,并且生成token作为redis的key,当我们校验用户是否登录时,会去携带着token进行访问,从redis中取出token对应的value,判断是否存在这个数据,如果没有则拦截,如果存在则将其保存到threadLocal中,并且放行。
在这里插入图片描述

4. 基于Redis实现短信登录

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

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

要修改的逻辑如下:

  1. 保存验证码到session → 保存验证码到redis(String)
  2. redis存储的时候,key是手机号
    在这里插入图片描述
    修改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方法
在这里插入图片描述
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);
    }

考虑一下有效期问题,于是我们增加代码
在这里插入图片描述
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,修改之前,解决一个问题。
在这里插入图片描述
更改MvcConfig.java
在这里插入图片描述
看前台代码login.html,引入common.js
在这里插入图片描述
common.js中前台通过拦截器拿到token进行保存
在这里插入图片描述
所以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();
    }
}

重启程序,点击发送验证码
在这里插入图片描述
验证码如下
在这里插入图片描述
看一下Redis的图形界面
在这里插入图片描述
直接登录的话,发现报错
在这里插入图片描述
网页控制台输出错误信息
在这里插入图片描述
IDEA控制台输出错误信息
在这里插入图片描述
原因很简单StringRedisTemplate要求键和值都是String,而UserDTO类中id是Long类型的,所以会有异常。
在这里插入图片描述
修改UserServiceImpl.java

// 将 UserDTO转换为Map方式一:用自定义转换
        //public static Map<String, Object> beanToMap(Object bean, Map<String, Object> 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<String, String> hashMap = new HashMap<>();
        hashMap.put("id", userDTO.getId().toString());
        hashMap.put("nickName", userDTO.getNickName());
        hashMap.put("icon", userDTO.getIcon());*/

如图所示
在这里插入图片描述
再重启然后登录,发现登录成功了
在这里插入图片描述
登录成功后查看Redis图形界面
在这里插入图片描述
查看前台控制台,确实携带了authorization
在这里插入图片描述
总结
Redis代替session需要考虑的问题:
◆选择合适的数据结构
◆选择合适的key
◆选择合适的存储粒度

4.1.5. Redis实现session的刷新问题

1. 初始方案思路总结:

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

2. 优化方案

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

  • 第一个拦截器的任务

  • (1). 拦截一切路径

  • (2). 刷新token

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

  • 第二个拦截器的任务

  • (1).获取ThreadLocal中的用户信息,用户存在就放行,不存在就拦截
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-816FBnkx-1676040926728)(.\Redis实战篇.assets\1653320764547.png)]

3. 代码

新建第一个拦截器,拦截所有路径。
在这里插入图片描述

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添加注解。
在这里插入图片描述
看一下执行顺序,先跑的Refresh拦截器,后跑的Login拦截器
在这里插入图片描述

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/339735.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

选购交换机的参数依据和主要的参数指标详解

如何选购交换机&#xff1f;用什么交换机&#xff1f;在选购交换机时交换机的优劣无疑十分的重要&#xff0c;而交换机的优劣要从总体构架、性能和功能三方面入手。交换机选购时。性能方面除了要满足RFC2544建议的基本标准&#xff0c;即吞吐量、时延、丢包率外&#xff0c;随着…

网络是怎么连接笔记(一)WEB浏览器

文章目录介绍生成HTTP请求消息向DNS服务器查询WEB服务的IP地址全世界DNS服务器的大接力委托协议栈发送消息介绍 互联网整个消息传递流程 生成HTTP请求消息向DNS服务器查询WEB服务的IP地址然后DNS服务器进行查询IP地址委托协议给对应IP发送消息 生成HTTP请求消息 整个网络发…

Spring面试重点(三)——AOP循环依赖

Spring面试重点 AOP 前置通知&#xff08;Before&#xff09;&#xff1a;在⽬标⽅法运行之前运行&#xff1b;后置通知&#xff08;After&#xff09;&#xff1a;在⽬标⽅法运行结束之后运行&#xff1b;返回通知&#xff08;AfterReturning&#xff09;&#xff1a;在⽬标…

2023年前端面试知识点总结(CSS篇)

近期整理了一下高频的前端面试题&#xff0c;分享给大家一起来学习。如有问题&#xff0c;欢迎指正&#xff01; 1. 对CSS盒模型的理解 CSS3的盒模型有两种盒子模型&#xff1a;标准盒子模型、IE盒子模型 盒模型都是由四个部分组成的&#xff0c;分别是content&#xff08;内容…

layui框架学习(6:基础菜单)

菜单是应用系统的必备元素&#xff0c;虽然网页中的导航也能作为菜单使用&#xff0c;但菜单和导航的样式和用途有所不同&#xff08;不同之处详见参考文献5&#xff09;。Layui中用不同的预设类定义菜单和导航的样式&#xff0c;同时二者依赖的模块也不一样。本文主要学习和记…

Vue (3)

文章目录1. 数据代理1.1 回顾1.2 开始2. 事件处理2.1 v-on:click 点击事件2.2 事件修饰符2.3 键盘事件3. 计算属性3.1 插值语法实现3.2 methods实现3.3 计算属性实现4. 监视属性4.1 深度监视4.2 监视属性的简写形式4.3 watch 与 computed 对比1. 数据代理 在学习 数据代理 时 先…

SQL数据查询——单表查询和排序

文章目录一、单表查询1.查询列1&#xff09;查询全部列指定列2&#xff09;查询经过计算的值3&#xff09;列的别名2.查询元组1&#xff09;消除取值重复的行(DISTINCT)2&#xff09;条件查询(WHERE)3.空值参与运算4.着重号二、排序(ORDER BY子句)一、单表查询 单表查询指仅涉及…

Webpack的知识要点

在前端开发中&#xff0c;一般情况下都使用 npm 和 webpack。   npm是一个非常流行的包管理工具&#xff0c;帮助开发者管理项目中使用的依赖库和工具。它可以方便地为项目安装第三方库&#xff0c;并在项目开发过程中进行版本控制。   webpack是一个模块打包工具&#xff…

C语言深度剖析之程序环境和预处理

1.程序的翻译环境和执行环境 第一种是翻译环境&#xff0c;在这个环境中源代码被转换为可执行的机器指令 第二种是执行环境&#xff0c;它用于实际执行代码 2.翻译环境 分为四个阶段 预编译阶段 &#xff0c;编译&#xff0c;汇编&#xff0c;链接 程序编译过程&#xff1a;多个…

使用vue3,vite,less,flask,python从零开始学习硅谷外卖(16-40集)

严正声明&#xff01; 重要的事情说一遍&#xff0c;本文章仅供分享&#xff0c;文章和代码都是开源的&#xff0c;严禁以此牟利&#xff0c;严禁侵犯尚硅谷原作视频的任何权益&#xff0c;我知道学习编程的人各种各样的心思都有&#xff0c;但这不是你对开源社区侵权的理由&am…

iptables防火墙之SNAT与DNAT

目录 1、SNAT策略概述 1.SNAT策略的典型应用环境 2.SNAT策略的原理 3.SNAT工作原理 4.SNAT转换前提条件 5.开启SNAT命令 6.SNAT转换 2.SNAT示例 1. 配置网关服务器 2.Xshell 连接192.168.100.100 3.DNAT策略及应用 1. DNAT策略概述 2.DNAT 策略的应用 3.DNAT转换前提条件…

看完这篇 教你玩转渗透测试靶机vulnhub——Hack Me Please: 1

Vulnhub靶机Hack Me Please: 1渗透测试详解Vulnhub靶机介绍&#xff1a;Vulnhub靶机下载&#xff1a;Vulnhub靶机安装&#xff1a;Vulnhub靶机漏洞详解&#xff1a;①&#xff1a;信息收集&#xff1a;②&#xff1a;漏洞利用③&#xff1a;获取反弹shell&#xff1a;④&#x…

how https works?https工作原理

简单一句话&#xff1a; https http TLShttps 工作原理&#xff1a;HTTPS (Hypertext Transfer Protocol Secure)是一种带有安全性的通信协议&#xff0c;用于在互联网上传输信息。它通过使用加密来保护数据的隐私和完整性。下面是 HTTPS 的工作原理&#xff1a;初始化安全会…

Camtasia2023最新版电脑视频录屏记录编辑软件

在Mac或Wind上有各种可用的视频记录和编辑软件&#xff0c;其中Camtasia被称为视频记录器和视频编辑器。录屏软件Camtasia2023到底有什么特色功能&#xff1f;本文将帮助您选择理想的选择来开始视频捕获&#xff0c;创建和编辑。Camtasia2023是Mac/win平台上一款使用非常简单的…

【JavaScript】题(牛客网)——熟练使用函数调用,超详细讲解

1 熟练使用函数调用 1.1 题目 执行以下程序&#xff0c;输出结果为 var uname "window"; var object {uname: "object",fun: function () {console.log(this.uname);return function () {console.log(this.uname);};}, };object.fun()();1.2 答案 ob…

ThingsBoard-设备配置

1、概述 从 ThingsBoard 3.2 开始,租户管理员可以使用设备配置文件为多个设备配置通用设置。每个设备在单个时间点都有一个且唯一的配置文件。 有经验的 ThingsBoard 用户会注意到设备类型已被弃用,取而代之的是设备配置文件。更新脚本将根据唯一的设备类型自动创建设备配置…

三、Java面向对象

1 . 方法 方法(method)是程序中最小的执行单元方法就是一些代码的打包 需要的时候可以直接调用方法之间是平级的关系 不能在方法里面定义方法方法不调用就不执行 方法的定义 // 方法的定义 /* [修饰符] 返回值类型 方法名称([参数 1],[参数 2]){语句A;return 返回值; } *///…

VT虚拟化框架编写

文章目录前言VT架构基础VT框架编写步骤一&#xff1a;检测VT是否开启VMM和VMVMON和VMCSVT框架编写步骤二 填充VMONVT框架编写步骤三 进入VTVT框架编写步骤四 初始化VMCSVT框架编写步骤五 初始化VMCS数据区VT框架编写步骤六 处理必要事件前言 学习VT相关的知识&#xff0c;需要…

C++11新特性

文章目录说在前面花括号{}初始化new的列表初始化STL相关容器的列表初始化相关语法格式容器列表初始化的底层原理forward_list和array与类型相关的新特性decltype左值引用和右值引用什么是左值&#xff0c;什么是右值左值和右值的本质区别右值引用如何理解右值引用std::move移动…

【软考系统架构设计师】2022下综合知识历年真题

【软考系统架构设计师】2022下综合知识历年真题 【2022下架构真题第01题&#xff1a;绿色】 01.云计算服务体系结构如下图所示&#xff0c;图中①、②、③分别与SaaS、PaaS、Iaas相对应&#xff0c;图中①、②、③应为( ) A.应用层、基础设施层、平台层 B.应用层、平台层、基础…