首页 > 其他分享 >黑马点评part1 -- 短信登录

黑马点评part1 -- 短信登录

时间:2024-04-04 20:59:38浏览次数:24  
标签:return -- 验证码 part1 token session user import 黑马

目录

 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中的时间重置了  : 

标签:return,--,验证码,part1,token,session,user,import,黑马
From: https://blog.csdn.net/ros275229/article/details/137108686

相关文章

  • Python编程题(for循环与format合用)
    题目描述:用 * 构造一个对角线长 55 个字符,倾斜放置的菱形。输出格式 * ******** *** *首先补充format()格式化刷新函数:当我们了解并会熟练使用format()函数的时候我们便可以写出代码: foriinrange(-2,3):print("{:^5}".format("*"*(5-abs(2*i)......
  • 『VUE』11. 操作数组的方法(详细图文注释)
    目录vue中操作数组的方法会修改原数组的会进行渲染更新不修改原数组的不会进行渲染更新push自动渲染concat赋值渲染总结欢迎关注『VUE』专栏,持续更新中欢迎关注『VUE』专栏,持续更新中vue中操作数组的方法vue中数组数据呈现在网页,只检测一开始用到的数......
  • 『VUE』10. 事件修饰符(详细图文注释)
    目录什么是事件修饰符?vuejs不使用修饰符原生js实现禁用事件对象的默认事件使用事件修饰符.prevent使用事件修饰符.stop使用事件修饰符.self欢迎关注『VUE』专栏,持续更新中欢迎关注『VUE』专栏,持续更新中什么是事件修饰符?vue在Vue.js中,事件修饰符......
  • python之数据读取
    在Python中,你可以使用各种库和方法来读取数据,具体取决于数据的类型和格式。以下是一些常用的方法:目录1.内置的open()函数:2.csv模块:这里补充介绍一下csv文件:优点:缺点:csv文件的读写方法:3.使用pandas库:读取数据DataFrame和Series:DataFrame:Series:4.使用numpy库:5.使......
  • Kotlin中那些让你眼花缭乱的关键字和操作符
    符号含义说明示例Int非空类型valx:Int=10Int?可空类型valx:Int?=nullInt!不确定类型不确定是否非空可以赋值给非空类型,也可以赋值给可空类型这个类型无法主动使用,只是兼容其它语言的一种方式只能在Kotlin调用其它语言时,由编译器进行提示?.非空则执行非空则执行语......
  • 【T5中的激活函数】GLU Variants Improve Transformer
    【mT5中的激活函数】GLUVariantsImproveTransformer论文信息阅读评价AbstractIntroductionGatedLinearUnits(GLU)andVariantsExperimentsonText-to-TextTransferTransformer(T5)Conclusion论文信息名称内容论文标题GLUVariantsImprov......
  • 人是否应该以貌取人 英语作文 四级备考 20240404
    题目:Doyouagreeordisagreewiththefollowingstatement?Oneshouldneverjudgeapersonbyexternalappearances.Usespecificreasonsanddetailstosupportyouropinion.(150words)作文:Ifirmlybelievethatoneshouldneverjudgeapersonsolelybyt......
  • SSRF漏洞
    Web渗透_SSRF漏洞一、介绍SSRF漏洞SSRF(Server-SideRequestForgery,服务器端请求伪造)是一种由攻击者构造请求,由服务端发起请求的安全漏洞。一般情况下,SSRF攻击的目标是外网无法访问的内部系统(正因为请求是由服务端发起的,所以服务端能请求到与自身相连而与外网隔离的内部系......
  • 2-32. 制作 Player 的动画
    创建Animator动画状态机Idle->WalkRun没有退出时间,Duration为1Idle的BlendTreeWalkRun的BlendTree创建AnimatorOverrideController用同样的方法创建头发和手臂Player控制动画状态机播放动画按住左Shift键的时候,让人物进入走路状态项......
  • 挑战程序设计竞赛 2.6章习题 POJ 1930 Dead Fraction
    https://vjudge.csgrandeur.cn/problem/POJ-1930迈克在最后一刻拼命地赶着完成他的论文。在接下来的3天里,他需要将所有的研究笔记整理成较为连贯的形式。不幸的是,他注意到他在计算方面非常粗心。每当他需要进行算术运算时,他只是将其输入计算器,并将他认为相关的答案写下来。每当......