redis实现短信登录(Session共享)

一,基于Session实现登录

思维导图
image

1.1 发送短信验证码

  1. 控制层
    @Resource
    private IUserService userService;
    /**
     * 发送手机验证码
    */
    @PostMapping("code")
    public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {
        // 发送短信验证码并保存验证码
    
        return userService.sendCode(phone,session);
    }
    
  2. 业务层
    @Override
    public Result sendCode(String phone, HttpSession session) {
        //1.校验手机号
        if(RegexUtils.isPhoneInvalid(phone)){
            //2.如果不符合返回错误信息
            return Result.fail("手机号格式错误!");
        }
        //3.符合就生成验证码
        String code = RandomUtil.randomNumbers(6);
        //4.保存验证码到session中
        session.setAttribute("code",code);
        //5.发送验证码(需要调用阿里云短信发送三方平台,不是重点,跳过即可)
        log.info("发送短信验证码成功,验证码:{}",code);
        //返回ok
        return Result.ok();
    }
    

1.2 验证码登录

  1. 数据接收对象

    @Data
    public class LoginFormDTO {
        //手机号
        private String phone;
        //验证码
        private String code;
        //密码(因为支持密码登录也支持验证码登录)
        private String password;
    }
    
  2. 控制层

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

    @Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
        //1.校验手机号
        String phone = loginForm.getPhone();
        if(RegexUtils.isPhoneInvalid(phone)){
            //2.如果不符合,返回错误信息
            return Result.fail("手机号错误!");
        }
        //2.校验验证码
        Object cacheCode = session.getAttribute("code");
        if(cacheCode==null || !cacheCode.toString().equals(loginForm.getCode())){
            //3.不一致,报错
            return Result.fail("验证码错误!");
        }
        //4.一致,根据手机号查询用户
        User user = query().eq("phone", phone).one();
        //5.判断用户是否存在
        if(user==null){
            //6.不存在,则创建新用户并保持
            user=createUserWithPhone(phone);
        }
        //6.保持用户信息到session中
        session.setAttribute("user",user);
    
        return Result.ok();
    }
    
    private User createUserWithPhone(String phone) {
        //1.创建用户
        User user = new User();
        user.setPhone(phone);
        user.setNickName("User_"+RandomUtil.randomNumbers(10));
        //2.保存用户
        save(user);
        return user;
    }
    

1.3 登录验证功能

拦截器实现登录校验
image

public class LoginInterceptor implements HandlerInterceptor {
    
    private static final ThreadLocal<User> t1= new ThreadLocal<>();
    
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //1. 获取session
        HttpSession session = request.getSession();
        //2. 获取session中的用户
        Object user = session.getAttribute("");
        //3.判断用户是否存在
        if(user == null){
            //4. 不存在,拦截,返回401状态码
            response.setStatus(401);
            return false;
        }
        //5. 存在,保存用户信息到ThreadLocal
        t1.set( (User) user);
        //6. 放行
        return true;
    }
    
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        //移除用户
        t1.remove();
    }
}

二,集群的Session共享问题

image

三,基于Redis代替Session实现共享登录

image

3.1 用Redis替换Session发送短信验证码

唯一的区别就是将code存储从session变成了redis里面存储

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @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("login:code"+phone,code,2, TimeUnit.MINUTES);
        //5.发送验证码(需要调用阿里云短信发送三方平台,不是重点,跳过即可)
        log.info("发送短信验证码成功,验证码:{}",code);
        //返回ok
        return Result.ok();
    }

3.2 用Redis替换Session验证码登录,注册

区别就是将用户数据存到了Redis里面而不是Session里面

public Result login(LoginFormDTO loginForm, HttpSession session) {
        //1.校验手机号
        String phone = loginForm.getPhone();
        if(RegexUtils.isPhoneInvalid(phone)){
            //2.如果不符合,返回错误信息
            return Result.fail("手机号错误!");
        }
        //3.从redis中获取code进行校验
        String cacheCode = stringRedisTemplate.opsForValue().get("login:code" + phone);

        if(cacheCode==null || !cacheCode.equals(loginForm.getCode())){
            //4.不一致,报错
            return Result.fail("验证码错误!");
        }
        //5.一致,根据手机号查询用户
        User user = query().eq("phone", phone).one();
        //6.判断用户是否存在
        if(user==null){
            //7.不存在,则创建新用户并保持
            user=createUserWithPhone(phone);
        }
        //8.保持用户信息到redis中
        //生成随机token作为登录令牌
        String token = UUID.randomUUID().toString();
        //将User对象转化为hash存储
        UserDTO userDto=BeanUtil.copyProperties(user, UserDTO.class);
                //我们用的是StringRedisTemplate,他要求key和value都是String类型
        //我们的这个UserDTO里面的id是Long类型的,直接转会失败,需要我们手动修改
        Map<String, Object> userMap = BeanUtil.beanToMap(userDto,new HashMap<>(),
                //创建做出拷贝时的选项
                CopyOptions.create()
                        //忽略一些空的值
                        .setIgnoreNullValue(true)
                        //对字段值的修改器
                        .setFieldValueEditor(
                                //将Long修改为String类型
                                (fieldName,fieldValue)-> fieldValue.toString()
                        )
        );
        stringRedisTemplate.opsForHash().putAll("login:token:"+token,userMap);
        //设置key的有效期为30分钟
        stringRedisTemplate.expire("login:token:"+token,30,TimeUnit.MINUTES);
        //返回token

        return Result.ok(token);
    }

3.3 用Redis替换Session登录验证功能

public class LoginInterceptor implements HandlerInterceptor {
    private StringRedisTemplate stringRedisTemplate;
    //构造方法注入,需要在拦截器配置里那边传入
    public LoginInterceptor(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)){
           //不存在,拦截返回401状态码
           response.setStatus(401);
           return false;
       }
       //2.基于token获取user信息
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries("login:token:" + token);
        //3.判断用户是否存在
        if(userMap.isEmpty()){
            response.setStatus(401);
            return false;
        }
        //4.将Map转化为UserDto对象
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
        //5.将用户信息存储到ThreadLocal
        UserHolder.saveUser(userDTO);
        //6.刷新token有效期
        stringRedisTemplate.expire("login:token:" + token,30, TimeUnit.MINUTES);
        //7.放行
        return true;

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        //移除用户
        UserHolder.removeUser();
    }
}
posted @ 2024-07-03 17:08  wdadwa  阅读(55)  评论(0)    收藏  举报