相关推荐recommended
【SpringBoot篇】详解基于Redis实现短信登录的操作
作者:mmseoamin日期:2023-12-13

文章目录

  • 🥰前言
  • 🛸StringRedisTemplate
    • 🌹使用StringRedisTemplate
      • ⭐常用的方法
      • 🛸为什么我们要使用Redis代替Session进行登录操作
        • 🎆具体使用
          • ✨编写拦截器
          • ✨配置拦截器
          • 🌺基于Redis实现发送手机验证码操作
            • 🎈总体思路
            • 🎈具体步骤
            • 🎍基于Redis实现短信登录并注册的操作
              • 🎈总体思路
              • 🎈具体步骤

                【SpringBoot篇】详解基于Redis实现短信登录的操作,在这里插入图片描述,第1张

                🥰前言

                使用 Redis 进行登录适用于以下情况:

                • 分布式系统:

                  当系统需要支持多个节点的分布式部署时,使用 Redis 存储登录信息能够更好地支持多节点间的共享和同步,确保用户的登录状态能够在整个系统中得到有效的传递和管理。

                • 高并发访问:

                  面对大规模的并发访问,使用 Redis 可以提供更好的性能表现。Redis 是一个基于内存的高性能 Key-Value 数据库,能够更快速地读取和写入数据,因此适用于需要处理大量并发请求的场景。

                • 灵活的数据结构需求:

                  如果系统需要根据业务需求选择最佳的数据结构,并且对存储和操作登录信息有更多的灵活性,那么使用 Redis 将会是一个不错的选择。Redis 支持多种数据类型的存储和操作,包括字符串、哈希表、列表、集合和有序集合等,能够满足不同的业务需求。

                • 需要持久化支持:

                  如果系统需要对登录信息进行持久化存储,以防止数据丢失,Redis 的持久化功能可以很好地满足这一需求。

                  总的来说,使用 Redis 进行登录适用于需要支持分布式部署、面对高并发访问、有灵活的数据结构需求以及需要持久化支持的系统场景。通过合理地利用 Redis 的特性,可以更好地满足上述情况下的需求,提高系统的可扩展性、性能和稳定性。

                  虽然 Spring Boot 应用通常是单体应用,但是在实际运行中,我们也经常会遇到多个实例同时运行的情况,这时候就需要使用 Redis 进行分布式 Session 管理。

                  🛸StringRedisTemplate

                  StringRedisTemplate是Spring Data Redis提供的一个类,它是一个具体的对象,用于操作Redis数据库中的字符串类型数据。

                  StringRedisTemplate封装了Redis的操作,并提供了一系列方法来对Redis中的字符串进行读取、写入和删除操作。它是RedisTemplate的一个子类,专门用于处理字符串类型的数据。

                  🌹使用StringRedisTemplate

                  首先引入依赖,引入StringRedisTemplate的依赖

                  
                      org.springframework.boot
                      spring-boot-starter-data-redis
                  
                  

                  ⭐常用的方法

                  StringRedisTemplate提供了多个方法来操作Redis中的字符串类型数据。下面是一些常用的方法:

                  • opsForValue().set(key, value):将一个字符串类型的值value存储到Redis中,并指定键key。
                  • opsForValue().get(key):根据键key获取对应的字符串类型的值。
                  • opsForValue().increment(key, delta):将键key所对应的值增加delta,delta可以为负数。
                  • opsForValue().size(key):获取值的长度。

                    🛸为什么我们要使用Redis代替Session进行登录操作

                    集群session存在共享问题,会导致数据丢失

                    • 保存相同的数据,大家互相copy,会有内存空间的浪费
                    • 我们copy数据的时候,是需要有一定的时间的,会有延迟,如果在这个延迟之内,如果有人来访问,仍然会造成数据不一致的情况

                      【SpringBoot篇】详解基于Redis实现短信登录的操作,请添加图片描述,第2张

                      如果我们使用Redis的话。

                    • Redis是在tomcat外面的存储,如果任意一台tomcat都能访问到Redis,可以实现数据共享,储存在Redis里面的数据,任何tomcat都可以看到,使用就不存在数据丢失的问题
                    • Redis读写延迟非常低,方便进行内存存储
                    • Redis是key-value结构

                      【SpringBoot篇】详解基于Redis实现短信登录的操作,在这里插入图片描述,第3张

                      🎆具体使用

                      ✨编写拦截器

                      【SpringBoot篇】详解基于Redis实现短信登录的操作,在这里插入图片描述,第4张

                      RefreshTokenInterceptor.java

                      【SpringBoot篇】详解基于Redis实现短信登录的操作,在这里插入图片描述,第5张

                      在拦截器中配置拦截操作

                      package com.hmdp.utils;
                      import cn.hutool.core.bean.BeanUtil;
                      import cn.hutool.core.util.StrUtil;
                      import com.hmdp.dto.UserDTO;
                      import org.springframework.data.redis.core.StringRedisTemplate;
                      import org.springframework.web.servlet.HandlerInterceptor;
                      import javax.servlet.http.HttpServletRequest;
                      import javax.servlet.http.HttpServletResponse;
                      import java.util.Map;
                      import java.util.concurrent.TimeUnit;
                      import static com.hmdp.utils.RedisConstants.LOGIN_USER_KEY;
                      import static com.hmdp.utils.RedisConstants.LOGIN_USER_TTL;
                      public class RefreshTokenInterceptor implements HandlerInterceptor {
                          private StringRedisTemplate stringRedisTemplate;
                          public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {
                              this.stringRedisTemplate = stringRedisTemplate;
                          }
                          @Override
                          public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
                              // 1.获取请求头中的token
                              String token = request.getHeader("authorization");
                              if (StrUtil.isBlank(token)) {
                                  return true;
                              }
                              // 2.基于TOKEN获取redis中的用户
                              String key  = LOGIN_USER_KEY + token;
                              Map userMap = stringRedisTemplate.opsForHash().entries(key);
                              // 3.判断用户是否存在
                              if (userMap.isEmpty()) {
                                  return true;
                              }
                              // 5.将查询到的hash数据转为UserDTO
                              UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
                              // 6.存在,保存用户信息到 ThreadLocal
                              UserHolder.saveUser(userDTO);
                              // 7.刷新token有效期
                              stringRedisTemplate.expire(key, LOGIN_USER_TTL, TimeUnit.MINUTES);
                              // 8.放行
                              return true;
                          }
                          @Override
                          public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
                              // 移除用户
                              UserHolder.removeUser();
                          }
                      }
                      

                      【SpringBoot篇】详解基于Redis实现短信登录的操作,在这里插入图片描述,第6张

                      刷新token的目的

                      用户每访问一次,这个token就会刷新一次,主要用户一直在操作,这个token就不会消失


                      但是如果仅仅拦截的是需要登录的路径,用户 访问 不需要登录 的路径 的时候(比如首页),这个拦截器就不生效,此时token就不会刷新,这样子,过了token的有效期后,尽管用户还在访问,用户的登录状态却消失了,这样肯定不太合理

                      那么我们就需要在原来的拦截器基础上再加上一个拦截器

                      【SpringBoot篇】详解基于Redis实现短信登录的操作,请添加图片描述,第7张

                      LoginInterceptor.java

                      【SpringBoot篇】详解基于Redis实现短信登录的操作,在这里插入图片描述,第8张

                      在拦截器中配置拦截操作

                      package com.hmdp.utils;
                      import org.springframework.web.servlet.HandlerInterceptor;
                      import javax.servlet.http.HttpServletRequest;
                      import javax.servlet.http.HttpServletResponse;
                      public class LoginInterceptor implements HandlerInterceptor {
                          @Override
                          public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
                              // 1.判断是否需要拦截(ThreadLocal中是否有用户)
                              if (UserHolder.getUser() == null) {
                                  // 没有,需要拦截,设置状态码
                                  response.setStatus(401);
                                  // 拦截
                                  return false;
                              }
                              // 有用户,则放行
                              return true;
                          }
                      }
                      

                      【SpringBoot篇】详解基于Redis实现短信登录的操作,在这里插入图片描述,第9张

                      ✨配置拦截器

                      我们上面编写了拦截器,我们还需要配置拦截器,使这个拦截器生效

                      MvcConfig.java

                      【SpringBoot篇】详解基于Redis实现短信登录的操作,这里是引用,第10张

                      package com.hmdp.config;
                      import com.hmdp.utils.LoginInterceptor;
                      import com.hmdp.utils.RefreshTokenInterceptor;
                      import org.springframework.context.annotation.Configuration;
                      import org.springframework.data.redis.core.StringRedisTemplate;
                      import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
                      import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
                      import javax.annotation.Resource;
                      @Configuration
                      public class MvcConfig implements WebMvcConfigurer {
                          @Resource
                          private StringRedisTemplate stringRedisTemplate;
                          @Override
                          public void addInterceptors(InterceptorRegistry registry) {
                              // 登录拦截器
                              registry.addInterceptor(new LoginInterceptor())
                                      .excludePathPatterns(
                                              "/shop/**",
                                              "/voucher/**",
                                              "/shop-type/**",
                                              "/upload/**",
                                              "/blog/hot",
                                              "/user/code",
                                              "/user/login"
                                      ).order(1);
                              // token刷新的拦截器
                              registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);
                          }
                      }
                      

                      【SpringBoot篇】详解基于Redis实现短信登录的操作,在这里插入图片描述,第11张

                      🌺基于Redis实现发送手机验证码操作

                      🎈总体思路

                      【SpringBoot篇】详解基于Redis实现短信登录的操作,在这里插入图片描述,第12张

                      🎈具体步骤

                      我们首先引入上面说的依赖,然后在application.yml文件(或yaml文件)中进行配置,如下

                      【SpringBoot篇】详解基于Redis实现短信登录的操作,在这里插入图片描述,第13张

                      下面我们编写发送手机验证码的核心代码

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

                      【SpringBoot篇】详解基于Redis实现短信登录的操作,在这里插入图片描述,第14张

                      上面代码里面的RegexUtils.isPhoneInvalid(phone)这段代码是什么用法

                      【SpringBoot篇】详解基于Redis实现短信登录的操作,在这里插入图片描述,第15张

                      stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);这段代码有什么用

                      这段代码的作用是将一个验证码(即code)存储到Redis中,并设置了过期时间为LOGIN_CODE_TTL分钟。以便在一定时间后自动删除该键值对。

                      🎍基于Redis实现短信登录并注册的操作

                      🎈总体思路

                      【SpringBoot篇】详解基于Redis实现短信登录的操作,在这里插入图片描述,第16张

                      🎈具体步骤

                      我们首先引入上面说的依赖,并且在application.yml文件(或yaml文件)中进行配置(同上)

                      然后我们来编写核心代码

                      @Slf4j
                      @Service
                      public class UserServiceImpl extends ServiceImpl implements IUserService {
                          @Resource
                          private StringRedisTemplate stringRedisTemplate;
                          @Override
                          public Result login(LoginFormDTO loginForm, HttpSession session) {
                              // 1.校验手机号
                              String phone = loginForm.getPhone();
                              if (RegexUtils.isPhoneInvalid(phone)) {
                                  // 2.如果不符合,返回错误信息
                                  return Result.fail("手机号格式错误!");
                              }
                              // 3.从redis获取验证码并校验
                              String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
                              String code = loginForm.getCode();
                              if (cacheCode == null || !cacheCode.equals(code)) {
                                  // 不一致,报错
                                  return Result.fail("验证码错误");
                              }
                              // 4.一致,根据手机号查询用户 select * from tb_user where phone = ?
                              User user = query().eq("phone", phone).one();
                              // 5.判断用户是否存在
                              if (user == null) {
                                  // 6.不存在,创建新用户并保存
                                  user = createUserWithPhone(phone);
                              }
                              // 7.保存用户信息到 redis中
                              // 7.1.随机生成token,作为登录令牌
                              String token = UUID.randomUUID().toString(true);
                              // 7.2.将User对象转为HashMap存储
                              UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
                              Map userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),
                                      CopyOptions.create()
                                              .setIgnoreNullValue(true)
                                              .setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));
                              // 7.3.存储
                              String tokenKey = LOGIN_USER_KEY + token;
                              stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);
                              // 7.4.设置token有效期
                              stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);
                              // 8.返回token
                              return Result.ok(token);
                          }
                          
                          private User createUserWithPhone(String phone) {
                              // 1.创建用户
                              User user = new User();
                              user.setPhone(phone);
                              user.setNickName(USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));
                              // 2.保存用户
                              save(user);
                              return user;
                          }
                          
                      }
                      

                      【SpringBoot篇】详解基于Redis实现短信登录的操作,在这里插入图片描述,第17张

                      User user = query().eq(“phone”, phone).one();这段代码使用了mybatisplus,相当于select * from tb_user where phone = ?

                      为什么要使用HashMap进行存储

                      在这段代码中,使用HashMap进行存储是为了将用户对象转换成键值对形式,便于统一保存到Redis中,并且可以方便地进行序列化和反序列化操作。具体来说:

                      • 便于存储和读取:将用户对象转为HashMap后,可以方便地通过stringRedisTemplate.opsForHash().putAll()方法一次性将整个用户对象存储到Redis的Hash数据结构中,而不需要对用户对象的每个字段分别进行存储。
                      • 数据结构清晰:使用HashMap可以清晰地表示用户对象的各个字段和对应的数值,便于管理和维护。
                      • 方便序列化和反序列化:HashMap作为Java中的常用数据结构,可以方便地进行序列化(将数据转换为字节序列)和反序列化(将字节序列转换为数据)操作,便于在存储到Redis或者从Redis中读取时进行数据格式的转换。

                        总之,使用HashMap进行存储能够简化代码逻辑,提高数据存储和读取的效率,并且方便进行数据结构的转换和管理。

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

                        CopyOptions.create()

                        .setIgnoreNullValue(true)

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

                        这段代码为什么要这样写,这些参数有什么用

                        其中,beanToMap是一个方法,用于将Java对象(Bean)转换为Map类型的数据结构。

                        在这段代码中,BeanUtil.beanToMap()方法被使用,它是一个工具类方法,可以通过反射机制将Java对象的属性和对应的值转换为键值对形式,并存储到一个Map对象中。

                        具体来说,beanToMap方法接收三个参数:

                        • userDTO:表示要转换的源对象,即需要将其转换为Map的对象。

                        • new HashMap<>():表示用于存储转换结果的目标HashMap对象,这里使用了一个新的空HashMap,用于接收转换后的键值对数据。

                        • CopyOptions.create().setIgnoreNullValue(true):这是使用BeanUtil进行对象转换时的配置选项。setIgnoreNullValue(true)表示忽略源对象中值为null的属性,不将其放入目标Map中。

                        • .setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()):这个配置项表示对转换过程中的字段值进行编辑处理。在这里,它的作用是将字段值转换为字符串类型,确保最终存储到Map中的值都是字符串类型。

                          综合起来,这段代码的目的是将UserDTO对象转换为Map类型,同时忽略空值属性,并确保所有属性值都被转换为字符串类型。这样做的原因可能是为了在存储到Redis中时,确保数据的统一性和一致性,便于后续从Redis中读取并进行处理。

                          在技术的道路上,我们不断探索、不断前行,不断面对挑战、不断突破自我。科技的发展改变着世界,而我们作为技术人员,也在这个过程中书写着自己的篇章。让我们携手并进,共同努力,开创美好的未来!愿我们在科技的征途上不断奋进,创造出更加美好、更加智能的明天!

                          【SpringBoot篇】详解基于Redis实现短信登录的操作,在这里插入图片描述,第18张