黑马点评-短信登录

bash 复制代码
@Override
public Result sendCode(String phone) {
//        1.检验手机号
if (RegexUtils.isPhoneInvalid(phone)) {
//  这里抛出异常和return fail有什么区别吗?---------> 有区别,抛出异常会被全局异常处理器捕获,返回fail不会
throw new RuntimeException("手机号格式不正确");
}
//        2.生成验证码
String code = RandomUtil.randomNumbers(6);
//        3.保存验证码到session  ---------> 保存到redis中,redis名字、值、过期时间、时间单位
//        session.setAttribute("code", code);
stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);

//        4.发送验证码到手机
//        注意这里的log是lombok的@Slf4j注解生成的,不然只能写一个参数
log.debug("发送验证码:{},到手机:{}", code, phone);
return Result.ok();
}

这段代码是一个Java方法,用于向指定的手机号发送验证码并处理相关逻辑。下面是对代码中关键部分的解释和你提到的几点疑问的解答:

  1. 关于异常处理

    • 当使用 throw new RuntimeException("手机号格式不正确"); 时,如果手机号格式验证失败,会抛出一个运行时异常。这个异常如果没有被捕获,会直接中断当前方法的执行,并且,如果项目中配置了全局异常处理器(如Spring的@ControllerAdvice),这个异常会被全局异常处理器捕获,然后按照全局异常处理器中定义的方式处理,通常会转换成特定的错误响应给客户端。
    • 而如果直接 return Result.fail("手机号格式不正确"); (假设Result类有这样一个静态方法表示操作失败并附带错误信息),则是一种更直接的错误处理方式,不涉及异常抛出,直接通过返回值告知调用方操作失败及原因。这种方式适用于不需要中断程序执行流,或者不需要全局统一处理该类型错误的场景。
  2. 保存验证码到Redis

    • 代码中使用了Spring Data Redis的StringRedisTemplate来操作Redis数据库,将生成的验证码保存到Redis中,键为LOGIN_CODE_KEY + phone,这样做可以确保每个手机号对应的验证码存储是独立的,并且设置了验证码的有效时间为LOGIN_CODE_TTL分钟,这样可以自动清理过期验证码,避免数据冗余和安全问题。
  3. 日志记录

    • 使用Lombok的@Slf4j注解自动生成的日志对象(如log)可以简化日志记录的代码。log.debug(...)语句会在调试模式下记录消息,帮助开发者追踪验证码的发送过程,包括发送的验证码内容和目标手机号。如果不使用Lombok,则需要手动创建日志对象,例如:private static final Logger log = LoggerFactory.getLogger(YourClassName.class);

总结来说,这段代码实现了手机号格式校验、验证码生成、验证码存储至Redis以及日志记录的功能,同时展示了在遇到错误时使用异常与直接返回错误结果两种不同处理方式的区别。

在代码片段中,使用的Redis数据类型是 字符串(String)。具体体现在这一行代码:

java 复制代码
stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);

这里,opsForValue() 方法用于操作Redis中的字符串值。set 方法则用于设置键值对,其中键是LOGIN_CODE_KEY + phone,值是生成的验证码code,并且设置了过期时间LOGIN_CODE_TTL分钟。这种操作非常适合用来存储临时数据,比如短信验证码,因为字符串类型是Redis中最基本且灵活的数据类型,能够存储任何二进制安全的数据,包括数字、文本、甚至是序列化的对象。

bash 复制代码
@Override
public Result login(LoginFormDTO loginForm) {
//        1.检验手机号  --------->  因为每个请求都是单独的,使用还要再检查一次
String phone = loginForm.getPhone();
if (RegexUtils.isPhoneInvalid(phone)) {
return Result.fail("手机号格式错误!");
}
//        2.检验验证码   --  从redis中获取
//        Object cacheCode = session.getAttribute("code");
String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
String code = loginForm.getCode();
if (cacheCode == null || !cacheCode.equals(code)) {
System.out.println("cacheCode = " + cacheCode);
return Result.fail("验证码错误");
}
//        3.检验用户是否存在
//        法1:最简洁的用法,但是有硬编码
//        User user = query().eq("phone", phone).one();
//        法2:使用lambda表达式,减少硬编码
//        User user = this.lambdaQuery().eq(User::getPhone, loginForm.getPhone()).one();
//        法3:复杂一点,但多了一个isNotBlank的动态查询
LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(StringUtils.isNotBlank(phone), User::getPhone, phone);
User user = userMapper.selectOne(queryWrapper);
//        如果不存在则创建用户
if (user == null) {
user = createUserWithPhone(phone);
}

//        4.保存用户到session  -- 保存到token中
//        session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class));
String token = UUID.randomUUID().toString(true);
UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
// 因为user的id是long类型的,但是StringRedisTemplate只支持String类型的key-value,因此要需要⾃定义map映射规将user转成map后进⾏hash存储
// userDTO:要转换为Map的Java对象       new HashMap<>():存储转换后的Map的容器
Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),
// 忽略userDTO对象中的空值属性,即那些值为null的属性不会被放入userMap中
CopyOptions.create().setIgnoreNullValue(true)
// 将属性值放入userMap前,将属性值转换为其字符串表示形式
.setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));

//        5.存储
String tokenKey = LOGIN_USER_KEY + token;
stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);
//        设置过期时间
stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);
return Result.ok(token);
}

该段Java代码是一个登录方法的实现,主要功能是验证用户通过手机号和验证码登录。以下是关键步骤的解析:

  1. 校验手机号格式:首先使用正则表达式验证传入的手机号是否合法。

  2. 校验验证码 :从Redis中根据手机号取出之前保存的验证码(使用stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone)),并与用户提交的验证码进行比较。如果Redis中没有验证码或者不匹配,则登录失败。

  3. 查询用户是否存在

    • 使用Lambda表达式构建查询条件,动态地检查手机号是否非空,并据此查询数据库中的用户记录。这样做可以避免硬编码查询条件,增加代码的灵活性和可维护性。
    • 如果用户不存在,则通过createUserWithPhone(phone)方法创建新用户并保存到数据库。
  4. 用户认证成功处理

    • 生成一个随机的UUID作为token。
    • 将查询到的用户实体转化为UserDTO对象,然后进一步转化为一个Map(这一步是为了方便以HashMap的形式存储到Redis中,因为Redis的Hash数据结构适合存储对象)。
    • 将用户信息以HashMap的形式存入Redis,键为LOGIN_USER_KEY + token,并且设置相应的过期时间,这样就实现了基于token的会话管理,而不是传统的session。
  5. 返回结果:最后返回一个包含生成的token的结果对象,前端可以使用这个token来进行后续的认证请求。

此登录逻辑结合了安全性(如验证码校验)、灵活性(动态查询构造)以及扩展性(使用token而非session管理用户状态),是现代Web应用中常见的认证流程实现方式。

在上述登录方法中,使用的Redis数据类型是Hash

当用户登录成功后,代码片段通过以下命令将用户信息存储到Redis中:

java 复制代码
stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);

这里,opsForHash() 方法表明操作的是Redis的Hash数据结构。Hash允许我们将一个键关联到多个字段-值对,非常适合用来存储对象,其中键(tokenKey)代表了这个存储单元,而userMap中的每个键值对则代表了用户的一个属性及其值。这种方式使得可以通过一个键快速访问到用户的所有信息,并且便于单独修改或删除某个属性,非常适合用于会话管理和用户信息的临时存储场景。此外,通过设置过期时间,还可以自动清理不再需要的会话信息,保持Redis内存的有效利用。

为什么保存验证码用String类型,保存用户信息用hash类型

在Redis中选择使用String类型存储验证码和Hash类型存储用户信息,主要是基于以下考虑:

  1. 验证码的特性

    • 简单性:验证码一般只是一个简短的字符串,比如6位数字或字母组合,其数据结构相对简单,不需要复杂的字段关联。
    • 读写频繁:验证码的生成和验证是高频率的操作,每次验证后可能还需要清除或标记已使用的验证码。String类型提供了快速的读写能力,适合这种简单且高频的访问模式。
    • 生命周期短:验证码的有效期较短,通常几分钟到几十分钟,使用String类型可以直接设置键值对的过期时间,实现自动清理过期数据,简单高效。
  2. 用户信息的特性

    • 结构化数据:用户信息通常包含多个属性,如用户名、密码、邮箱、电话等,这些信息之间存在逻辑关联,形成一个结构化的数据对象。
    • 更新灵活性:用户信息可能需要部分更新,例如修改密码或邮箱,而不会影响其他信息。Hash类型允许对单个字段进行操作(如HSET, HGET等),提供了针对字段级别的操作能力,非常适用于需要频繁更新或查询部分属性的场景。
    • 存储效率:虽然理论上String也可以通过序列化整个对象来存储用户信息,但这样做会导致每次读写都需要序列化和反序列化整个对象,效率较低。而Hash类型仅需操作相关的字段,提高了存储和访问的效率。

综上所述,选择String类型存储验证码是因为其简单、高效且生命周期短的特点,而选择Hash类型存储用户信息则是为了适应用户信息的结构化特性和提高数据操作的灵活性及效率。

相关推荐
程序员-珍16 分钟前
使用openapi生成前端请求文件报错 ‘Token “Integer“ does not exist.‘
java·前端·spring boot·后端·restful·个人开发
2401_8572979142 分钟前
招联金融2025校招内推
java·前端·算法·金融·求职招聘
福大大架构师每日一题1 小时前
23.1 k8s监控中标签relabel的应用和原理
java·容器·kubernetes
金灰1 小时前
HTML5--裸体回顾
java·开发语言·前端·javascript·html·html5
菜鸟一皓1 小时前
IDEA的lombok插件不生效了?!!
java·ide·intellij-idea
爱上语文1 小时前
Java LeetCode每日一题
java·开发语言·leetcode
bug菌1 小时前
Java GUI编程进阶:多线程与并发处理的实战指南
java·后端·java ee
程序猿小D2 小时前
第二百六十九节 JPA教程 - JPA查询OrderBy两个属性示例
java·开发语言·数据库·windows·jpa
极客先躯3 小时前
高级java每日一道面试题-2024年10月3日-分布式篇-分布式系统中的容错策略都有哪些?
java·分布式·版本控制·共识算法·超时重试·心跳检测·容错策略
夜月行者3 小时前
如何使用ssm实现基于SSM的宠物服务平台的设计与实现+vue
java·后端·ssm