黑马点评part1 -- 短信登录

目录

 1 . 导入项目 : 

2 . 基于Session实现短信验证登录

2 . 1 原理 : 

2 . 2 发送短信验证码 : 

2 . 3 短信验证码登录和验证功能 :

2 . 4 登录验证功能

2 . 5 隐藏用户敏感信息

2 . 6 session共享问题

2 . 7 Redis 代替 session

2 . 8 基于Redis实现短信登录

UserServiceImpl

发送短信验证码 : 

用户登录 : 

LoginInterceptor : 

报错 : 

测试 : 

2 . 9 登录拦截器的优化 


 1 . 导入项目 : 

先导入sql文件 : 

导入后端项目 : 

注意要修改一些地方 : 

1 . mysql配置,要改成自己的 : 

如果用的是8.x版本,需要在pom文件中修改依赖 : 

2 . 修改redis的url,为自己虚拟机redis开放端口 : 

3 . 直接启动项目之后,访问http://localhost:8081/shop-type/list

4 . 将前端搭建好之后,访问8080,用手机模式打开 : 

2 . 基于Session实现短信验证登录

2 . 1 原理 : 

发送验证码:

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

如果手机号合法,后台此时生成对应的验证码,同时将验证码进行保存,然后再通过短信的方式将验证码发送给用户

短信验证码登录、注册:

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

校验登录状态:

用户在请求时候,会从cookie中携带者JsessionId到后台,后台通过JsessionId从session中拿到用户信息,如果没有session信息,则进行拦截,如果有session信息,则将用户信息保存到threadLocal中,并且放行 ;

2 . 2 发送短信验证码 : 

    @Override
    public Result sendCode(String phone, HttpSession session) {
        // 1 . 检验啊手机号
        if(RegexUtils.isPhoneInvalid(phone)){
            // 2 . 如果不符合 , 报错
            return Result.fail("手机号格式错误!") ;
        }
        // 3 . 符合 , 生成验证码
        String code = RandomUtil.randomNumbers(6) ; //生成长度为6位的随机验证码

        // 4 . 保存验证码到 session
        session.setAttribute("code",code);

        // 5 . 发送验证码
        log.debug("验证码方程成功,验证码 : {}",code);

        return Result.ok();
    }

关于如何校验,参考 : java实现手机号,密码,游邮箱 , 验证码的正则匹配工具类-CSDN博客

这里的发送验证码功能没有实现,只是做了个假的,日后有时间再完成 ;

启动项目,在前端点击发送验证码 : 

2 . 3 短信验证码登录和验证功能 :

    /**
     * 用户登录
     * @param loginForm
     * @param session
     * @return
     */
    @Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
        // 1 . 校验手机号
        String phone = loginForm.getPhone() ; // 获取手机号码
        if (RegexUtils.isPhoneInvalid(phone)){
            // 2 . 不符合 , 返回错误信息
            return Result.fail("手机号格式错误") ;
        }
        // 3 . 校验验证码
        Object cacheCode = session.getAttribute("code") ;
        String code = loginForm.getCode() ;
        if(cacheCode == null || !cacheCode.toString().equals(code)){
            // 3 . 1 不一致,直接报错返回
            return Result.fail("验证码错误") ;
        }
        // 4 . 一致, 根据手机号查询用户
        User user = query().eq("phone",phone).one() ;

        // 5 . 判断用户是否存在
        if(user == null){
            // 6 . 为空,表示之前未创建 , 则创建
            user = createUserWithPhone(phone) ;
        }
        // 7 . 保存用户信息到session中
        session.setAttribute("user",user);

        return Result.ok() ;

    }

    private User createUserWithPhone(String phone) {
        // 1. 新建用户
        User user = new User() ;
        user.setPhone(phone) ;
        user.setNickName(USER_NICK_NAME_PREFIX + RandomUtil.randomString(10)) ;
        // 2 . 保存用户
        save(user) ;
        return user ;
    }

2 . 4 登录验证功能

先定义一个拦截器 : 

/**
 * 拦截器
 */
public class LoginInterceptor implements HandlerInterceptor {

    /**
     * 前置拦截器
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1 . 获取session
        HttpSession session = request.getSession()  ;
        // 2 .  获取session中的用户
        Object user = session.getAttribute("user") ;
        // 3 . 判断用户是否存在
        if(user==null){
            // 4 . 不存在,拦截,返回401状态码
            response.setStatus(401);
            return false ;
        }
        // 5 . 存在,保存用户信息到ThreadLocal
        UserHolder.saveUser((User) user);
        // 6 . 放行
        return true ;
    }

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

这里因为之前登录的时候,在session中存了user信息,如果这里查不到,那就对其进行拦截,查到了,就放行 ;

然后让定义的拦截器生效 (定义一个配置类) : 

@Configuration
public class MvcConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor())
                .excludePathPatterns(
                        "/user/code",
                        "/user/login",
                        "/blog/hot",
                        "upload/**",
                        "/shop/**",
                        "/shop-type/**",
                        "voucher/**"
                );
    }
}

 其中设置了一些放行的端口(也就是不需要登录也能够访问得到的端口) ;

然后再实现一下"me"接口 : 

这里直接获取ThreadLocal中之前设置的user对象即可 ;

这里ThreadLocal来设置user和获取user定义成了一个工具类 : 

package com.hmdp.utils;

import com.hmdp.dto.UserDTO;
import com.hmdp.entity.User;


public class UserHolder {
    private static final ThreadLocal<User> tl = new ThreadLocal<>();

    public static void saveUser(User user){
        tl.set(user);
    }

    public static User getUser(){
        return tl.get();
    }

    public static void removeUser(){
        tl.remove();
    }
}

到时候直接调用即可 ;

2 . 5 隐藏用户敏感信息

        用UserDto(只包括id,nickName,icon三个)来隐藏用户的敏感信息(如password,phone等),也可以减少内存的压力 ;

这里直接在存入session的时候,就转换为UserDTO : 

然后在LoginInterceptor中存入ThreadLocal的时候将user转换为UserDTo,

那么对应的UserHolder中也要改 : 

然后修改报错的地方,将User修改成UserDTO ;

然后重新登录测试 : 

2 . 6 session共享问题

用redis来解决session的内存不共享的问题 ;

2 . 7 Redis 代替 session

在登录发验证码的时候用手机号作为key,验证码作为value ;

在保存用户的时候 : 

用hash结构来保存用户信息 , 用一个随机的token作为key ;

在用session做登录校验的时候,tomcat会将session的id写到浏览器的cookie中,然后每一次的请求都会带着cookie,也就带着session_id , 然后就能够通过session_id找到session,然后找到用户 ;

在用redis代替token的时候,我们只能够手动的将token传给前端(客户端),然后客户端每一次请求都会携带token,然后我们可以基于token获取用户数据 ;

2 . 8 基于Redis实现短信登录

UserServiceImpl

发送短信验证码 : 

先注入StringRedisTemplate对象 : 

修改保存逻辑,将验证码保存到redis中以key= phone,value:code的形式,并且设置过期时间 : 

这里对于"login:code:"和2可以设置一个常量类保存起来代码更加规范 : 

完整代码 : 

    @Resource
    private StringRedisTemplate stringRedisTemplate ; // 注入

    /**
     * 发送手机验证码
     * @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) ; //生成长度为6位的随机验证码

        // 4 . 保存验证码到 redis , 并设置两分钟的有效期(减少内存压力,防止一直点)
        stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY+phone,code,LOGIN_CODE_TTL, TimeUnit.MINUTES);// 前面加一个login:code:标识,进行业务区分

        // session.setAttribute("code",code);

        // 5 . 发送验证码
        log.debug("验证码方程成功,验证码 : {}",code);

        return Result.ok();
    }
用户登录 : 
    @Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
        // 1 . 校验手机号
        String phone = loginForm.getPhone() ; // 获取手机号码
        if (RegexUtils.isPhoneInvalid(phone)){
            // 2 . 不符合 , 返回错误信息
            return Result.fail("手机号格式错误") ;
        }
        // 3 . 校验验证码
        String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone) ;// 本地code
        String code = loginForm.getCode(); // 前端传来的code
        if(cacheCode == null || !cacheCode.toString().equals(code)){
            // 3 . 1 不一致,直接报错返回
            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) ;
        Map<String,Object> userMap = BeanUtil.beanToMap(userDTO);
        // 7 . 3 存储
        String tokenKey = LOGIN_USER_KEY + token ;
        stringRedisTemplate.opsForHash().putAll(tokenKey,userMap);
        // 7 . 4 设置token有效期 (30分钟)
        stringRedisTemplate.expire(tokenKey,LOGIN_USER_TTL,TimeUnit.MINUTES);//这样是在登录那一刻的30分钟后就过期了,然后可以在拦截器哪里设置每一次访问就更新有效期

        //  8 . 返回token
        return Result.ok(token) ;

    }
  • 这里校验验证码,直接从redos中获取 ;
  • 保存用户到redis中 , 使用hash存储,用随机的token作为key(前面加一个标识前缀),用相应的user转换成map对象作为value ;
  • 最后还要设置token的有效期,这里只能够设置在登录之后有效期为30分钟,但是实际应该为每次访问的时候,都能够有30分钟的有效期,那么这个将在拦截器中设置 ;

LoginInterceptor : 

完整代码 : 

package com.hmdp.utils;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import com.hmdp.dto.UserDTO;
import com.hmdp.entity.User;
import lombok.val;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * 拦截器
 */
public class LoginInterceptor implements HandlerInterceptor {

    private StringRedisTemplate stringRedisTemplate ;

    // 这里不能够使用Resource 和 AutoWired 等来进行注入,只能够使用构造函数来进行依赖注入
    // 因为 LoginInterceptor 是我们自己手动new出来的 , 不是由spring创建的 ;
    // 这里可以在MvcConfig中来注入 stringRedisTemplate 对象
    public LoginInterceptor(StringRedisTemplate stringRedisTemplate){
        this.stringRedisTemplate = stringRedisTemplate ;
    }

    /**
     * 前置拦截器
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1 . 获取请求头中的token
        String token = request.getHeader("authorization") ;
        if(StrUtil.isBlank(token)){
            // 4 . 不存在,拦截,返回401状态码
            response.setStatus(401);
            return false ;
        }
        // 2 . 基于token获取redis中的用户
        String key = RedisConstants.LOGIN_USER_KEY + token ;
        Map<Object,Object> userMap = stringRedisTemplate.opsForHash().entries(key) ;
        // 3 . 判断用户是否存在
        if(userMap.isEmpty()){
            // 4 . 不存在,拦截,返回401状态码
            response.setStatus(401);
            return false ;
        }
        // 5 . 将查寻到的Hash数据转换为UserDTo对象
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);

        // 6 . 存在,保存用户信息到ThreadLocal
        UserHolder.saveUser(userDTO);
        // 7 . 刷新token的有效期
        stringRedisTemplate.expire(key , 30, TimeUnit.MINUTES) ;
        // 8 . 放行
        return true ;
    }

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

这里主要进行修改的就是需要基于token从redis中获取数据 , 然后还要在每次拦截的时候,对token的有效期进行修改 ;

对于导入StringRedisTemplate方法参考 : 关于在拦截器中注入依赖对象-CSDN博客

报错 : 

运行起来之后,登录一下啊,能够发现报错 : 

能够发现,大概是 : 出现类型转换错误 :

详细参考 : java.lang.Long cannot be cast to class java.lang.String at redis.serializer.StringRedisSerializer报错-CSDN博客

测试 : 

能够看到请求头中携带了token,然后redis中也存入了响应的token ;

这样改造就完成了 ;

2 . 9 登录拦截器的优化 

        在上面方案中,他确实可以使用对应路径的拦截,同时刷新登录token令牌的存活时间,但是现在这个拦截器他只是拦截需要被拦截的路径,假设当前用户访问了一些不需要拦截的路径,那么这个拦截器就不会生效,所以此时令牌刷新的动作实际上就不会执行,所以这个方案他是存在问题的

首先加一个token刷新拦截器类 : 



package com.hmdp.utils;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import com.hmdp.dto.UserDTO;
import com.hmdp.entity.User;
import lombok.val;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * 拦截器
 */
public class RefreshTokenInterceptor implements HandlerInterceptor {

    private StringRedisTemplate stringRedisTemplate ;

    // 这里不能够使用Resource 和 AutoWired 等来进行注入,只能够使用构造函数来进行依赖注入
    // 因为 LoginInterceptor 是我们自己手动new出来的 , 不是由spring创建的 ;
    // 这里可以在MvcConfig中来注入 stringRedisTemplate 对象
    public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate){
        this.stringRedisTemplate = stringRedisTemplate ;
    }

    /**
     * 前置拦截器
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    @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 , 30, TimeUnit.MINUTES) ;
        // 8 . 放行
        return true ;
    }

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

然后LoginInterceptor中就只需要执行拦截功能了 : 

 然后在MvcConfig中进行配置 : 

package com.hmdp.config;


import com.hmdp.utils.LoginInterceptor;
import com.hmdp.utils.RefreshTokenInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import javax.annotation.Resource;

@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",
                        "upload/**",
                        "/shop/**",
                        "/shop-type/**",
                        "voucher/**"
                ).order(1);
        // token刷新的拦截器
        registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);
    }
}

测试 : 在主界面刷新一下,然后看到redis中的时间重置了  : 

  • 52
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 黑马qt公开课是一套为期5天的课件,主要介绍了Qt框架的基础知识和应用。Qt框架是一个跨平台的开发工具,可以方便地进行应用程序的设计、开发和调试,被广泛应用于图形界面开发、嵌入式系统和移动应用等领域。 在5天的课程中,学习者将对Qt框架的整体架构有一个全面的认识,包括Qt及其常用库的概念、功能和用法,也学会了如何使用Qt Designer进行界面设计和基于信号与槽的事件编程。 此外,课程还将介绍Qt中常用的编程模式和技术,如MVC架构、文件操作、网络编程等,并通过实例让学习者深入理解和应用这些概念和技术。 5天的课件中还提供了大量的实践操作,让学习者通过编写实际案例,深入理解所学知识,并更好地掌握Qt框架的基础和应用,为以后的工作打下坚实的基础。 总之,如果你想快速入门Qt框架的基础知识和应用,那么黑马qt公开课—5天的课件,将是一个非常好的选择。 ### 回答2: 黑马qt公开课的课件共分为5天,内容涵盖了Qt的基础知识、UI设计、绘图系统、多线程编程和网络编程等方面。通过这5天的学习,学员可以全面掌握Qt的开发技能和应用场景,具备开发Qt应用程序的能力。 第一天课程主要介绍了Qt的基础知识,包括Qt窗口和控件、信号与槽机制、事件处理、布局和样式等内容。通过这些基础知识的学习,学员可以了解Qt的基本工作原理和操作方法。 第二天的课程主要讲解了Qt的UI设计,包括UI设计器的使用、自定义控件和样式等内容。学员可以从中学习到如何设计美观、直观的用户界面。 第三天的课程则主题为Qt的绘图系统,包括2D和3D绘图、动画效果和图形转换等内容。在这一天的课程中,学员可以学习到如何使用Qt进行图形绘制和界面效果的优化。 第四天的课程主要介绍了Qt的多线程编程,包括线程的创建和管理、互斥锁和信号量等内容。学员可以从中学习到如何在Qt中实现多线程应用程序。 第五天的课程则主题为Qt的网络编程,包括socket编程、HTTP协议和Web服务等内容。学员可以从中学习到如何使用Qt进行网络编程,实现客户端和服务器的互通。 总体来说,黑马qt公开课的5天课程涵盖了Qt的核心知识点,让学员能够全面掌握Qt的开发技能和应用场景。通过这些课程的学习,学员可以成为一名合格的Qt开发工程师。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值