🍖 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 = 2L stringRedisTemplate.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中 // 生成随机token String 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 = 30L stringRedisTemplate.expire(RedisConstants.LOGIN_USER_KEY + token, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES); // 返回前端token return 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; }