当前位置: 首页 > news >正文

Redis缓存技术 基础第三篇(Redis实现短信验证)

文章目录

  • 一、Redis短信登录流程描述
  • 二、基于Redis实现短信验证码
  • 三、短信验证码的验证
  • 四、 校验是否登录
  • 五、登录验证优化

一、Redis短信登录流程描述

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

二、基于Redis实现短信验证码

UserController定义与前端交互

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

上面使用到了sendCode方法,在userService里定义一下接口,然后在对应实现类中按照上面的流程重写该方法的业务逻辑代码

@Override
public Result sendCode(String phone, HttpSession session) {// 校验手机号if (RegexUtils.isPhoneInvalid(phone)) {// 无效手机号,返回错误信息return Result.fail("手机号格式有误!");}// 有效生成验证码String code = RandomUtil.randomNumbers(6);// 保存 (固定前缀+手机号) 和验证码到Redis中,设置验证码的有效期为2分钟// RedisConstants.LOGIN_CODE_KEY = “login:code:”// RedisConstants.LOGIN_CODE_TTL = 2LstringRedisTemplate.opsForValue().set(RedisConstants.LOGIN_CODE_KEY + phone, code, RedisConstants.LOGIN_CODE_TTL, TimeUnit.MINUTES);// 模拟发送验证码log.debug("验证码:{}", code);// 返回return Result.ok();
}

手机号格式校验使用到的RegexUtils类中的工具方法

/*** 手机号正则*/
public static final String PHONE_REGEX = "^1([38][0-9]|4[579]|5[0-3,5-9]|6[6]|7[0135678]|9[89])\\d{8}$";/**
* 是否是无效手机格式* @param phone 要校验的手机号* @return true:符合,false:不符合*/
public static boolean isPhoneInvalid(String phone){return mismatch(phone, RegexPatterns.PHONE_REGEX);
}// 校验是否不符合正则格式
private static boolean mismatch(String str, String regex){if (StrUtil.isBlank(str)) {return true;}return !str.matches(regex);
}

三、短信验证码的验证

UserController定义与前端交互,其中参数LoginFormDTO 是前端使用手机号+验证码登录或者手机号+密码登录是传递过来的JSON数据

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

上面使用到了login方法,在userService里定义一下接口,然后在对应实现类中按照上卖弄的流程描述重写该方法的业务逻辑代码

@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {String phone = loginForm.getPhone();// 验证码校验String code = loginForm.getCode();String cacheCode = stringRedisTemplate.opsForValue().get(RedisConstants.LOGIN_CODE_KEY + phone);if (cacheCode == null || !code.equals(cacheCode)) {return Result.fail("验证码错误!");}// 根据手机号查询用户信息User user = query().eq("phone", phone).one();if (user == null) {// 不存在就创建一个新用户user = createUserWithPhone(phone);}// 保存用户信息到redis中// 生成随机tokenString token = UUID.randomUUID().toString(true);// user先转userDTO再转hashMap存储  转HashMap时的第三个参数的意思是忽略null值将值都转换成String类型UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),CopyOptions.create().setIgnoreNullValue(true).setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));// RedisConstants.LOGIN_USER_KEY = "login:token:"stringRedisTemplate.opsForHash().putAll(RedisConstants.LOGIN_USER_KEY + token, userMap);// 设置失效时间为30分钟// RedisConstants.LOGIN_USER_TTL = 30LstringRedisTemplate.expire(RedisConstants.LOGIN_USER_KEY + token, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);// 返回前端tokenreturn Result.ok(token);
}private User createUserWithPhone(String phone) {User user = new User();user.setPhone(phone);// SystemConstants.USER_NICK_NAME_PREFIX = "user_"user.setNickName(SystemConstants.USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));save(user);return user;

保存的时候使用BeanUtil将User转换成UserDTO进行存储,UserDTO的结构如下,只保存一部分的数据,一方面可以不用来回传递用户有关的隐私数据,一方面也节省内存提高性能。由于这里的id是数值类型,但是stringRedisTemplate存储时需要hash的键值都是String型,所以说应该在存储之前将id的值转换成String类型,就在上面代码块的24~27行完成了这个操作

@Data
public class UserDTO {private Long id;private String nickName;private String icon;
}

四、 校验是否登录

用户发送请求不止一次,所以说登录验证也不止进行一次,于是可以使用拦截器完成验证,拦截器的使用可分为两步:
创建拦截器

/*** @author : mereign* @date : 2022/5/5 - 10:31* @desc : 拦截器,实现请求拦截,判断登录信息*/
@Component
public class LoginInterceptor implements HandlerInterceptor {@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 获取请求头中的token信息String token = request.getHeader("authorization");if (StrUtil.isBlank(token)) {// token为空,返回401未授权状态码,拦截response.setStatus(401);return false;}// 根据token获取redis中的用户valueMap<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(RedisConstants.LOGIN_USER_KEY + token);HttpSession session = request.getSession();// 判断用户是否存在if (userMap.isEmpty()) {// 用户不存在,返回401未授权状态码,拦截response.setStatus(401);return false;}// 用户存在,将hash数据转换为userDTO,存信息到ThreadLocalUserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);UserHolder.saveUser(userDTO);// 刷新token有效期,放行stringRedisTemplate.expire(RedisConstants.LOGIN_USER_KEY + token, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);return true;}@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {UserHolder.removeUser();}
}

注册拦截器

/*** @author : mereign* @date : 2022/5/5 - 10:43* @desc :*/
@Configuration
public class MvcConfig implements WebMvcConfigurer {@Autowiredprivate LoginInterceptor loginInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(loginInterceptor).excludePathPatterns("/shop/**","/shop-type/**","/voucher/**","/upload/**","/blog/hot","/user/code","/user/login");}
}

缓存用户的信息到ThreadLocal中的工具方法

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();}
}

UserController定义与前端交互

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

五、登录验证优化

由上面的登录验证可知,我们对一些需要用户登录验证的功能设置了拦截器,如果验证通过会刷新token的有效期,这样的话只要用户一直访问我们拦截的功能就可以一直保持token是有效的。但是,如果用户登陆之后的操作一直是不需要验证的,那也就意味着token的有效期一直不会刷新,这样的话30分钟之后token就会失效用户验证就会失败,这样显然是不合理的
  于是我们可以使用两个拦截器完成,最前面的负责拦截所有的请求,获取token、从redis中查询用户,将查询结果放到ThreadLocal(可能存null)、刷新token有效期,最后直接放行;后面的拦截器只负责判断有没有从redis中查询到用户,他从ThreadLocal获取查询结果,判断有则放行无则拦截

创建两个拦截器

/*** @author : mereign* @date : 2022/5/5 - 10:31* @desc : 前置拦截器,拦截所有请求,前置工作*/
@Component
public class RefreshTokenInterceptor implements HandlerInterceptor {@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 获取请求头中的token信息String token = request.getHeader("authorization");if (StrUtil.isBlank(token)) {// token为空 直接放行return true;}// 根据token获取redis中的用户valueMap<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(RedisConstants.LOGIN_USER_KEY + token);HttpSession session = request.getSession();// 判断用户是否存在if (userMap.isEmpty()) {// 用户不存在 直接放行return true;}// 用户存在,将hash数据转换为userDTO,存信息到ThreadLocalUserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);UserHolder.saveUser(userDTO);// 刷新token有效期,放行stringRedisTemplate.expire(RedisConstants.LOGIN_USER_KEY + token, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);return true;}@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {UserHolder.removeUser();}
}
/*** @author : mereign* @date : 2022/5/5 - 10:31* @desc : 登录拦截器,拦截需要拦截的请求,判断登录信息*/
@Component
public class LoginInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 判断登录if (UserHolder.getUser() == null) {response.setStatus(401);return false;}return true;}@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {}
}

创建完拦截器之后要将两个拦截器通过配置类配置到容器中生效,多个拦截器的优先级,默认按照添加顺序执行优先级,但是也可以使用order方法指定优先级,按参数的大小排序优先级,参数越小优先级越高

/*** @author : mereign* @date : 2022/5/5 - 10:43* @desc : 配置类注册拦截器*/
@Configuration
public class MvcConfig implements WebMvcConfigurer {@Autowiredprivate RefreshTokenInterceptor refreshTokenInterceptor;@Autowiredprivate LoginInterceptor loginInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {// 前置拦截器registry.addInterceptor(refreshTokenInterceptor).addPathPatterns("/**").order(0);// 后置拦截器registry.addInterceptor(loginInterceptor).excludePathPatterns("/shop/**","/voucher/**","/shop-type/**","/upload/**","/blog/hot","/user/code","/user/login").order(1);}
}

http://www.mrgr.cn/news/58015.html

相关文章:

  • 手机照片怎么转换成jpg格式?分享6种图片格式转换方法
  • 【Java SE 题库】LeetCode 热题 100--->两数之和
  • 光伏电站折旧率的计算
  • 关于java继承(深入解析父类属性的抽取与构造函数的作用)
  • 龙迅#LT89101 适用于 MIPI DSI/CSI摄像头和 LVDS 中继信号延长功能,分辨率可支持 1080P@60HZ!
  • 基于springboot美食商城推荐系统
  • 1.CentOS安装
  • NSSCTF(PWN)16
  • 6SV辐射传输模型编译和加入自定义传感器参数
  • 【模型部署】python中socket编程入门知识-系列1
  • 【Linux学习】(4)Linux权限
  • MyBatis-Plus(三):使用自定义目录下的xml时踩坑记录
  • CMake中的List关键词:详细指南
  • 数据结构与算法——Java实现 46. 从前序与中序遍历序列构造二叉树
  • 【多线程和高并发】多线程和高并发提纲
  • Gradle核心概念总结
  • 论软件著作权
  • 线性可分支持向量机代码实现
  • Python 代码的主要功能是从给定的日志文件和设备列表中提取特定设备(华为和华三)的用户账号信息
  • Java 开发——(下篇)从零开始搭建后端基础项目 Spring Boot 3 + MybatisPlus
  • AI基础:传教士与野人
  • Python如何处理zip压缩文件(Python处理zip压缩文件接口源码)
  • SLAM:未来智能科技的核心——探索多传感器融合的无限可
  • [蓝桥杯 2024 省 C] 回文数组
  • LeetCode199. 二叉树的右视图(2024秋季每日一题 47)
  • Linux 权限的理解