Redis篇:实现短信登录

实现的是黑马点评的手机号短信验证码登录功能

1.实现流程

发送验证码:

用户在提交手机号后,会使用正则表达式校验手机号是否合法,如果不合法,则要求用户重新输入手机号

如果手机号合法,后台此时生成对应的验证码,同时将验证码保存到redis,然后再通过短信的方式将验证码发送给用户(我们这里只模拟,不会真使用短信服务来发送短信)

短信验证码登录、注册:

用户将验证码和手机号进行输入,后台从redis中拿到当前验证码,然后和用户输入的验证码进行校验,如果不一致,则无法通过校验,如果一致,则后台根据手机号查询用户,如果用户不存在,则为用户创建账号信息,保存到数据库,无论是否存在,都会将用户信息保存到redis中,方便后续获得当前登录信息

校验登录状态:

用户在请求时候,后台从请求头中获取token,并基于token获取redis中的用户信息,如果没有token信息,则进行拦截,如果有token信息,则将用户信息保存到threadLocal中,并且放行

2.实现发送短信验证码功能

页面流程

Controller:

/**
     * 发送手机验证码
     */
    @PostMapping("/code")
    public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {
        //发送短信验证码并保存验证码
        return userService.sendCode(phone,session);
    }

ServiceImpl:

/**
     * 发送短信验证码
     * @param phone
     * @param session
     * @return
     */
    @Override
    public Result sendCode(String phone, HttpSession session) {
        //1.校验手机号
        if (RegexUtils.isPhoneInvalid(phone)) {
            //2.如果不符合,返回错误信息
            return Result.fail("手机号格式错误!");
        }
        //3.符合,生成验证码
        String code = RandomUtil.randomNumbers(6);

        //4.保存验证码到redis
        stringRedisTemplate.opsForValue().set(RedisConstants.LOGIN_CODE_KEY + phone,code,RedisConstants.LOGIN_CODE_TTL, TimeUnit.MINUTES);
        //5.发送验证码 (这里先模拟一下,真实开发需要调用第三方api,比如阿里云的短信服务)
        log.debug("发送短信成功!验证码:{}",code);
        //返回ok
        return Result.ok();

    }

此处使用了hutool包的RandomUtil生成6位数字的随机验证码。

3.实现登录功能:

Controller:

/**
     * 登录功能
     * @param loginForm 登录参数,包含手机号、验证码;或者手机号、密码
     */
    @PostMapping("/login")
    public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){
        // 实现登录功能
        return userService.login(loginForm,session);
    }

ServiceImpl:

/**
     * 登录功能
     * @param loginForm
     * @param session
     * @return
     */
    @Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
        //1.校验手机号
        String phone = loginForm.getPhone();
        if (RegexUtils.isPhoneInvalid(phone)){
            //1.1 不符合,返回错误信息
            return Result.fail("手机号格式错误!");
        }
        //2.从redis获取验证码,校验验证码
        String cacheCode = stringRedisTemplate.opsForValue().get(RedisConstants.LOGIN_CODE_KEY + phone);
        String code = loginForm.getCode();
        if(cacheCode == null || !cacheCode.equals(code)){
            //3.不一致,报错
            return Result.fail("验证码错误");
        }

        //4.一致,根据手机号查询用户
        User user = query().eq("phone", phone).one();

        //5.判断用户是否存在
        if(user == null){
            //6.不存在,创建新用户,并保存
            user = createUserWithPhone(phone);

        }

        //7.保存用户信息到redis
        //7.1随机生成token,作为登录令牌
        String token = UUID.randomUUID().toString(true);
        //7.2将User对象转为Hash存储
        UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
        //7.2.1把userDTO转化为Map形式(这个地方要设置转换类型,
        //因为id是Long类型,但是key的要求必须全都是String,直接转换会报错
        //(Long-> String错误))
        Map<String, Object> userMap = BeanUtil.beanToMap(userDTO,new HashMap<>(),
                CopyOptions.create().setIgnoreNullValue(true).setFieldValueEditor((fieldName,fieldValue)-> fieldValue.toString()));
        String tokenKey = LOGIN_USER_KEY + token;
        //7.2.2 存储到Hash中
        stringRedisTemplate.opsForHash().putAll(tokenKey,userMap);

        //7.3 设置Token的有效期
        stringRedisTemplate.expire(tokenKey,RedisConstants.LOGIN_USER_TTL,TimeUnit.MINUTES);
        System.out.println(token);

        return Result.ok(token);
    }

4.校验登录状态

所以我们可以使用String结构,就是一个简单的key,value键值对的方式,但是关于key的处理,session他是每个用户都有自己的session,但是redis的key是共享的,咱们就不能使用code了

在设计这个key的时候,我们之前讲过需要满足两点

1、key要具有唯一性

2、key要方便携带

如果我们采用phone:手机号这个的数据来存储当然是可以的,但是如果把这样的敏感数据存储到redis中并且从页面中带过来毕竟不太合适,所以我们在后台生成一个随机串token,然后让前端带来这个token就能完成我们的整体逻辑了

设置拦截器:

此处我们使用两层拦截器:第一层是拦截所有请求,第二层是拦截那些需要登录的请求,第一层的目的是用来刷新token有效期,防止用户使用途中因为token(设定的有效期为30min)失效而导致登录失效。

第一层RefreshTokenInterceptor:

public class RefreshTokenInterceptor implements HandlerInterceptor {

    private StringRedisTemplate stringRedisTemplate;

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

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1.获取请求头中的token
        String token = request.getHeader("authorization");
        if (StrUtil.isBlank(token)) {
            return true;
        }
        // 2.基于TOKEN获取redis中的用户
        String key  = RedisConstants.LOGIN_USER_KEY + token;
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);
        // 3.判断用户是否存在
        if (userMap.isEmpty()) {
            return true;
        }
        // 5.将查询到的hash数据转为UserDTO
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
        // 6.存在,保存用户信息到 ThreadLocal
        UserHolder.saveUser(userDTO);
        // 7.刷新token有效期
        stringRedisTemplate.expire(key, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
        // 8.放行
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 移除用户
        UserHolder.removeUser();
    }
}

第二层LoginInterceptor:

public class LoginInterceptor implements HandlerInterceptor{

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1.判断是否需要拦截(ThreadLocal中是否有用户)
        if (UserHolder.getUser() == null) {
            // 没有,需要拦截,设置状态码
            response.setStatus(401);
            // 拦截
            return false;
        }
        // 有用户,则放行
        return true;
    }
}

然后配置个MvcConfig让拦截器生效(记得加@Configuration):

@Configuration
public class MvcConfig implements WebMvcConfigurer {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor())
                .excludePathPatterns(
                        "/user/code",
                        "/user/login",
                        "blog/hot",
                        "/shop/**",
                        "/shop-type/**",
                        "upload/**",
                        "voucher/**"
                ).order(1); //order设定顺序,0会在1之前执行
        registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**")
                .order(0);
    }
}

登录成功会跳转到/user/me这个接口(作用是从ThreadLocal中获取用户信息):

Controller:

 @GetMapping("/me")
    public Result me(){
        //获取当前登录的用户并返回
        UserDTO user = UserHolder.getUser();
        return Result.ok(user);
    }

UserHodel:

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();
    }
}

登录后效果:

​​​​​​​

相关推荐
向上的车轮15 分钟前
Django学习笔记二:数据库操作详解
数据库·django
编程老船长25 分钟前
第26章 Java操作Mongodb实现数据持久化
数据库·后端·mongodb
全栈师1 小时前
SQL Server中关于个性化需求批量删除表的做法
数据库·oracle
Data 3171 小时前
Hive数仓操作(十七)
大数据·数据库·数据仓库·hive·hadoop
BergerLee2 小时前
对不经常变动的数据集合添加Redis缓存
数据库·redis·缓存
gorgor在码农2 小时前
Mysql 索引底层数据结构和算法
数据结构·数据库·mysql
huapiaoy2 小时前
Redis中数据类型的使用(hash和list)
redis·算法·哈希算法
小徐敲java2 小时前
通用mybatis-plus查询封装(QueryGenerator)
mybatis
bug菌¹2 小时前
滚雪球学Oracle[6.2讲]:Data Guard与灾难恢复
数据库·oracle·data·灾难恢复·guard
一般路过糸.3 小时前
MySQL数据库——索引
数据库·mysql