Redis day02-应用-实战-黑马点评-短信登录

短信登录

导入项目

基于session实现登录

流程图

发送短信验证码

controller:

serviceimpl:

复制代码
 
@Service
@Slf4j
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {

    @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);
        //TODO 5.发送验证码
    log.debug("发送成功,验证码:{}",code);
        //6.返回ok
        return Result.ok();
    }
}

短信验证码登录与注册

controller:

impl:

为什么不需要返回一个登录凭证:

因为session原理就是cookie,每一个session都有一个唯一的id,会自动写进session。

复制代码
  @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");
        String code = loginForm.getCode();
        //3.不一致报错
if (cachecode==null ||!cachecode.toString().equals(code)){
    return Result.fail("验证码错误");}
        //4.一致用手机号查询用户
        User user = query().eq("phone", phone).one();
        //5.没找到注册新用户
if (user==null){
    //不存在则创建
  user=  createUserWithPhone(phone);
}
        //6.保存信息到session
session.setAttribute("user",user);
return Result.ok();
    }

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

登录校验拦截器

一个项目有很多controller,比如登录业务就是前端向usercontroller发送请求,在usecontroller中编写业务逻辑,但是随着业务开发,越来越多的业务都需要去校验用户的登录,不可能在每个都写一堆校验登录的业务逻辑。

为了节省,使用拦截器,所有请求都必须经过拦截器,让拦截器判断是否放行。

但是还是有一个问题,拦截器是可以帮助我们进行业务校验,但是后续业务有的时需要业务信息的,还需要一种方案将拦截器里的用户信息传递到controller中。安全问题用ThreadLocal解决。因为ThreadLocal是一个线程对象,每一个进入tomcat请求都是独立进程。

拦截器使用前置拦截和返回用户之前拦截。

前置拦截:

复制代码
 @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.不存在拦截,拦截就是返回false
    response.setStatus(401);//这是给用户提示,报错401
    return  false;

    }
        //5.存在保存用户信息到ThreadLocal
        //ThreadLocal不用自己写,放在了utils.UserHolder中
        UserHolder.saveUser((UserDTO) user);
        //6,放行

        return true;
    }

其中,ThreadLocal放在了utils.UserHolder中:

其实这样只是写好了还并没有让拦截器生效,想要生效的话需要进行配置拦截器:

复制代码
public class MvcConfig implements WebMvcConfigurer {
    @Override
    //顾名思义:添加拦截器
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInteerceptor())
                //excludePathPatterns排除参数,排除不需要拦截的路径
                .excludePathPatterns(
                        "/shop/**",
                        "/shop-type/**",
                        "/blog/hot",
                        "/user/code",
                        "/user/login"
                )
    }
}

完成拦截器后,请求会到controller中,登录校验使用是user/me的接口,需要将当前登录的用户信息返回到前端,因为已经将user放到了userholder中:

复制代码
@GetMapping("/me")
public Result me(){
    // 获取当前登录的用户并返回
    //因为将拦截器放到了usercoder中,只需直接取用
    UserDTO user = UserHolder.getUser();
    return Result.ok(user);
}

隐藏用户敏感信息

进行测试发现,前端返回的信息有点多:

一般来说。查询当前登录用户的时候,其实只需要返回id,头像信息就够了。

时间密码等敏感信息不需要返回。

为什么会返回这么多信息呢:

因为这个方法,直接返回了从userholder中保存的完整user信息,而userHolder信息又是从拦截器中获得的。

这说明从session取出来的就是完整信息,而session是tomcat的内存空间,往这里存储的信息越多,对服务的压力也就越大。

而又是谁在给session中存储信息:

是登录业务时,从数据库查出user直接传给了session

从一开始我们就应该给session传递的是部分信息,虽然说存储的越完整将来使用越方便。但是有内存问题和隐私问题,如何解决:

首先,我们只需要给前端传递id,名字,头像这三个信息,所以直接创建一个userdto:

在传递给session之前先将user传递给dto:

所以从拦截器中取出来的其实也是dto对象:

有时候(userdto)user会发现有错误,这是因为userholder里面是user,需要修改:

又因为userholder中的getuser方法在有些地方使用过

集群的session共享问题

同一个用户,他可能登录是通过第一个tomcat,但是做其他业务被分配到了其他tomcat,会发现其他的session是空的,这就是session不共享带来的问题。

事实上为了解决这个问题,tomcat提供了数据拷贝,但是这样会有问题,多台 tomcat互相拷贝数据就是浪费内存。然后拷贝数据卷=也是有延迟的,在延迟中请求还是请求不到。

于是有了替代session的方案:redis.

基于redis实现共享session登录

使用redis的话,key使用手机号,这样可以避免重复和冲突,有助于获取验证码进行验证

商户查询缓存

使用redis的话:

前端如图:

redis代替session

发送短信验证码流程发生变化:就是保存验证码不再保存到session而是保存到redis/

先进行注入:

@Resource:作用:这是 Java 提供的依赖注入注解,告诉 Spring 框架自动查找并注入一个 StringRedisTemplate 类型的 Bean

查找方式:默认按类型(byType)注入,如果找到多个则按字段名(byName)注

为什么选择 @Resource 而不是 @Autowired?

@Resource:Java 标准注解,按名称注入优先

@Autowired:Spring 提供,按类型注入优先

两者在这个场景下效果相同,都是 Spring 支持的注入方式

StringRedisTemplate 是 Spring Data Redis 提供的核心类,用于操作 Redis 数据库:

特点:专门处理 String 类型:所有的 key 和 value 都会被序列化为 String 格式

基于 Spring 封装:简化了 Redis 的原生命令操作

线程安全:可以在多线程环境中安全使用

代码:userimpl,发送验证码逻辑:

复制代码
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
@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.保存验证码到session
//session.setAttribute("code",code);
        //4.保存验证码到redis,key设置有效期:2, TimeUnit.MINUTES)两分钟
        stringRedisTemplate.opsForValue().set("login:code:"+phone,code,2, TimeUnit.MINUTES);
        //TODO 5.发送验证码
    log.debug("发送成功,验证码:{}",code);
        //6.返回ok
        return Result.ok();
    }
/*

短信登录和注册:

分为从redis中获取验证码。

验证码经过校验后将user登录信息保存到redis中,分为几步:

生成token作为登录令牌

转成hashmap对象

设置有效期

复制代码
   @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");//获得code
//        String code = loginForm.getCode();//前端提交的
        //从redis获取验证码
        String cachecode = stringRedisTemplate.opsForValue().get("login:code:" + phone);
        String code = loginForm.getCode();//前端提交的
        //3.不一致报错
if (cachecode==null ||!cachecode.toString().equals(code)){
    return Result.fail("验证码错误");}
        //4.一致用手机号查询用户
        User user = query().eq("phone", phone).one();
        //5.没找到注册新用户
if (user==null){
    //不存在则创建
  user=  createUserWithPhone(phone);
}
//        //6.保存信息到session
//        session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class));
//        return Result.ok();
        //6保存到redis
        //6.1生成token作为登录令牌
        String token = UUID.randomUUID().toString(true);
        //将user对象转为hashMap储存
        UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
        Map<String, Object> userMap = BeanUtil.beanToMap(userDTO);
        //存储,存储时不能设置有效期,先存储再设置
        stringRedisTemplate.opsForHash().putAll("login:token:"+token,userMap);
        //设置有效期
        stringRedisTemplate.expire("login:token:"+token,30,TimeUnit.MINUTES);
        //还需要注意,如果客户一直在访问那么30分钟就无效肯定是不合理的,需要让用户再访问期间一直有token,一直更新redis
        //怎么知道用户再访问我?
        //使用登录拦截校验,只要经过了校验就能知道用户在登录和访问
        //7.返回token
return Result.ok(token);
    }

这样做完,还需要注意,因为我们设置了30分钟,超过会删除数据,但如果用户一直在访问,无疑30分钟就删除是不合理的,如果想在用户 访问期间一直有token,让redis一直更新。

如何知道用户是在访问:那就是登录拦截校验,只要经过校验就能知道用户在登录和访问

即添加一个更新token有效期的逻辑 。

拦截器代码:

如图,为什么LoginInteerceptor类中不能注解进行注入而是使用构造方法注入:

因为LoginInteerceptor的拦截器对象是手动new出来的不是通过注解构建的,也就是说这个对象不是由spring构建的。所以用构造函数注入,至于谁来帮我们进行注入,谁用了谁进行。

// 在配置类中,我们是这样创建拦截器的

public class MvcConfig {

@Resource

private StringRedisTemplate stringRedisTemplate;

@Override

public void addInterceptors(InterceptorRegistry registry) {

// ❌ 手动创建对象

registry.addInterceptor(new LoginInteerceptor(stringRedisTemplate));

}

}

复制代码
   @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//      //1.获取session
//        HttpSession session = request.getSession();
        //获取请求头中的token,头名称authorization前端写的
        String token = request.getHeader("authorization");
        //判断空,strutil是工具类
        if (StrUtil.isBlank(token)){
        response.setStatus(401);
        return false;
        }
//        //2.获取session中用户
//        Object user = session.getAttribute("user");
        ///基于token获取redis用户
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries("login:token:" + token);
        //3.判断用户是否存在
//if (user==null){
        if (userMap.isEmpty()){
            //4.不存在拦截,拦截就是返回false
            response.setStatus(401);//这是给用户提示,报错401
            return  false;
        }
//将查询到的数据转成userdto对象
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
        //5.存在保存用户信息到ThreadLocal
        //ThreadLocal不用自己写,放在了utils.UserHolder中
//        UserHolder.saveUser((UserDTO) user);
        UserHolder.saveUser((UserDTO) userDTO);
//刷新token有效期
        stringRedisTemplate.expire("login:token:" + token,30, TimeUnit.MINUTES);
//6,放行
        return true;
    }

运行报错:

类型转换异常在userserviceimpl中:

说类型转换long不能转成string。

而long类型在userdto中只有id。

也就是说long类型的id无法存到redis中。

为什么会有这样一个错误:

因为我们使用的template是stringtemplate,其特点是要求所有的key和value都是string结构。

如何解决:

存储数据到map中时确保每一个值都是以string形式:

方法1:不用这个工具类,自己new一个map,然后将userdto对象里面的字段名作为key,将字段值作为值。

方法2:还是使用这个工具,而且这个工具其实是可以自定义的。默认情况下值是什么类型就是什么类型,但是可以自定义

修改后:

登录拦截器的优化

登录是基于拦截器做的校验,用户请求进入拦截器后会尝试去获取请求头上的token。

而拦截器因为目前拦截的都是登录的路径,因此会出现一个问题,如果用户一直访问的不是登录路径就会造成拦截器不生效,这样就不会刷新token。

所以加一个拦截器拦截所有路径:

新拦截器复制粘贴原先拦截器,并在此基础上进行修改:

复制代码
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 {
        //获取请求头中的token,头名称authorization前端写的
        String token = request.getHeader("authorization");
        //判断空,strutil是工具类
        if (StrUtil.isBlank(token)){
            //新的拦截器不用做拦截,为空直接放行即可
            //若return false,第一次访问登录页面时就会被拦截;
            // 若return true,第一次访问登录页会进入Login拦截器,由于登录页为放行路径,放行
        return true;
        }
        ///基于token获取redis用户
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries("login:token:" + token);
        //3.判断用户是否存在
        if (userMap.isEmpty()){
            //4.这里也是,不存在放行
//            response.setStatus(401);//这是给用户提示,报错401
            return  true;
        }
//将查询到的数据转成userdto对象
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
        //5.存在保存用户信息到ThreadLocal
        UserHolder.saveUser((UserDTO) userDTO);
//刷新token有效期
        stringRedisTemplate.expire("login:token:" + token,30, TimeUnit.MINUTES);
//6,放行
        return true;
    }
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
    }
}

然后进行分工优化,旧拦截器唯一要做的其实就是要不要做拦截:

和之前的login拦截器区别太多了,修改过后只有几行代码。

接下来就是配置mvcconfig:

相关推荐
瀚高PG实验室2 小时前
易智瑞GeoScene Pro连接瀚高安全版数据库 458
数据库·安全·瀚高数据库
551只玄猫2 小时前
【数据库原理 实验报告3】索引的创建以及数据更新
数据库·sql·课程设计·实验报告·操作系统原理
加农炮手Jinx2 小时前
Flutter for OpenHarmony:postgrest 直接访问 PostgreSQL 数据库的 RESTful 客户端(Supabase 核心驱动) 深度解析与鸿蒙适配指南
数据库·flutter·华为·postgresql·restful·harmonyos·鸿蒙
xiaohe073 小时前
Spring Boot 各种事务操作实战(自动回滚、手动回滚、部分回滚)
java·数据库·spring boot
setmoon2143 小时前
使用Scikit-learn构建你的第一个机器学习模型
jvm·数据库·python
2401_833197733 小时前
为你的Python脚本添加图形界面(GUI)
jvm·数据库·python
执笔画情ora4 小时前
oracle数据库优化-表碎片优化性能。
数据库·oracle
givemeacar4 小时前
Spring Boot中集成MyBatis操作数据库详细教程
数据库·spring boot·mybatis
skiy4 小时前
MySQL Workbench菜单汉化为中文
android·数据库·mysql