点评中是如何实现短信登录的

点评中是如何实现短信登录的

首先在这个项目中 我们主要还是通过session来实现的验证码登录

根据这个图片我们很清楚的知道 首先要制造一个随机的验证码以确保并且保存到redis中(等下我会说为什么不存在session中)接下来前端传来验证码 后端校验后 判断是否为新用户 如果是新用户 那么注册一个用户 将昵称设置为随机字段 手机号为唯一字段 如果存在 就保存在redis中 返回随机token给客户端 接下来前端传来手机号 我们从redis中拿到用户信息 保存到Threadlocal中 放行请求

public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {@Resourceprivate StringRedisTemplate stringRedisTemplate;private static final String LOGIN_CODE_KEY = "login:code:";private static final Long LOGIN_CODE_TTL = 2L;/*** 发送手机验证码*/public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {// 发送短信验证码并保存验证码if (RegexUtils.isEmailInvalid(phone)) {return Result.fail("邮箱格式不正确");}String code = MailUtils.achieveCode();// 将验证码存入Redis,设置有效期为2分钟stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);log.info("发送登录验证码:{}", code);try {MailUtils.sendTestMail(phone, code);} catch (MessagingException e) {throw new RuntimeException(e);}return Result.ok();}@Overridepublic Result login(LoginFormDTO loginForm, HttpSession session) {// 1.校验手机号String phone = loginForm.getPhone();if (RegexUtils.isEmailInvalid(phone)) {return Result.fail("邮箱格式不正确");}//2. 校验验证码String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);String code = loginForm.getCode();log.info("登录校验 - 手机号: {}, 输入的验证码: {}, 缓存的验证码: {}", phone, code, cacheCode);if (cacheCode == null || !cacheCode.equals(code)) {// 不一致,报错return Result.fail("验证码错误");}//3. 验证通过后删除验证码stringRedisTemplate.delete(LOGIN_CODE_KEY + phone);//4. 根据手机号查询用户User user = query().eq("phone", phone).one();//5. 判断用户是否存在if (user == null) {// 6.不存在,创建新用户并保存user = createUserWithPhone(phone);}//7.存在,保存用户信息到redis中UserDTO userDTO = new UserDTO();BeanUtils.copyProperties(user, userDTO);session.setAttribute("user", userDTO);// 将对象中字段全部转成string类型,StringRedisTemplate只能存字符串类型的数据Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),CopyOptions.create().setIgnoreNullValue(true).setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));String token = UUID.randomUUID().toString();String tokenKey = LOGIN_USER_KEY + token;stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);//8.返回tokenreturn Result.ok(token);}private User createUserWithPhone(String phone) {User user = new User();// 如果是邮箱,生成一个随机的手机号作为唯一标识if (phone.contains("@")) {// 生成一个随机的11位数字作为phoneString randomPhone;User exist;do {// 生成13开头的11位随机手机号randomPhone = "13" + RandomUtil.randomNumbers(9);// 检查该手机号是否已存在exist = query().eq("phone", randomPhone).one();} while (exist != null); // 如果存在,继续生成直到找到一个不存在的user.setPhone(randomPhone);// 可以考虑将邮箱保存到其他字段,如果有需要的话} else {user.setPhone(phone);}user.setNickName(USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));save(user);return user;}
}

redis的优点

首先我们来看session会出现的问题

什么是Session集群共享问题?

在分布式集群环境中,会话(Session)共享是一个常见的挑战。默认情况下,Web 应用程序的会话是保存在单个服务器上的,当请求不经过该服务器时,会话信息无法被访问。

Session集群共享问题造成哪些问题?

服务器之间无法实现会话状态的共享。比如:在当前这个服务器上用户已经完成了登录,Session中存储了用户的信息,能够判断用户已登录,但是在另一个服务器的Session中没有用户信息,无法调用显示没有登录的服务器上的服务
如何解决Session集群共享问题?

方案一:Session拷贝(不推荐)

Tomcat提供了Session拷贝功能,通过配置Tomcat可以实现Session的拷贝,但是这会增加服务器的额外内存开销,同时会带来数据一致性问题

方案二:Redis缓存(推荐)

Redis缓存具有Session存储一样的特点,基于内存、存储结构可以是key-value结构、数据共享

Redis缓存相较于传统Session存储的优点:

  1. 高性能和可伸缩性:Redis 是一个内存数据库,具有快速的读写能力。相比于传统的 Session 存储方式,将会话数据存储在 Redis 中可以大大提高读写速度和处理能力。此外,Redis 还支持集群和分片技术,可以实现水平扩展,处理大规模的并发请求。
  2. 可靠性和持久性:Redis 提供了持久化机制,可以将内存中的数据定期或异步地写入磁盘,以保证数据的持久性。这样即使发生服务器崩溃或重启,会话数据也可以被恢复。
    丰富的数据结构:Redis 不仅仅是一个键值存储数据库,它还支持多种数据结构,如字符串、列表、哈希、集合和有序集合等。这些数据结构的灵活性使得可以更方便地存储和操作复杂的会话数据。
  3. 分布式缓存功能:Redis 作为一个高效的缓存解决方案,可以用于缓存会话数据,减轻后端服务器的负载。与传统的 Session 存储方式相比,使用 Redis 缓存会话数据可以大幅提高系统的性能和可扩展性。
  4. 可用性和可部署性:Redis 是一个强大而成熟的开源工具,有丰富的社区支持和活跃的开发者社区。它可以轻松地与各种编程语言和框架集成,并且可以在多个操作系统上运行。
    PS:但是Redis费钱,而且增加了系统的复杂度

从前面的分析来看,显然Redis是要优于Session的,但是Redis中有很多数据结构,我们应该选择哪种数据结构来存储用户信息才能够更优呢?可能大多数同学都会想到使用 String 类型的数据据结构,但是这里我推荐使用 Hash结构!

Hash 结构与 String 结构类型的比较:
1 . String 数据结构是以 JSON 字符串的形式保存,更加直观,操作也更加简单,但是 JSON 结构会有很多非必须的内存开销,比如双引号、大括号,内存占用比 Hash 更高
2. Hash 数据结构是以 Hash 表的形式保存,可以对单个字段进行CRUD,更加灵活
Redis替代Session需要考虑的问题:

  1. 选择合适的数据结构,了解 Hash 比 String 的区别
  2. 选择合适的key,为key设置一个业务前缀,方便区分和分组,为key拼接一个UUID,避免key冲突防止数据覆盖
  3. 选择合适的存储粒度,对于验证码这类数据,一般设置TTL为3min即可,防止大量缓存数据的堆积,而对于用户信息这类数据可以稍微设置长一点,比如30min,防止频繁对Redis进行IO操作

单独配置一个拦截器用户刷新Redis中的token:在基于Session实现短信验证码登录时,我们只配置了一个拦截器,这里需要另外再配置一个拦截器专门用户刷新存入Redis中的 token,因为我们现在改用Redis了,为了防止用户在操作网站时突然由于Redis中的 token 过期,导致直接退出网站,严重影响用户体验。那为什么不把刷新的操作放到一个拦截器中呢,因为之前的那个拦截器只是用来拦截一些需要进行登录校验的请求,对于哪些不需要登录校验的请求是不会走拦截器的,刷新操作显然是要针对所有请求比较合理,所以单独创建一个拦截器拦截一切请求,刷新Redis中的Key

登录拦截器:

public class LoginInterceptor implements HandlerInterceptor {/*** 前置拦截器,用于判断用户是否登录*/@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 判断当前用户是否已登录if (ThreadLocalUtls.getUser() == null){// 当前用户未登录,直接拦截response.setStatus(HttpStatus.HTTP_UNAUTHORIZED);return false;}// 用户存在,直接放行return true;}
}
public class RefreshTokenInterceptor implements HandlerInterceptor {// new出来的对象是无法直接注入IOC容器的(LoginInterceptor是直接new出来的)// 所以这里需要再配置类中注入,然后通过构造器传入到当前类中private StringRedisTemplate stringRedisTemplate;public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;}@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 1、获取token,并判断token是否存在String token = request.getHeader("authorization");if (StrUtil.isBlank(token)){// token不存在,说明当前用户未登录,不需要刷新直接放行return true;}// 2、判断用户是否存在String tokenKey = LOGIN_USER_KEY + token;Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(tokenKey);if (userMap.isEmpty()){// 用户不存在,说明当前用户未登录,不需要刷新直接放行return true;}// 3、用户存在,则将用户信息保存到ThreadLocal中,方便后续逻辑处理,比如:方便获取和使用用户信息,Redis获取用户信息是具有侵入性的UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);ThreadLocalUtls.saveUser(BeanUtil.copyProperties(userMap, UserDTO.class));// 4、刷新token有效期stringRedisTemplate.expire(token, LOGIN_USER_TTL, TimeUnit.MINUTES);return true;}
}
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {// new出来的对象是无法直接注入IOC容器的(LoginInterceptor是直接new出来的)// 所以这里需要再配置类中注入,然后通过构造器传入到当前类中@Resourceprivate StringRedisTemplate stringRedisTemplate;@Overridepublic void addInterceptors(InterceptorRegistry registry) {// 添加登录拦截器registry.addInterceptor(new LoginInterceptor())// 设置放行请求.excludePathPatterns("/user/code","/user/login","/blog/hot","/shop/**","/shop-type/**","/upload/**","/voucher/**").order(1); // 优先级默认都是0,值越大优先级越低// 添加刷新token的拦截器registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);}
}

需要注意的是 我这里用到的是qq邮箱来发验证码 所以还需要配置qq邮箱的工具类

package com.hmdp.utils;import javax.mail.*;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeMessage.RecipientType;
import java.util.*;public class MailUtils {public static void main(String[] args) throws MessagingException {sendTestMail("收件人邮箱@qq.com", new MailUtils().achieveCode());}public static void sendTestMail(String email, String code) throws MessagingException {Properties props = new Properties();props.put("mail.smtp.auth", "true");props.put("mail.smtp.host", "smtp.qq.com");props.put("mail.smtp.port", "465");props.put("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory");props.put("mail.smtp.socketFactory.port", "465");props.put("mail.smtp.socketFactory.fallback", "false");props.put("mail.user", "198733702@qq.com");props.put("mail.password", "lvxzwwixjiewcbcf");Authenticator authenticator = new Authenticator() {protected PasswordAuthentication getPasswordAuthentication() {String userName = props.getProperty("mail.user");String password = props.getProperty("mail.password");return new PasswordAuthentication(userName, password);}};Session mailSession = Session.getInstance(props, authenticator);MimeMessage message = new MimeMessage(mailSession);InternetAddress from = new InternetAddress(props.getProperty("mail.user"));message.setFrom(from);InternetAddress to = new InternetAddress(email);message.setRecipient(RecipientType.TO, to);message.setSubject("Kyle's Blog 邮件测试");message.setContent("尊敬的用户:你好!\n注册验证码为:" + code + "(有效期为一分钟,请勿告知他人)", "text/html;charset=UTF-8");Transport.send(message);}public static String achieveCode() {String[] beforeShuffle = new String[]{"2", "3", "4", "5", "6", "7", "8", "9","A", "B", "C", "D", "E", "F", "G", "H","I", "J", "K", "L", "M", "N", "P", "Q","R", "S", "T", "U", "V", "W", "X", "Y", "Z","a", "b", "c", "d", "e", "f", "g", "h","i", "j", "k", "l", "m", "n", "p", "q","r", "s", "t", "u", "v", "w", "x", "y", "z"};List<String> list = Arrays.asList(beforeShuffle);Collections.shuffle(list);StringBuilder sb = new StringBuilder();for (String s : list) {sb.append(s);}return sb.substring(3, 8);}
}

这段代码不仅配置了工具类 还写了随机生成验证码的方法

  mail:host: smtp.qq.comport: 465username: 198733702@qq.compassword: xprotocol: smtps

不要忘记配置qq邮箱的yml文件

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/389.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Kafka入门-集群基础环境搭建(JDK/Hadoop 部署 + 虚拟机配置 + SSH 免密+Kafka安装启动)

Kafka 简介 传统定义&#xff1a;Kafka是一个分布式的基于发布/订阅模式的消息队列&#xff0c;应用于大数据实时处理领域。 Kafka最新定义&#xff1a;Apache Kafka是一个开源分布式事件流平台&#xff0c;被数千家公司用于高性能数据管道、流分析、数据集成和关键任务应用…

从OSI到TCP/IP:网络协议的演变与作用

个人主页&#xff1a;chian-ocean 文章专栏-NET 从OSI到TCP/IP&#xff1a;网络协议的演变与作用 个人主页&#xff1a;chian-ocean文章专栏-NET 前言网络发展LANWAN 协议举个例子&#xff1a; 协议的产生背景 协议的标准化OSI模型参考OSI各个分层的作用各层次的功能简介 TCP/…

Java观察者模式深度解析:构建松耦合事件驱动系统的艺术

目录 观察者模式基础解析核心结构与实现原理Java内置观察者实现Spring框架中的高级应用典型应用场景与实战案例观察者模式变体与优化常见问题与最佳实践总结与未来展望1. 观察者模式基础解析 1.1 模式定义与核心思想 观察者模式(Observer Pattern)是一种行为型设计模式,它…

前端(vue)学习笔记(CLASS 7):vuex

vuex概述 vuex是一个vue的状态管理工具&#xff0c;状态就是数据 大白话&#xff1a;vuex是一个插件&#xff0c;可以帮我们管理vue通用的数据&#xff08;多组件共享的数据&#xff09; 场景 1、某个状态在很多个组件来使用&#xff08;个人信息&#xff09; 2、多个组件…

论文中pdf图片文件太大怎么办

文章目录 1.使用pdf文件的打印功能将文件导出2.操作3.前后文件大小对比 1.使用pdf文件的打印功能将文件导出 该方法在保证清晰度的同时&#xff0c;内存空间也能实现减少&#xff08;如果使用线上的压缩pdf工具&#xff0c;清晰度会直线下降&#xff09; 2.操作 点击文件—&…

Java并发编程:读写锁与普通互斥锁的深度对比

在Java并发编程中&#xff0c;锁是实现线程安全的重要工具。其中&#xff0c;普通互斥锁&#xff08;如synchronized和ReentrantLock&#xff09;和读写锁&#xff08;ReentrantReadWriteLock&#xff09;是两种常用的同步机制。本文将从多个维度深入分析它们的区别、适用场景及…

C#面向对象实践项目--贪吃蛇

目录 一、项目整体架构与核心逻辑 二、关键类的功能与关系 1. 游戏核心管理类&#xff1a;Game 2. 场景接口与基类 3. 具体场景类 4. 游戏元素类 5. 基础结构体与接口 三.类图 四、核心流程解析 五、项目可优化部分 一、项目整体架构与核心逻辑 该项目运用场景管理模…

传输层协议:网络通信的关键纽带

在计算机网络的复杂体系中&#xff0c;传输层协议扮演着举足轻重的角色&#xff0c;它如同桥梁一般&#xff0c;连接着应用层与网络层&#xff0c;为不同主机上的应用进程提供端到端的通信服务&#xff0c;确保数据能够准确、高效地在网络中传输。深入理解传输层协议&#xff0…

高效易用的 MAC 版 SVN 客户端:macSvn 使用体验

高效易用的 MAC 版 SVN 客户端&#xff1a;macSvn 使用体验 下载安装使用总结 最近有个项目要使用svn, 但是mac缺乏一款像 Windows 平台 TortoiseSVN 那样全面、高效且便捷的 SVN 客户端工具, 直到博主找到了该工具本文将结合实际使用体验&#xff0c;详细介绍 macSvn工具的核心…

LeetCode 热题 100 394. 字符串解码

LeetCode 热题 100 | 394. 字符串解码 大家好&#xff01;今天我们来探讨一道非常有趣的算法题目——LeetCode 394. 字符串解码。这道题考察了我们对栈这种数据结构的理解和应用能力&#xff0c;同时也涉及到了字符串的处理技巧。接下来&#xff0c;我将详细地为大家解析这道题…

详解一下RabbitMQ中的channel.Publish

函数定义&#xff08;来自 github.com/streadway/amqp&#xff09; func (ch *Channel) Publish(exchange string,key string,mandatory bool,immediate bool,msg Publishing, ) error这个方法的作用是&#xff1a;向指定的交换机 exchange 发送一条消息 msg&#xff0c;带上路…

docker使用sh脚本创建容器,保持容器正常运行,异常关闭后马上重启

docker run -d --name dadeName \--memory5120m \-p 40060:80 \-p 40061:3306 \-v "$data:$dockerData" \-v "$img:$dockerImg" \--restartalways \ # 关键参数&#xff1a;总是重启dade:120 \/bin/bash -c "/www/start.sh && tail -f /dev/…