zl程序教程

您现在的位置是:首页 >  数据库

当前栏目

基于Redis的短信验证登录

2023-04-18 14:21:00 时间


在这里插入图片描述

1、用户调用发送短信验证码接口

用户调用sendCode()接口,把phone传到后端,后端对phone进行格式校验,如果通过校验,则生成6位数验证码,并保存到redis中,phone为key,code为value,注意设置有效期

    /**
     * 基于Redis的发送并保存验证码
     *
     * @param phone
     * @param session
     * @return
     */
    @Override
    public Result sendCode(final String phone, final HttpSession session) {
//         1.校验手机号是否正确,如果手机号不通过校验,则return
        if (RegexUtils.isPhoneInvalid(phone)) {
            return Result.fail("手机号码格式错误!请重新输入!");
        }
//         2.如果手机号通过校验,则自动生成6位数的随机验证码
        String code = RandomUtil.randomNumbers(6);
//         3.把验证码保存到redis中
        stringRedisTemplate.opsForValue().set(RedisConstants.LOGIN_CODE_KEY + phone, code, RedisConstants.LOGIN_CODE_TTL, TimeUnit.MINUTES);
        log.debug("验证码生成成功:" + code);
//         4.返回ok
        return Result.ok("验证码发送成功!");
    }

2、用户调用登录/注册接口

  1. 用户调用login(LoginData loginForm)接口,把phone跟code传到后端

  2. 后端先对phone进行格式校验,如果不通过校验,则返回提示信息

  3. 如果通过校验,则根据phone从redis中查询缓存的cacheCode,跟用户传过来的code进行匹配,如果不匹配或者cacheCode已经过期,那么提示用户验证码错误或者验证码已经过期

  4. 如果phone格式正确并且code跟缓存中的cacheCode匹配,则根据phone字段在数据库中查找用户的数据,如果用户不存在,则说明用户发送验证码是首次登录,需要进行信息注册,那么把user保存到数据库中

  5. 如果数据库中的用户已经存在,说明用户发送验证码和手机号是为了登录,则自动生成token作为key,用户信息user作为value,选择hash数据类型或者string类型存储到Redis中,并设置有效期

  6. 把生成的token返回给前端,以便前端把token保存到浏览器的缓存中,在每次发送请求之前在浏览器缓存中中取出token,放到请求头的authorization中,以便请求发送到后端时,可以在拦截器中根据token从redis缓存中获取用户信息,判断用户是否处于登录状态

    /**
     * 用户登录或者注册
     *
     * @param loginForm
     * @return
     */
    @Override
    public Result login(LoginFormDTO loginForm) {
//         1.用户传来phone和code,先校验手机号是否正确
        String phone = loginForm.getPhone();
//         2.如果手机号不通过校验,则返回手机号格式错误
        if (RegexUtils.isPhoneInvalid(phone)) {
            return Result.fail("手机号格式错误!请重新输入!");
        }
        String code = loginForm.getCode();
//         3.从redis中查询cacheCode,如果不匹配,则返回验证码错误
        String cacheCode = stringRedisTemplate.opsForValue().get(RedisConstants.LOGIN_CODE_KEY + phone);
        if (cacheCode == null || !cacheCode.equals(code)) {
            return Result.fail("验证码错误!请重新输入!");
        }
//        4.如果匹配,则可以登录,但是如果用户是首次登录,则数据库中没有该用户相关数据,需要创建一个新用户
//        5.根据phone从数据库中查询用户信息
        User user = query().eq("phone", phone).one();
        if (user == null) {
            user = createUserWithPhone(phone);//此时数据库中一定有了用户
        }
//        6.用token作为redis中保存用户信息的key
//        使用hutool的工具包生成token
        String token = UUID.randomUUID().toString(true);
//        7.把user转换成UserDTO
        UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
//        8.转换成Map进行存储,因为存储到Redis选择Hash结构,需要注意BeanUtil工具类只能转换类中String类型的字段,所以需要自定义字段转换器,把字段值转换成String类型
        Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(), CopyOptions.create().setIgnoreNullValue(true).setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));
//        9.把userMap存储到Redis中
        stringRedisTemplate.opsForHash().putAll(RedisConstants.LOGIN_USER_KEY + token, userMap);
//        10.设置token的有效期
        stringRedisTemplate.expire(RedisConstants.LOGIN_USER_KEY + token,RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
//        11.返回token给前端,前端会把token保存到浏览器缓存中,然后每次发送请求事,请求拦截器会把token添加到请求头authorization中
//        这样每次发送请求,只要有token,就说明用户处于登录状态
        return Result.ok(token);
    }

    private User createUserWithPhone(String phone) {
        User user = new User();
        user.setNickName(USER_NICK_NAME_PREFIX + RandomUtil.randomNumbers(10));
        user.setPhone(phone);
        save(user);
        return user;
    }

3、用户调用校验接口

用户调用me()接口,从ThreadLocal中获取用户信息,返回给前端

    @GetMapping("/me")
    public Result me() {
//        返回UserDTO,省略掉了敏感字段
        return Result.ok(UserHolder.getUser());
    }

4、SpringMvc拦截器注册

@Configuration
public class MvcConfig implements WebMvcConfigurer {

    @Resource
    private StringRedisTemplate stringRedisTemplate;
    /**
     * 注册拦截器
     *
     * @param registry
     */
    @Override
    public void addInterceptors(final InterceptorRegistry registry) {
//        添加登录拦截器
        registry.addInterceptor(new LoginInterceptor()).excludePathPatterns(
            "/user/login",//用户登录接口
            "/shop/**",//购物模块的浏览
                "/shop-type/**",//商品类型
                "/upload/**",//用户上传博客模块
                "/blog/hot",//博客热点接口
                "/voucher/**",//优惠卷模块
                "/user/code"//获取验证码接口
        ).order(1);
//        添加刷新token拦截器,拦截并校验所有页面
        registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).order(0);
    }
}

5、token刷新拦截器

注册RedisFreshInterceptor拦截器,实现HandlerInterceptor接口,重写调用controller前的方法preHandle,对所有页面进行拦截,判断是否存在token,如果不存在,则直接放行给后面进行登录校验或者普通页面,如果Redis中有token,则刷新token的有效期,并从Redis中获取用户信息,保存到本地线程对象ThreadLocal中,以便me()接口进行用户登录状态校验

public class RefreshTokenInterceptor implements HandlerInterceptor {
    private StringRedisTemplate stringRedisTemplate;

    public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    /**
     * 刷新用户token
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    @Override
    public boolean preHandle(final HttpServletRequest request, final HttpServletResponse response, final Object handler) throws Exception {
//        1.获取请求头中的token
        String token = request.getHeader("authorization");
        if (StrUtil.isBlank(token)) {//如果没有token,则一定没有用户信息,不需要刷新
            return true;//放行给后面校验
        }
//        2.从redis中获取token对应的用户信息
        String userKey = RedisConstants.LOGIN_USER_KEY + token;
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(userKey);
        if(userMap.isEmpty()){//如果token已经过期,也直接放行
            return true;
        }
//        3.把userMap转换成UserDTO
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
//        4.把UserDTO存放到ThreadLocal中,方便登录拦截器获取校验
        UserHolder.saveUser(userDTO);
//        5.刷新token的有效期
        stringRedisTemplate.expire(userKey, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
//        6.放行
        return true;
    }

    @Override
    public void afterCompletion(final HttpServletRequest request, final HttpServletResponse response, final Object handler, final Exception ex) throws Exception {
        HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
    }
}

6、登录拦截器

对普通页面进行放行,对需要用户登录状态的页面进行拦截,从本地线程对象ThreadLocal中获取用户信息,如果用户信息不存在,则说明用户未进行登录,进行请求拦截不放行,提示用户进行登录,返回状态码401

public class LoginInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(final HttpServletRequest request, final HttpServletResponse response, final Object handler) throws Exception {
//        1.从本地线程对象中获取userDTO
        UserDTO userDTO = UserHolder.getUser();
//        2.判断是否有用户信息,如果没有,则说明用户没有登录
        if(userDTO == null){
            response.setStatus(401);
            return false;
        }
//        3.放行
        return true;
    }

    @Override
    public void afterCompletion(final HttpServletRequest request, final HttpServletResponse response, final Object handler, final Exception ex) throws Exception {
        HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
    }
}