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

实战篇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
在这里插入图片描述

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
Redis全套学习笔记.pdf》是一本关于Redis数据库的学习笔记,内容涵盖了Redis的基本概念、原理、操作、应用等方面的知识。 首先,Redis是一种开源的内存数据库,它具有高性能、高可用性和高扩展性的特点。它可以用于缓存、消息队列、实时排行榜等场景,广泛应用于Web应用开发、大数据存储和分析等领域。 在学习笔记中,首先介绍了Redis的基本概念,包括数据结构、持久化、单线程架构等方面的知识。数据结构包括字符串、哈希表、列表、集合和有序集合等,笔记详细介绍了它们的特点和使用方法。持久化方面,介绍了RDB快照和AOF日志两种持久化方式的原理和使用方法。同时,笔记也解释了为什么Redis选择单线程架构以及如何充分利用单线程的优势。 其次,学习笔记还包括了Redis的常用操作,例如数据的增删改查、事务和管道操作、过期时间设置等。这些操作是使用Redis进行开发和使用时必不可少的知识点,通过学习笔记可以快速掌握这些操作的使用方法。 此外,学习笔记还涉及了Redis的高级应用,如发布订阅、Lua脚本、事件通知等。这些高级应用可以帮助开发者更好地利用Redis的功能和特性,提升系统的性能和稳定性。 综上所述,《Redis全套学习笔记.pdf》是一本全面介绍Redis的学习资料,通过学习这本笔记,读者可以了解Redis的基本概念和原理,掌握Redis的常用操作和高级应用,从而更好地使用Redis进行开发和应用。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

心向阳光的天域

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

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

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

打赏作者

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

抵扣说明:

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

余额充值