黑马点评-验证码登录功能-redis

整体的工作流程

> 这里我直接使用redis了,而不是使用session。

1. 发送短信功能,会把验证码保存到redis上

2. 点击登录功能,校验你的手机号和验证码。将你当前登录账号的信息保存到redis上。

3. 校验登录状态,如果已经登录,判断redis上是否有该用户的信息。

发送短信功能

这里没有开启短信服务,所以使用简单的将验证码输入到控制台。

工作流程:

前端:

1. 点击【发送验证码】的时候,会先校验你的账号,如果你的账号不合理,是发送不了请求的。

后端:

  1. 接收到前端发送过来的请求,携带手机号码过来
  2. 后端再次校验你的手机号码是否合理
  3. 如果不合理,就直接返回错误信息即可。
  4. 如果合理,生成验证码
  5. 将验证码保存到redis上。注意这里的redis的 key 和 value的选择是很重要的,这个key我们是选择  【功能模块+手机号】,而value是选择String类型-->保存验证码
  6. 发送验证码
  7. 返回发送成功,这里不需要什么特别的返回值。

代码实现

请求的url地址:http://localhost:8080/api/user/code?phone=13434123456

请求的方式:post

请求参数是:phone

返回值:不需要

>请求Url上的:这个api表示是向tomcat发送请求的前缀,会被过滤掉。

- 手机的校验

- 验证码的生成

都是使用hutool包来做的 

测试效果

已经能够发送成功了,但是还没办法完成登录。 

点击登录的功能

工作流程

前端:

点击登录之后,将手机号码和验证码,携带过来发给送给后端

后端:

  1. 接收到前端发送过来的请求,携带手机号码,和验证码过来,和密码发送过来。所以我们需要一个entity来接收这个数据。我们定义一个LoginFormDto类。将前端发送过来的数据,保存到loginFormDto对象上
  2. 从loginFormDto对象,取出 phone这个值。然后再去校验这个phone是否合理
  3. 如果phone合理,通过phone从redis中找到对应的验证码
  4. 将loginFormDto上的验证码,和从redis上取出来的验证码作比较
  5. 如果验证码码不一样直接会犯错误。
  6. 如果验证码一样,判断当前手机号 是否在user表中存在,如果不存在,就创建user对象。
  7. 如果存在,直接存储到redis中。
  8. 然后返回的东西是什么,保存在redis上的key

代码实现

 

>

 

 测试

 

 

登录成功的校验功能

点击某些页面,你是不能访问的,你必须先登录之后才能访问

> 如果我们已经完成了:【短信发送】、【点击登录】、着两个功能的逻辑,不代表我们已经能够登录了,还差点东西。就是登录校验,你登录成功之后得有一个登录凭证。这个登录凭证主要是给后端的。前端你已经将token返回给前端了,浏览器知道你有这个东西了。但是有哪些网页你访问不了,你访问了就会自动跳转,虽然说自动跳转是前端完成的,前端的跳转时跟你后端返回的数据来进行跳转的,如果你返回的数据是前端需要的,那它还跳转干嘛?

解下来就使用拦截器,来完成这个登录凭证

  1. 使用拦截器来完成这个功能。为什么呢?因为每一个网页,我们都需要去登录校验,判断你是否已经登录。所以在拦截器的时候,如果没有登录,我们就可以拦截下来。

执行流程

这里我们这里使用的redis,把session换成redis即可。

  1. 因为我们是拦截器对吧,首先网页请求过来一定会访问我们这个拦截器。
  2. 如果我们登录过,我们会把 token返回给浏览器,然后存储在请求上对吧。这个为什么会放在请求头上面,是前端做的,我们后端负责把token返回给前端。然后前端存储在请求里面,它的头部信息是:authorization 这是前端设计好的。
  3. 我们拦截器的第一步是:先判断是否已经登陆过了。
  4. 如果没有登录过,拦截下来。但是不能全部都取拦截,比如有些页面我们必须放行,比如登录页面,如果这个都拦住,我们就没办法登录了。
  5. 如果已经登录过了,直接放行。

 

代码实现

1. 后端必须有一个自定义拦截器类,去实现HandlerIntercetor接口。重写两个方法,一直是请求前拦截,一个请求结束后处理。 就是线程开启处理的逻辑,线程结束后处理的逻辑

ublic 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,token就是存储在redis上的健,该值就是登录的用户
        String token = request.getHeader("authorization");//取出token
        if(StrUtil.isAllBlank(token)){//如果取出来的token是不存在的,就说明没有登录过,直接拦截
            return false;
        }
        //2.从redis上取出数据,从redis上取数据,就必须使用StringRedisTemplate。所以注入进来,但这里能使用@Authorized注入码?不行
        // 为什么因为我们这个配置器类,并没有交给spring管理。所以如果要注入,就只能使用构造器注入
        Map<Object, Object> entries = stringRedisTemplate.opsForHash().entries(LOGIN_USER_KEY + token);
        //3.判断这个entries是否为null
        if(entries.isEmpty()){
            //如果是null,直接返回错误信息就可以了
            response.setStatus(401);//401状态码表示未登录的意思
           //拦截器怎么返回 异常信息???没办法直接拦截就可以了
            return false;
        }
        //如果说redis上有值,某些页面也需要使用到这个用户信息,我们把用户信息存储在哪里?
        //如果我们的执行是一条龙,就是同一条线程的话,我们只需要把用户信息存储到ThreadLocal上
        // 难点是什么:取出来的是map数据,怎么将其转换为UserDto对象?使用工具类,将map转为Bean
        UserDTO userDTO = new UserDTO();
        BeanUtil.fillBeanWithMap(entries,userDTO,false);//我记得前面,我们存储到redis上的时候,里面的value全是String,我这个Dto的Id是long可以直接接收的。
        //接下来将UserDto保存到ThreadLocal上
        UserHolder.saveUser(userDTO);
        //保存到ThreadLocal上之后,我们去刷新一下在redis保存的时间。否则30分钟一到,我们这个redis就会过去。只要有访问到需要验证的页面,我们就会刷新token
        stringRedisTemplate.expire(LOGIN_USER_KEY + token,30, TimeUnit.MINUTES);
        //放行
        return true;
    }
    //就是本次这个页面结束之后,执行的逻辑
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        //当前线程结束,我们就把这个ThreadLocal里面的User清理掉
        UserHolder.removeUser();
    }
}

2. 定义好拦截器类之后,我们定义好一个mvc配置类,去实现WebMvcConfig接口,加载这个自定义拦截器。

         2.1

 

> 当前这个类,添加了Configuration注解,所以当前这个配置类是交给spring管理的,这里我们需要添加拦截器对象。而我们的拦截器是没办法通过@Resoure或者@Autowire注入StringRedisTemplate。所以我们在MVC配置类,添加这个拦截器对象的时候,将StringRedisTemplate通过构造器注入的方式将StringRedisTemplate注入进去。
总之:就是我们在mvc配置类上,要通过@Resource注解,将redisTempalte注入进来,然后将其传给拦截器。

最后实现的效果,就是点击某些需要登录的页面之后,就会自动跳转到登录页面

登录校验功能优化

分析

1. 如果我们只使用一个拦截器去做,登录校验,是可以的,但有一个弊端。

就是当我们访问:需要登录校验的网页,我们才会去刷新token,当我们一直处于不需要登陆的校验的页面,30分钟后,我们的token就会消失,相当于我们自动退出了。即使ThreadLocal保存了用户信息,当我们遇到拦截器的时候,直接就先判断redis上的token,压根就进不去你的ThreadLocal就会被直接拦截。

所以了为了解决这个,一直处于不需要登录校验的页面,会自动退出的问题,我们就需要多一个拦截器。

思路分析

新拦截器需要做的事

  1. 获取请求头上的token
  2. 如果token为null,直接放行。这样ThreadLocal就没有数据了
  3. 如果token不为null,就从redis中找数据,因为我们前面存储userDto对象的时候,key是【功能模块+token】,value是hash类型。所以取出来的时候,可以各种取法,但是我们选择以键值对的形式取出来。就是取出来的是一个map集合
  4. 判断这个map集合是否为null,如果为null直接放行(这样ThreadLocal上就没有数据)
  5. 如果不为null,就重写写入到Redis上,会发生覆盖,然后重新设置存货时间。

旧拦截器:

  1. 只需要判断ThreadLocal是否null即可
  2. 如果为null就拦截,如果被拦截之后,前端页面就会自动跳转到登录页面
  3. 如果不为null,直接放行即可

代码实现

 1. 新拦截器

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数据,取出来的token是空串,直接放行,说明登录,让第二个拦截器去处理。
        String token = request.getHeader("authorization");//取出token
        if(StrUtil.isBlank(token)) {
            return true;
        }
        //token不是空串,就去redis上取出对应的Value
        Map<Object, Object> entries = stringRedisTemplate.opsForHash().entries(LOGIN_USER_KEY + token);
        if(entries.isEmpty()){
            //如果取出来的是null,直接放行。这样ThreadLocal上就不会有数据,第二个拦截器是否拦截器本次请求,是根据ThreadLocal是否有数据来决定的
            return true;
        }
        //这里让其创建UserDto,并保存到reids上,需要吗?肯定啊。否则第二个拦截器是根据你的ThreadLocal是否有值来决定,你是否拦截器的
        // 如果你此时不将redis 根据token取出来的对象存放在threadLocal上,就会一直被拦截。
        UserDTO userDTO = new UserDTO();
        BeanUtil.fillBeanWithMap(entries,userDTO,false);
        log.info("userDtoNickName:"+userDTO.getNickName());
        UserHolder.saveUser(userDTO);
        stringRedisTemplate.expire(LOGIN_USER_KEY + token,30, TimeUnit.MINUTES);
        return true;
    }
    
}

2. 旧拦截器

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 {
        //第二个拦截器,我们还需要从Redis取出值吗。没有必要了。所以把StringRedisTemplate删掉,或者注释掉
        //我们当前这个拦截器,判断你ThreadLocal上的值是否为null,来决定是否拦截的,如果拦截了,前端会帮我们自动跳转到登录页面
        UserDTO user = UserHolder.getUser();
        if(user==null){
            response.setStatus(401);
            return false;
        }
        return true;
    }

}

3. mvc配置类

@Configuration
public class MVCConfig implements WebMvcConfigurer {
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor()).excludePathPatterns(
            //LoginInterceptor 拦截器,有那些是不能拦的?发送验证码(发出的请求)不能栏。点击登录之后的请求不能拦。还有一些页面你也不用去栏。
                //可以给别人参观。
                "/user/code",
                "/user/login",
                "/blog/hot",
                "/shop/**",
                "/shop-type/**",
                "/voucher/**"
        ).order(1);
        //TODO 设置优先级,因为我们必须让这个自动刷新token的放在前面,这样我们访问不需要校验的页面,也会自动刷新token
        // 并且新拦截器是拦截所有的,并且都会放行。
        // 至于会不会拦截,留给第二个拦截器去判断。如果token存在,并且对应value里面有数据,就会保存到ThreadLocal。
        // 第二个拦截器判断ThreadLocal里是否有数据,如果没有就会拦截
        registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);
    }
}

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值