Redis实现短信验证码登录
要保存验证码到Redis中,可以直接使用String类型进行存储,手机号作为key,value存储验证码
用户信息则使用hash进行存储,使用随机token为key存储用户数据
首先,明确验证码登录的流程
-
发送验证码
-
首先要验证用户输入的手机号是否符合格式
/** * 手机号正则 */ 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}$";
-
符合则生成验证码
-
将生成的验证码存储到Redis,key设置为login:code:phone的格式,并且设置有效期为2分钟
-
发送验证码
-
返回生成成功
public Result sendCode(String phone, HttpSession session) { //1.检验手机号 if(RegexUtils.isPhoneInvalid(phone)){ //2.不符合返回错误信息 return Result.fail("手机号格式错误"); } //3.符合生成验证码 String code = RandomUtil.randomNumbers(6); //4.保存验证码到Redis,并且设置有效期2分钟 redisTemplate.opsForValue().set(LOGIN_CODE_KEY+ phone,code,LOGIN_CODE_TTL, TimeUnit.MINUTES); //5.发送验证码(模拟发送) log.debug("发送验证码成功,验证码:{}",code); //返回ok return Result.ok(); }
在Redis中存储验证码
-
-
登录方法验证验证码
- 检验手机号格式
- 验证传入的验证码是否和redis中存储的验证码相同
- 查看用户是否存在,如果没有注册直接进行注册,如果存在则直接登录
- UUID随机生成作为登录令牌
- 将User对象转为存储基本信息的UserDTO
- 将UserDTO转为Hash存储在Redis中
- 设置有效期,如果半小时内用户不再访问网页则令牌过期(借助拦截器实现,如果用户在访问别的页面,那么必然经过拦截器,此时就为用户更新令牌的过期时间)
- 返回token给前端
首先,将登录方法逻辑完成
public Result login(LoginFormDTO loginForm, HttpSession session) { //检验手机号格式 if(RegexUtils.isPhoneInvalid(loginForm.getPhone())) { return Result.fail("手机号格式错误!"); } //验证验证码 String cacheCode = redisTemplate.opsForValue().get(LOGIN_CODE_KEY+ loginForm.getPhone()); String code = loginForm.getCode(); if(cacheCode == null || !cacheCode.equals(code)) { return Result.fail("验证码错误"); } //查看手机是否已注册 User user = query().eq("phone", loginForm.getPhone()).one(); //用户是否存在,不存在直接注册,存在则登录 if(user == null){ user = createUserWithPhone(loginForm.getPhone()); } //随机生成token作为登录令牌 String token = UUID.randomUUID().toString(true); //将User对象转为Hash存储 UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class); Map<String, Object> userMap = BeanUtil.beanToMap(userDTO,new HashMap<>(), CopyOptions.create().setIgnoreNullValue(true) .setFieldValueEditor((fieldName,filedValue)->filedValue.toString())); redisTemplate.opsForHash().putAll(LOGIN_USER_KEY+token,userMap); String tokenKey = LOGIN_USER_KEY + token; //设置有效期 redisTemplate.expire(tokenKey,LOGIN_USER_TTL,TimeUnit.SECONDS); //返回token return Result.ok(token); } private User createUserWithPhone(String phone){ User user = new User(); user.setPhone(phone); user.setNickName(USER_NICK_NAME_PREFIX+RandomUtil.randomString(10)); save(user); return user; }
登录成功则存储令牌信息到Redis
编写工具类存储UserDTO到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();
}
}
编写两个拦截器A、B,分别具备不同功能,用户的请求会先经过拦截器A再经过拦截器B。
-
拦截器A:A拦截一切路径,但是直接放行。目的是在A拦截器中刷新用户的令牌有效期,让用户在使用过程中令牌有效期始终为30min。拦截器A需要:
- 获取token
- 查询Redis中是否有该用户,有则获取用户信息
- 保存用户到ThreadLocal
- 刷新token有效期
- 放行
public class RefreshTokenInterceptor implements HandlerInterceptor { //因为这是自己写的类,并不是Spring配置的类,不能直接@Autowired注入SpringRedisTemplate,可以通过MvcController进行注入,在构造方法赋值 private StringRedisTemplate redisTemplate; public RefreshTokenInterceptor(StringRedisTemplate redisTemplate){ this.redisTemplate=redisTemplate; } @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //获取请求头中的token String token = request.getHeader("authorization"); //判断token是否为空 if(StrUtil.isBlank(token)){ return true; } //获取Redis中用户 String key = RedisConstants.LOGIN_USER_KEY + token; Map<Object, Object> userMap = redisTemplate.opsForHash().entries(key); //判断用户是否存在 if(userMap.isEmpty()){ return true; } //将hash数据转换成UserDTO对象 UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false); //存在则保存用户信息到ThreadLocal UserHolder.saveUser(userDTO); //刷新token有效期 redisTemplate.expire(key,RedisConstants.LOGIN_USER_TTL, TimeUnit.SECONDS); return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { HandlerInterceptor.super.afterCompletion(request, response, handler, ex); } }
-
拦截器B:B则拦截需要所有登陆的路径,目的是限制未登录状态下能访问的请求。拦截器B需要:
- 查询ThreadLocal中的用户
- 不存在则拦截,存在则放行
public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //判断ThreadLocal中是否有用户是否需要拦截 if(UserHolder.getUser() == null){ response.setStatus(401); return false; } return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { HandlerInterceptor.super.afterCompletion(request, response, handler, ex); } }
编写MvcConfig配置拦截器
public class MvcConfig implements WebMvcConfigurer {
@Autowired
private StringRedisTemplate redisTemplate;
@Override
public void addInterceptors(InterceptorRegistry registry){
//order表示优先级,数字越小越先执行,默认都是0,执行顺序按照添加顺序执行
registry.addInterceptor(new LoginInterceptor()).excludePathPatterns("" +
"/user/code","/user/login","/blog/hot","/shop/**"
,"/shop-type/**","/upload/**","/voucher/**").order(1);
registry.addInterceptor(new RefreshTokenInterceptor(redisTemplate)).addPathPatterns("/**").order(0);
}
}