黑马点评part1 -- 短信登录

目录

[1 . 导入项目 :](#1 . 导入项目 :)

[2 . 基于Session实现短信验证登录](#2 . 基于Session实现短信验证登录)

[2 . 1 原理 :](#2 . 1 原理 :)

[2 . 2 发送短信验证码 :](#2 . 2 发送短信验证码 :)

[2 . 3 短信验证码登录和验证功能 :](#2 . 3 短信验证码登录和验证功能 :)

[2 . 4 登录验证功能](#2 . 4 登录验证功能)

[2 . 5 隐藏用户敏感信息](#2 . 5 隐藏用户敏感信息)

[2 . 6 session共享问题](#2 . 6 session共享问题)

[2 . 7 Redis 代替 session](#2 . 7 Redis 代替 session)

[2 . 8 基于Redis实现短信登录](#2 . 8 基于Redis实现短信登录)

UserServiceImpl

[发送短信验证码 :](#发送短信验证码 :)

[用户登录 :](#用户登录 :)

[LoginInterceptor :](#LoginInterceptor :)

[报错 :](#报错 :)

[测试 :](#测试 :)

[2 . 9 登录拦截器的优化](#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中的时间重置了 :

相关推荐
魔道不误砍柴功1 小时前
Java 中如何巧妙应用 Function 让方法复用性更强
java·开发语言·python
NiNg_1_2341 小时前
SpringBoot整合SpringSecurity实现密码加密解密、登录认证退出功能
java·spring boot·后端
闲晨1 小时前
C++ 继承:代码传承的魔法棒,开启奇幻编程之旅
java·c语言·开发语言·c++·经验分享
测开小菜鸟2 小时前
使用python向钉钉群聊发送消息
java·python·钉钉
P.H. Infinity3 小时前
【RabbitMQ】04-发送者可靠性
java·rabbitmq·java-rabbitmq
生命几十年3万天3 小时前
java的threadlocal为何内存泄漏
java
caridle4 小时前
教程:使用 InterBase Express 访问数据库(五):TIBTransaction
java·数据库·express
^velpro^4 小时前
数据库连接池的创建
java·开发语言·数据库
苹果醋34 小时前
Java8->Java19的初步探索
java·运维·spring boot·mysql·nginx
秋の花4 小时前
【JAVA基础】Java集合基础
java·开发语言·windows