Redis —— Redis In Action —— Redis 实战—— 实战篇一 —— 基于 Redis 的短信登录功能 —— Redis + Token 的共享 session 应用— 有代码

本文目录

  • Redis 实战 —— 实战篇
    • SMS Login —— 基于 Redis 的短信登录功能
      • 导入 黑马点评 项目
        • ⭐ 导入后端项目
        • ⭐ 导入前端项目
      • 基于 Session 实现登录
        • ⭐ 发送短信验证码的逻辑思路
        • ⭐ 短信验证码登录、注册逻辑思路
        • ⭐ 校验登录状态逻辑思路
      • 集群的 Session 共享问题
      • 基于 Redis 实现共享 Session 登录
        • ❓ 错误:`java.lang.Long cannot be cast to java.lang.String`
        • ⭐ 重新测试
      • 登录拦截器的优化
      • 课后知识点 —— 所要了解掌握的面试题

Redis 实战 —— 实战篇

Redis —— Redis In Action —— Redis 实战—— 实战篇一 —— 基于 Redis 的短信登录功能 —— Redis + Token 的共享 session 应用— 有代码_第1张图片

课程介绍

heima 点评Redis —— 项目

1️⃣ 短信登录 —— Redis 的共享 session 应用

2️⃣ 商户查询缓存 —— 企业的缓存使用技巧| 缓存雪崩、穿透等问题解决

3️⃣ 达人探店 —— 基于 List 点赞链表|基于 SortedSet 的点赞排行榜

4️⃣ 优惠券秒杀 —— Redis 的计数器| Lua 脚本 Redis | 分布式锁 | Redis 的三种消息队列 ⭐

5️⃣ 好友关注 —— 基于 Set 集合的关注|取关|共同关注|消息推送的功能

6️⃣ 附近的商户 —— Redis 的 GeoHash 的应用

7️⃣ 用户签到 —— Redis 的 BitMap 数据统计功能

8️⃣ UV 统计 —— Redis 的 HyperLogLog 的统计功能

SMS Login —— 基于 Redis 的短信登录功能

导入 黑马点评 项目

⭐ 导入后端项目

步骤一:首先导入 SQL 文件到数据库当中

2022版Redis入门到精通_免费高速下载|百度网盘-分享无限制 (baidu.com)

Redis —— Redis In Action —— Redis 实战—— 实战篇一 —— 基于 Redis 的短信登录功能 —— Redis + Token 的共享 session 应用— 有代码_第2张图片

Redis —— Redis In Action —— Redis 实战—— 实战篇一 —— 基于 Redis 的短信登录功能 —— Redis + Token 的共享 session 应用— 有代码_第3张图片

其中表的数据结构说明

1️⃣ tb_user: 用户表

2️⃣ tb_user_info : 用户详情表

3️⃣ tb_shop : 商品信息表

4️⃣ tb_shop_type : 商户类型表

5️⃣ tb_blog : 用户日记表(达人探店日记)

6️⃣ tb_follow : 用户关注表

7️⃣ tb_voucher : 优惠券表

8️⃣ tb_voucher_order : 优惠券的订单表

注意点:MySQL 的版本采用 5.7 及其版本之上

项目架构

Redis —— Redis In Action —— Redis 实战—— 实战篇一 —— 基于 Redis 的短信登录功能 —— Redis + Token 的共享 session 应用— 有代码_第4张图片

步骤二:将写好的半成品源码导入到 IDEA 中并测试

Redis —— Redis In Action —— Redis 实战—— 实战篇一 —— 基于 Redis 的短信登录功能 —— Redis + Token 的共享 session 应用— 有代码_第5张图片

步骤三:改写 application.yaml 文件 改写成自己所对应的数据库

server:
  port: 8081
spring:
  application:
    name: hmdp
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://192.168.56.1:3306/hmdp?useSSL=false&serverTimezone=UTC
    username: root
    password: root
  redis:
    host: 192.168.56.103
    port: 6379
    lettuce:
      pool:
        max-active: 10
        max-idle: 10
        min-idle: 1
        time-between-eviction-runs: 10s
  jackson:
    default-property-inclusion: non_null # JSON处理时忽略非空字段
mybatis-plus:
  type-aliases-package: com.hmdp.entity # 别名扫描包
logging:
  level:
    com.hmdp: debug

测试连接到数据库

Redis —— Redis In Action —— Redis 实战—— 实战篇一 —— 基于 Redis 的短信登录功能 —— Redis + Token 的共享 session 应用— 有代码_第6张图片

步骤四:启动HmDianPingApplication并用浏览器进行访问测试

http://localhost:8081/shop-type/list

Redis —— Redis In Action —— Redis 实战—— 实战篇一 —— 基于 Redis 的短信登录功能 —— Redis + Token 的共享 session 应用— 有代码_第7张图片

测试导入成功~

⭐ 导入前端项目

Redis —— Redis In Action —— Redis 实战—— 实战篇一 —— 基于 Redis 的短信登录功能 —— Redis + Token 的共享 session 应用— 有代码_第8张图片

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

start nginx.exe

步骤二:打开 chrome 浏览器,在空白页面点击鼠标右键,选择检查,即可打开开发者工具——并开启手机模式

Redis —— Redis In Action —— Redis 实战—— 实战篇一 —— 基于 Redis 的短信登录功能 —— Redis + Token 的共享 session 应用— 有代码_第9张图片

http://localhost:8080/

如果出现了能正常打开 前端项目 但是对应的图片以及数据并没有显示,则说明你的后端项目没有开启,前端无法通过请求刷新渲染数据到前端,所以务必保证后端应用程序已经启动。

Redis —— Redis In Action —— Redis 实战—— 实战篇一 —— 基于 Redis 的短信登录功能 —— Redis + Token 的共享 session 应用— 有代码_第10张图片

如图所示大功告成啦~

基于 Session 实现登录

Redis —— Redis In Action —— Redis 实战—— 实战篇一 —— 基于 Redis 的短信登录功能 —— Redis + Token 的共享 session 应用— 有代码_第11张图片

⭐ 发送短信验证码的逻辑思路

Redis —— Redis In Action —— Redis 实战—— 实战篇一 —— 基于 Redis 的短信登录功能 —— Redis + Token 的共享 session 应用— 有代码_第12张图片

  1. 首先用户会提交个人的手机号请求发送验证码

    服务端会对手机号进行校验:

    1. 不符合条件:显示不符合的提示给用户,提示用户重新输入合法的手机号
    2. 符合条件:生成对应的验证码。
  2. 拿到生成好的验证码会将其保存到 Session 当中,随后执行 发送验证码 的业务服务

  3. 结束

步骤一:到对应的 UserController 层编写对应接口、并对对应接口服务的具体实现impl

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

IUserService

Result sendCode(String phone, HttpSession session);

UserServiceImpl

/**
 * 功能描述
 * 获取得到用户发送手机发送短信的获取验证码的请求
 * @date 2022/7/4
 * @author Alascanfu
 */
@Override
public Result sendCode(String phone, HttpSession session) {
    
    // 判断当前用户提交的 手机是否符合正确的 格式
    if (RegexUtils.isPhoneInvalid(phone)) {
        // 如果格式匹配失败 则放回错误信息给前端
        return Result.fail("对不起,您输入的手机号格式有问题,请重试!");
    }
    
    // 如果格式匹配成功 则生成一串随机的验证码
    String code = RandomUtil.randomNumbers(6);

    // 将生成好的随机验证码 保存到 session 当中
    session.setAttribute("code",code);
    
    // 发送短信验证码给用户
    log.info("验证码发送成功 , code => {}",code);
    
    // 发送验证码成功 返回成功响应
    return Result.ok();
}

步骤三:启动程序并测试是否能在后台查看到我们对应的验证码生成

在这里插入图片描述


⭐ 短信验证码登录、注册逻辑思路

Redis —— Redis In Action —— Redis 实战—— 实战篇一 —— 基于 Redis 的短信登录功能 —— Redis + Token 的共享 session 应用— 有代码_第13张图片

  1. 首先用户会将自身收到的验证码和手机号以请求的方式提交

    服务端对用户提交的校验码先进行校验:

    1. 不匹配:则提示用户验证码输入错误,请重试
    2. 匹配:根据手机号查询用户
  2. 根据手机号到数据库中去查找

    1. 没有该用户:说明当前用户为第一次使用,则自动为其注册一个账号,并将账号信息加入到数据库做持久化,随后将该用户的数据保存到 Session中方便调用数据。
    2. 有该用户:将该用户信息保存到 Session中即可。

步骤一:到 UserController 中编写好对应的登录服务接口

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

步骤二:编写对应的服务接口以及服务接口的具体实现

Result login(LoginFormDTO loginForm, HttpSession session);
/**
 * 功能描述
 * 登录功能的具体实现类 登录参数,包含手机号、验证码;或者手机号、密码
 * @date 2022/7/4
 * @author Alascanfu
 */
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
    // 首先拿到用户请求发送过来的 验证码 进行和 session 中对应的验证码是否匹配
    // 如果不匹配 则直接返回给前端 说明用户验证码 不正确
    String cacheCode = (String) session.getAttribute(SystemConstants.SESSION_PHONE_CODE);
    String cachePhone = (String) session.getAttribute(SystemConstants.SESSION_PHONE);
    String code = loginForm.getCode();
    String phone = loginForm.getPhone();
    
    if (RegexUtils.isPhoneInvalid(loginForm.getPhone())){
        return Result.fail("对不起,您输入的手机号格式有问题,请重试!");
    }
    
    if (!cachePhone.equals(phone)){
        return Result.fail("验证码与手机号不匹配,请重试!");
    }
    
    if (cacheCode == null || !cacheCode.equals(code)){
        return Result.fail("对不起,您输入的验证码有误,请重试!");
    }
    
    // 如果匹配则进行验证用户名输入的手机号是否是之前已经注册过的
    User user = userMapper.selectUserByPhone(phone);
    if (user == null){
        // 反之如果没有注册过, 则快速帮用户注册一个 用户账户 填充一些默认信息
        user = createUserWithPhone(phone);
    }
    
    // 将快速注册好的用户|对应登录成功的用户, 加入到当前 session 当中
    session.setAttribute(SystemConstants.SESSION_USER,user);
    // 最后登录成功
    return Result.ok();
}

private User createUserWithPhone(String phone) {
    User user = new User();
    user.setPhone(phone);
    user.setNickName(SystemConstants.USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));
    userMapper.insert(user);
    return user;
}

步骤三:启动测试

Redis —— Redis In Action —— Redis 实战—— 实战篇一 —— 基于 Redis 的短信登录功能 —— Redis + Token 的共享 session 应用— 有代码_第14张图片

Redis —— Redis In Action —— Redis 实战—— 实战篇一 —— 基于 Redis 的短信登录功能 —— Redis + Token 的共享 session 应用— 有代码_第15张图片

Redis —— Redis In Action —— Redis 实战—— 实战篇一 —— 基于 Redis 的短信登录功能 —— Redis + Token 的共享 session 应用— 有代码_第16张图片

登陆成功后会自动跳转会首页

Redis —— Redis In Action —— Redis 实战—— 实战篇一 —— 基于 Redis 的短信登录功能 —— Redis + Token 的共享 session 应用— 有代码_第17张图片

在这里插入图片描述

数据库中也有对应的数据~


⭐ 校验登录状态逻辑思路

Redis —— Redis In Action —— Redis 实战—— 实战篇一 —— 基于 Redis 的短信登录功能 —— Redis + Token 的共享 session 应用— 有代码_第18张图片

  1. 首先用户对该网站的访问请求都会携带cookie信息
  2. 随后服务端可以根据用户提供的 Jsessionid 查看 服务端 Session 是否存在该用户的信息。
    1. 如果没有,用户请求就会被拦截,返回到登录界面,提示用户重新登录。
    2. 如果存在,则将用户的信息 保存到 ThreadLocal 这个线程局部变量域当中方便后续操作。然后放行请求。

步骤一:编写拦截器、用于校验 Session 中的 user 信息

LoginInterceptor

/***
 * @author: Alascanfu
 * @date : Created in 2022/7/4 20:21
 * @description: LoginInterceptor
 * @modified By: Alascanfu
 **/
@Component
public class LoginInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 先通过 request 获取 session
        HttpSession session = request.getSession();
        // 然后通过 session 尝试获取 用户 信息
        User user = (User) session.getAttribute(SystemConstants.SESSION_USER);
        if (user == null){
            // 如果 user 不存在 则返回错误信息 对请求拦截 设置返回的状态码为 未授权
            response.setStatus(401);
            return false ;
        }
        // 如果存在 user 将其保存到 threadLocal 当中
        UserDTO userDTO = userToUserDTO(user);
        UserHolder.saveUser(userDTO);
        return true;
    }
    
    /** User 类型 转换为 UserDTO */
    private UserDTO userToUserDTO(User user) {
        UserDTO userDTO = new UserDTO();
        userDTO.setId(user.getId());
        userDTO.setIcon(user.getIcon());
        userDTO.setNickName(user.getNickName());
        return userDTO;
    }
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 当请求完成之后 清除掉对应 threadLoacl 中的 user 数据即可
        UserHolder.removeUser();
    }
}

步骤二:创建 WebMvcConfig 类 客制化拦截器配置

/***
 * @author: Alascanfu
 * @date : Created in 2022/7/4 20:36
 * @description: Web MVC configuration
 * @modified By: Alascanfu
 **/
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    @Autowired
    private LoginInterceptor loginInterceptor ;
    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginInterceptor)
            .excludePathPatterns(
                "/shop/**",
                "/voucher/**",
                "/shop-type/**",
                "/upload/**",
                "/blog/hot",
                "/user/code",
                "/user/login"
            );
    }
}

步骤三:改写 UserController 中的 /user/me 请求接口

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

步骤四:进行测试

按照用户登录的步骤进行登录,查看当前用户数据

Redis —— Redis In Action —— Redis 实战—— 实战篇一 —— 基于 Redis 的短信登录功能 —— Redis + Token 的共享 session 应用— 有代码_第19张图片


集群的 Session 共享问题

Redis —— Redis In Action —— Redis 实战—— 实战篇一 —— 基于 Redis 的短信登录功能 —— Redis + Token 的共享 session 应用— 有代码_第20张图片

分布式之session共享问题 4种解决方案及spring session的使用

基于 Redis 实现共享 Session 登录

Redis —— Redis In Action —— Redis 实战—— 实战篇一 —— 基于 Redis 的短信登录功能 —— Redis + Token 的共享 session 应用— 有代码_第21张图片

Redis —— Redis In Action —— Redis 实战—— 实战篇一 —— 基于 Redis 的短信登录功能 —— Redis + Token 的共享 session 应用— 有代码_第22张图片

需要查看前端代码逻辑 来了解后端是需要获取请求头中的哪个信息来获取 token 信息数据

Axios前后端异步请求库 后端人员这一篇就够了

// request拦截器,将用户token放入头中
let token = sessionStorage.getItem("token");
axios.interceptors.request.use(
  config => {
    if(token) config.headers['authorization'] = token
    return config
  },
  error => {
    console.log(error)
    return Promise.reject(error)
  }
)

很清楚的可以看到

let token = sessionStorage.getItem("token"); 从浏览器内存中获取 token 数值

axios.interceptors.request.use 会在每次发送请求时经过该 axios 拦截器。

将请求头中的 authorization 携带 token 数值信息。

步骤一:改写 原 Session 存储验证码 => Redis 存储验证码

1️⃣ 获取用户提交的请求数据

​ i. 验证手机号是否格式正确

​ 1.不正确 => 返回 错误信息

​ 2.正确 => 进行下步逻辑判断

2️⃣ 手机号码格式正确则生成一串随机的验证码

3️⃣ 将生成好的 验证码 保存到 redis 当中

​ i. key => 业务简写:业务唯一属性: + phone

​ ii.value => 验证码

4️⃣ 返回请求成功信息

    /**
     * 功能描述
     * 获取得到用户发送手机发送短信的获取验证码的请求
     * @date 2022/7/4
     * @author Alascanfu
     */
    @Override
    public Result sendCode(String phone, HttpSession session) {
        
        // 判断当前用户提交的 手机是否符合正确的 格式
        if (RegexUtils.isPhoneInvalid(phone)) {
            // 如果格式匹配失败 则放回错误信息给前端
            return Result.fail("对不起,您输入的手机号格式有问题,请重试!");
        }
        
        // 如果格式匹配成功 则生成一串随机的验证码
        String code = RandomUtil.randomNumbers(6);
    
        // 将生成好的随机验证码 保存到 Redis 当中
        // k : login:code:18584100561 v : code  expireTime 2 min
        stringRedisTemplate.opsForValue()
            .set(RedisConstants.LOGIN_CODE_KEY + phone ,
                code , RedisConstants.LOGIN_CODE_TTL , TimeUnit.MINUTES);
        
        // 发送短信验证码给用户
        log.info("验证码发送成功 , code => {}",code);
        
        // 发送验证码成功 返回成功响应
        return Result.ok();
    }

步骤二:改写登录具体逻辑

1️⃣ 获取用户提交表单的信息 LoginForm

​ i. 匹配当前提交的手机号码格式是否正确 ? “继续下步逻辑” : “返回错误信息”

​ ii. 通过用户提交的 手机号码 与 Redis常量 进行拼接 获取得到 key 进行查找

​ 1. 判断当前获取得到的value 是否不为空 ? “继续下步逻辑” : “返回错误信息”

2️⃣ 通过 用户 提交的手机号码 到 数据库中 查找对应数据

​ i. 如果用户不存在 则 进行快速创建一个用户

​ ii. 存在 执行下步逻辑

3️⃣ 随机生成 token 作为登录令牌 并将其作为 key 保存到 redis 当中

​ i. 将之前获取得到的 User 对象转换为不包含敏感数据的 UserDTO 然后再转换为 HashMap 进行存储

​ ii. 存储 以 Redis常量 + token 为 Key ,以 UserMap 作为数据存储

4️⃣ 防止大量 k-v 长时间占用内存空间 所以需要设置 token 有效期

5️⃣ 返回 token

/**
     * 功能描述
     * 登录功能的具体实现类 登录参数,包含手机号、验证码;或者手机号、密码
     * @date 2022/7/4
     * @author Alascanfu
     */
    @Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
        // 首先拿到用户请求发送过来的 验证码 进行和 session 中对应的验证码是否匹配
        String code = loginForm.getCode();
        String phone = loginForm.getPhone();
        
        // TODO 从 Redis 中获取验证码 并且进行校验
        String cacheCode = stringRedisTemplate.opsForValue()
            .get(RedisConstants.LOGIN_CODE_KEY + phone);
        
        
        if (RegexUtils.isPhoneInvalid(loginForm.getPhone())){
            return Result.fail("对不起,您输入的手机号格式有问题,请重试!");
        }
        
        if (cacheCode == null || !cacheCode.equals(code)){
            return Result.fail("对不起,您输入的验证码有误,请重试!");
        }
        
        // 如果匹配则进行验证用户名输入的手机号是否是之前已经注册过的
        User user = userMapper.selectUserByPhone(phone);
        if (user == null){
            // 反之如果没有注册过, 则快速帮用户注册一个 用户账户 填充一些默认信息
            user = createUserWithPhone(phone);
        }
        
        // 将快速注册好的用户|对应登录成功的用户, 加入到当前 session 当中
//        session.setAttribute(SystemConstants.SESSION_USER,user);
        
        // TODO 将用户信息 保存到 Redis 中 1=> 随机生成 token
        //  2=>将User对象转换为 Hash 进行存储
        //  3=>存储数据
        //  4=>设置 token 有效期
        String token = UUID.randomUUID().toString(true);
    
        UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
    
        Map<String, Object> userMap = BeanUtil.beanToMap(userDTO);
    
        String tokenKey = LOGIN_USER_KEY + token;
        stringRedisTemplate.opsForHash().putAll(tokenKey,userMap);
        stringRedisTemplate.expire(tokenKey,RedisConstants.LOGIN_USER_TTL ,TimeUnit.SECONDS);
        // 将 token 返回给浏览器
        
        return Result.ok(token);
    }

❓ 因为我们想要保证用户每次操作之后都会重新更新 token 凭证的时间,为避免出现定时剔除token凭证所以我们需要在拦截器中追加对应的逻辑,保证用户在每次请求之后都会更新 token凭证的存活时间

步骤三:改写登录校验逻辑

1️⃣ 首先从 请求头中获取用户携带的 token 数据信息

2️⃣ 通过获取得到的 token 信息与 Redis常量 拼接 从 Redis 中获取对应的用户数据

3️⃣ 将从 Redis 中获取得到的 Map 数据转换为 UserDTO 类型数据

4️⃣ 将 UserDTO 数据 添加到当前线程局部变量当中

5️⃣ 刷新 对应用户的 token 凭证过期时间

6️⃣ 放行

/***
 * @author: Alascanfu
 * @date : Created in 2022/7/4 20:21
 * @description: LoginInterceptor
 * @modified By: Alascanfu
 **/
@Component
public class LoginInterceptor implements HandlerInterceptor {
    
    @Autowired
    private StringRedisTemplate stringRedisTemplate ;
    
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 先通过 request 获取 session
        // 1.获取请求头中的 token
        HttpSession session = request.getSession();
        String token = request.getHeader("authorization");
        if (StrUtil.isBlank(token)){
            response.setStatus(401);
            return false ;
        }
        // 然后通过 session 尝试获取 用户 信息
        // 2.通过 token 从 Redis 中获取用户信息
//        User user = (User) session.getAttribute(SystemConstants.SESSION_USER);
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(RedisConstants.LOGIN_USER_KEY + token);
        if (userMap.isEmpty()){
            // 如果 user 不存在 则返回错误信息 对请求拦截 设置返回的状态码为 未授权
            response.setStatus(401);
            return false ;
        }
        // 3.将 查询得到的 Hash 数据类型转换为 UserDTO 对象
        // 如果存在 user 将其保存到 threadLocal 当中
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap,new UserDTO(),false);
        // 4. 保存用户信息到 ThreadLocal 当中
        UserHolder.saveUser(userDTO);
        // 7. 刷新 token 有效期
        stringRedisTemplate.expire(RedisConstants.LOGIN_USER_KEY + token , RedisConstants.LOGIN_USER_TTL, TimeUnit.SECONDS);
        // 8. 放行
        return true;
    }
    
    /** User 类型 转换为 UserDTO */
    private UserDTO userToUserDTO(User user) {
        UserDTO userDTO = new UserDTO();
        userDTO.setId(user.getId());
        userDTO.setIcon(user.getIcon());
        userDTO.setNickName(user.getNickName());
        return userDTO;
    }
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 当请求完成之后 清除掉对应 threadLoacl 中的 user 数据即可
        UserHolder.removeUser();
    }
}

步骤四:进行对应的测试

启动好程序之后我们来到用户登录界面,进行用户对应的登录,然后查看对应的 Redis 库中是否已经 有了 数据。

Redis —— Redis In Action —— Redis 实战—— 实战篇一 —— 基于 Redis 的短信登录功能 —— Redis + Token 的共享 session 应用— 有代码_第23张图片

❓ 错误:java.lang.Long cannot be cast to java.lang.String

2022-07-05 00:36:29.592 ERROR 24500 — [nio-8081-exec-5] com.hmdp.config.WebExceptionAdvice : java.lang.ClassCastException: java.lang.Long cannot be cast to java.lang.String

java.lang.ClassCastException: java.lang.Long cannot be cast to java.lang.String
at org.springframework.data.redis.serializer.StringRedisSerializer.serialize(StringRedisSerializer.java:36) ~[spring-data-redis-2.3.9.RELEASE.jar:2.3.9.RELEASE]

=> 这里报出了 类型转化错误,主要是 StringRedisSerializer 这个序列化对象时导致的。

=> 根据提示找到对应出错的代码错误处

Redis —— Redis In Action —— Redis 实战—— 实战篇一 —— 基于 Redis 的短信登录功能 —— Redis + Token 的共享 session 应用— 有代码_第24张图片

解决方案

改写对应的转换逻辑

Map<String, Object> userMap = BeanUtil.beanToMap(userDTO,new HashMap<>(),
    CopyOptions.create().setIgnoreNullValue(true).setFieldValueEditor((fieldName,fieldValue)->{
        return fieldValue.toString();
    }));

⭐ 重新测试

在这里插入图片描述

Redis —— Redis In Action —— Redis 实战—— 实战篇一 —— 基于 Redis 的短信登录功能 —— Redis + Token 的共享 session 应用— 有代码_第25张图片

Redis —— Redis In Action —— Redis 实战—— 实战篇一 —— 基于 Redis 的短信登录功能 —— Redis + Token 的共享 session 应用— 有代码_第26张图片

Redis —— Redis In Action —— Redis 实战—— 实战篇一 —— 基于 Redis 的短信登录功能 —— Redis + Token 的共享 session 应用— 有代码_第27张图片

登录拦截器的优化

Redis —— Redis In Action —— Redis 实战—— 实战篇一 —— 基于 Redis 的短信登录功能 —— Redis + Token 的共享 session 应用— 有代码_第28张图片

Redis —— Redis In Action —— Redis 实战—— 实战篇一 —— 基于 Redis 的短信登录功能 —— Redis + Token 的共享 session 应用— 有代码_第29张图片

步骤一:编写新的全局放行拦截器 RefreshTokenInterceptor

/***
 * @author: Alascanfu
 * @date : Created in 2022/7/5 1:07
 * @description: RefreshTokenInterceptor
 * @modified By: Alascanfu
 **/
@Component
public class RefreshTokenInterceptor implements HandlerInterceptor {
    @Autowired
    private 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. 查询对应的 Redis 用户
        String tokenKey = RedisConstants.LOGIN_USER_KEY + token ;
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(tokenKey);
        if (userMap.isEmpty()){
            return true ;
        }
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
        //  3. 保存到 ThreadLocal
        UserHolder.saveUser(userDTO);
        //  4. 刷新 token 有效期
        stringRedisTemplate.expire(tokenKey,RedisConstants.LOGIN_USER_TTL, TimeUnit.SECONDS);
        //  5. 放行
        return true;
    }
    
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        
    }
}

步骤二:改写优化 LoginInterceptor

/***
 * @author: Alascanfu
 * @date : Created in 2022/7/4 20:21
 * @description: LoginInterceptor
 * @modified By: Alascanfu
 **/
@Component
public class LoginInterceptor implements HandlerInterceptor {
    
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // TODO 判断是否需要拦截 判断 ThreadLocal 是否存在用户
        if (UserHolder.getUser() == null){
            response.setStatus(401);
            return false ;
        }
        // 有用户就放行
        return true;
    }
    
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 当请求完成之后 清除掉对应 threadLoacl 中的 user 数据即可
        UserHolder.removeUser();
    }
}

步骤三:改写 WebMvc 配置类进行对应的拦截器配置

/***
 * @author: Alascanfu
 * @date : Created in 2022/7/4 20:36
 * @description: Web MVC configuration
 * @modified By: Alascanfu
 **/
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    @Autowired
    private LoginInterceptor loginInterceptor ;
    @Autowired
    private RefreshTokenInterceptor refreshTokenInterceptor;
    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(refreshTokenInterceptor)
            .addPathPatterns("/**");
        
        registry.addInterceptor(loginInterceptor)
            .excludePathPatterns(
                "/shop/**",
                "/voucher/**",
                "/shop-type/**",
                "/upload/**",
                "/blog/hot",
                "/user/code",
                "/user/login"
            );
    }
}

步骤四:启动应用程序并打开浏览器进行测试

Redis —— Redis In Action —— Redis 实战—— 实战篇一 —— 基于 Redis 的短信登录功能 —— Redis + Token 的共享 session 应用— 有代码_第30张图片

刷新回到首页页面再来看是否刷新了 token 的凭证时间

Redis —— Redis In Action —— Redis 实战—— 实战篇一 —— 基于 Redis 的短信登录功能 —— Redis + Token 的共享 session 应用— 有代码_第31张图片

课后知识点 —— 所要了解掌握的面试题

一文搞懂Cookie + Session,Redis + Token,JWT 三者的区别

分布式之session共享问题 4种解决方案及spring session的使用

你可能感兴趣的:(Redis,实战与原理,redis,缓存,数据库)