【项目实战】基于Redis实现短信验证码登录 (附源码、思路)

在这里插入图片描述

  各位小伙伴们大家好,欢迎来到这个小扎扎的Redis 6专栏,在这个系列专栏中我对B站黑马的Redis教程进行一个总结,鉴于 看到就是学到、学到就是赚到 精神,这波依然是血赚 ┗|`O′|┛

💡Redis知识点速览

  • 🍖 Redis短信登录流程描述
    • 🥩 短信验证码的发送
    • 🥩 短信验证码的验证
    • 🥩 是否登录的验证
  • 🍖 源码分析
    • 🥩 模拟发送短信验证码
    • 🥩 短信验证码的验证
    • 🥩 校验是否登录
    • 🥩 登录验证优化

🍖 Redis短信登录流程描述

🥩 短信验证码的发送

  用户提交手机号,系统验证手机号是否有效,毕竟无效手机号会消耗你的短信验证次数还会导致系统的性能下降。如果手机号为无效的话就让用户重新提交手机号,如果有效就生成验证码并将该验证码作为value保存到redis中对应的key是手机号,之所以这么做的原因是保证key的唯一性,如果使用固定字符串作为可以的话会被后面的数据所覆盖。然后在控制台输出验证码模拟发送验证码的过程

🥩 短信验证码的验证

  用户的手机号接收到验证码后在平台上提交验证码,系统从redis中根据手机号读取验证码并进行校验,如果验证通过的话就根据用户验证使用的手机号去数据库中进行查询用户信息。如果存在就将查询到的用户信息保存到redis中,完成登录;如果不存在的话就创建一个新用户,并将该用户的信息分别保存到sql数据库和redis中,生成随机token作为key、使用hash结构存储user数据作为value,并将这个token返回给客户端,至此完成登录注册

🥩 是否登录的验证

  用户访问系统业务逻辑的时候需要校验他是否已经登录,如果登录可以访问否则就去登录,那么该如何完成是否登录的校验呢?这就要了解session的相关知识了,每一个session都有一个sessionId信息保存在浏览器的cookie中,当用户使用浏览器发送请求的时候会携带上cookie信息,此时系统就可以使用cookie中的sessionId获取到session信息,并通过session获取到登录时存储的用户信息。如果此时用户在数据库中存在的话就将该用户的信息缓存在ThreadLocal(方便后续验证)中,并放行该访问;否则就说明发送请求的用户未登录或不合法,就要拦截到他的请求前往登录

🍖 源码分析

🥩 模拟发送短信验证码

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.mzph.cn/news/535024.shtml

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

相关文章

加工中心刻字宏程序_FANUC OI系列图书——车床、铣床及加工中心编程

☞ 这是金属加工(mw1950pub)发布的第10035篇文章导读今天跟大家分享FANUC OI系列图书&#xff0c;包括车床、铣床及加工中心编程&#xff0c;快来看看吧&#xff01;《FANUC 0i数控车床/加工中心编程技巧与实例》选择在企业里应用*广泛、编程*具代表性的日本FANUCSeries0i-TC/T…

Redis 的缓存策略

各位小伙伴们大家好&#xff0c;欢迎来到这个小扎扎的Redis 6专栏&#xff0c;在这个系列专栏中我对B站黑马的Redis教程进行一个总结&#xff0c;鉴于 看到就是学到、学到就是赚到 精神&#xff0c;这波依然是血赚 ┗|&#xff40;O′|┛ &#x1f4a1;Redis知识点速览&#…

关闭后天 树莓派_陪你一起玩树莓派-系统安装

从今天就开始我们的树莓派之旅&#xff0c;心情是不是有点小激动&#xff1f;我们拿到一个树莓派是一个裸机。我们要准备一张16G的高速TF闪存卡&#xff0c;一个5V/2A的USB电源和一根micro B的 usb线。装机步骤&#xff1a;一、下载树莓派系统1、浏览器打开树莓派官方网站 http…

简单的签到代码_PHP实现一个小小的签到功能,到底用MySQL还是Redis?

来源 | http://suo.im/5EWN3k今天&#xff0c;看下签到功能怎么选择&#xff1f;现在的网站和app开发中&#xff0c;签到是一个很常见的功能&#xff0c;如微博签到送积分&#xff0c;签到排行榜~微博签到如移动app &#xff0c;签到送流量等活动&#xff0c;移动app签到用户签…

【Redis 6】缓存穿透、缓存雪崩、缓存击穿(附解决方案、代码)

各位小伙伴们大家好&#xff0c;欢迎来到这个小扎扎的Redis 6专栏&#xff0c;在这个系列专栏中我对B站黑马的Redis教程进行一个总结&#xff0c;鉴于 看到就是学到、学到就是赚到 精神&#xff0c;这波依然是血赚 ┗|&#xff40;O′|┛ &#x1f4a1;Redis知识点速览&#…

8软件遇到的问题及解决方法_Excel工作表中的8个常见问题,你一定遇到过,附解决方法...

在Excel工作表中&#xff0c;最常用的还是一些技巧&#xff0c;如果能够熟练掌握&#xff0c;对于工作效率的提高绝对不是一点点哦&#xff0c;结合工作实际&#xff0c;小编对工作中常见的问题进行了总结&#xff0c;一共有8类&#xff0c;你一定也遇到过……一、Excel工作表常…

unity着色器和屏幕特效开发秘笈_Oculus研发分享:开发移动VR内容时应避免的PC渲染技术...

查看引用/信息源请点击&#xff1a;映维网开发移动VR内容时应避免的PC渲染技术&#xff08;映维网 2019年11月25日&#xff09;有不少开发者都是以与PC相同的方式来开发Quest游戏&#xff0c;但这可能会导致优化性能方面出现大量困难。Oculus软件工程师特雷弗达什&#xff08;T…

Java包装类、java中的方法传参机制:按值调用

各位小伙伴们大家好&#xff0c;欢迎来到这个小扎扎的《Java核心技术 卷Ⅰ》笔记专栏&#xff0c;在这个系列专栏中我将记录浅学这本书所得收获&#xff0c;鉴于 看到就是学到、学到就是赚到 精神&#xff0c;这波简直就是血赚 &#x1f4a1;涉及的知识点速通&#x1f6eb; 方法…

尤克里里怎么样_尤克里里和吉他区别?尤克里里与吉他相比有什么不可替代的优势...

尤克里里和吉他有什么区别&#xff1f;想必大家都见过尤克里里吧&#xff0c;就是类似吉他形状的一种小型弦拨乐器。我们可以简单的认为&#xff1a;尤克里里是简化版本的吉他&#xff0c;更加简单&#xff0c;更加便宜。小编弹的就是尤克里里2.从外观上&#xff1a;吉他很大&a…

饿汉懒汉单例设计模式的使用及区别、java中的import关键字

各位小伙伴们大家好&#xff0c;欢迎来到这个小扎扎的《Java核心技术 卷Ⅰ》笔记专栏&#xff0c;在这个系列专栏中我将记录浅学这本书所得收获&#xff0c;鉴于 看到就是学到、学到就是赚到 精神&#xff0c;这波简直就是血赚 &#x1f4a1;涉及的知识点速通&#x1f6eb; 关于…

广电运通不好进吗_我可以说郑州新风的安装大部分都是垃圾吗?

说郑州的新风安装都是垃圾&#xff0c;这话很无礼&#xff0c;很自大&#xff0c;很傲慢&#xff0c;但是我能说确实是这样嘛&#xff1f;其实包括我以前安装的也不合格——虽然我不是故意的。这几年见过许多家同行安装的新风&#xff0c;可以说目前见到的很多家都不合格&#…

Java迭代器和Collection接口

各位小伙伴们大家好&#xff0c;欢迎来到这个小扎扎的《Java核心技术 卷Ⅰ》笔记专栏&#xff0c;在这个系列专栏中我将记录浅学这本书所得收获&#xff0c;鉴于 看到就是学到、学到就是赚到 精神&#xff0c;这波简直就是血赚 &#x1f4a1;涉及的知识点速通&#x1f6eb; 关于…

私钥经过哈希计算可以产生公钥_「区块链基础概念100」:公钥和私钥 | 027

免责声明&#xff1a;本文旨在传递更多市场信息&#xff0c;不构成任何投资建议。文章仅代表作者观点&#xff0c;不代表火星财经官方立场。小编&#xff1a;记得关注哦投资区块链&#xff0c;猛戳&#xff1a;火星财经App下载来源&#xff1a;学习区块链原文标题&#xff1a;「…

关于List集合类ArrayList、LinkedList、Vector详解

各位小伙伴们大家好&#xff0c;欢迎来到这个小扎扎的《Java核心技术 卷Ⅰ》笔记专栏&#xff0c;在这个系列专栏中我将记录浅学这本书所得收获&#xff0c;鉴于 看到就是学到、学到就是赚到 精神&#xff0c;这波简直就是血赚 &#x1f4a1;涉及的知识点速通&#x1f6eb; 关于…

关于Set集合类你都知道什么?来自《卷Ⅰ》的灵魂提问

各位小伙伴们大家好&#xff0c;欢迎来到这个小扎扎的《Java核心技术 卷Ⅰ》笔记专栏&#xff0c;在这个系列专栏中我将记录浅学这本书所得收获&#xff0c;鉴于 看到就是学到、学到就是赚到 精神&#xff0c;这波简直就是血赚 &#x1f4a1;涉及的知识点速通&#x1f6eb; 关于…

流程图虚线框表示什么_UI设计|APP的交互线框布局设计

一.流程图设计流程图(Flow Chart)&#xff1a;用图示的方式反映出特定主体为了满足特定需求而进行的有特定逻辑关系的一系列操作过程。流程图的四种基本结构&#xff1a;顺序结构&#xff0c;条件结构(又称选择结构)&#xff0c;循环结构&#xff0c;分支结构。1.流程图的常用符…

使用Redis完成商品秒杀业务

各位小伙伴们大家好&#xff0c;欢迎来到这个小扎扎的Redis 6专栏&#xff0c;在这个系列专栏中我对B站黑马的Redis教程进行一个总结&#xff0c;鉴于 看到就是学到、学到就是赚到 精神&#xff0c;这波依然是血赚 ┗|&#xff40;O′|┛ &#x1f4a1;Redis知识点速览&#…

表格列隐藏_【excel每日提升】Excel隐藏列,不让别人打开!

【新朋友】点击标题下面蓝色字“王俊东“关注。 【老朋友】点击右上角&#xff0c;转发或分享本页面内容。excel系列课程excel特效系列课程开始了&#xff0c;今天第2节&#xff01;第1节&#xff1a;Excel有公式的单元格标记颜色&#xff0c;很简单&#xff01;第2节&#xf…

“毕业季”|一个java开发实习生的OFFER之路

哈喽哈喽大家好&#xff0c;这里是小扎扎的博客。相信有关注过我的好盆友们可能会发现我已经有一段时间没有出来划水了&#xff0c;那么这段时间小扎扎都在干什么呢&#xff1f;没错&#xff01;我确实是去找实习了&#xff01;接下来就给大家介绍一下本次战役的战况如何 活动地…

virtualbox 该内存不能为written_系统提示“该内存不能为read”的原因和解决办法...

我们单位的电脑经常显示这个对话框&#xff0c;已经有好几年了&#xff0c;单位的老头们都不怎么懂电脑&#xff0c;我本人也不爱管闲事。但是出现这种对话框的原因是什么呢&#xff1f;又怎么解决呢&#xff1f;一般电脑经常出现蓝屏和死机&#xff0c;而且频繁出现。有时会出…