使用Redis解决使用Session登录带来的共享问题

news2025/2/11 15:22:42

        在学习项目的过程中遇到了使用Session实现登录功能所带来的共享问题,此问题可以使用Redis来解决,也即是加上一层来解决问题。

        接下来介绍一些Session的相关内容并且采用Session实现登录功能(并附上代码),进行分析其存在的问题,并使用Redis来解决这一问题。

一、什么是Session

可以参考一下这个博客的内容,比较详细:

Session详解

        简而言之,session是服务器为了保存用户状态而创建的一个特殊的对象,服务器会为每一个浏览器(客户端)创建一个唯一的session,session有一个JSESSIONID,这个是session的唯一标识。

        当浏览器(客户端)发送请求的时候,会在Cookie中携带JSESSIONID,以便服务器可以找到对应的Session,服务器就能知道客户端的状态了。

比如:在登陆一些网站成功之后,你再访问这个网站的其他内容的时候就不需要重新登陆了,因为此时服务器可以基于浏览器Cookie中的JSESSIONID(或者其他校验技术)来判断你的状态。但是当一段时间未登录之后,你重新访问网站,往往需要重新登录,这是因为浏览器访问时没有携带JSESSIONID,浏览器携带的JSESSIONID对应的session不存在(或者失效)的原因。

二、基于Session实现登录(短信登录)

1.流程:

2.代码实现:

(1)Controller层:(接收请求)

    /**
     * 发送手机验证码
     */
    @PostMapping("code")
    public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {
        //发送短信验证码并保存验证码
        return userService.sendCode(phone, session);
    }

    /**
     * 登录功能
     * @param loginForm 登录参数,包含手机号、验证码;或者手机号、密码
     */
    @PostMapping("/login")
    public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){
        // 实现登录功能
        return userService.login(loginForm, session);
    }

(2)Service层:(处理业务逻辑)

    /**
     * 发送短信验证码并保存验证码
     * @param phone
     * @param session
     * @return
     */
    @Override
    public Result sendCode(String phone, HttpSession session) {
        //1.校验手机号是否合法
        boolean phoneInvalid = RegexUtils.isPhoneInvalid(phone);
        //2.若不合法,返回错误信息
        if(!phoneInvalid){
            return Result.fail("手机号不合法");
        }
        //3.生成验证码 6位
        String code = RandomUtil.randomNumbers(6);
        //4.保存验证码到session中
        session.setAttribute("code",code);
        //5.发送验证码 (这里可以结合阿里云提供的第三方服务来做,但是为了方便,输出一下就可以)
        log.debug("发送短信验证码成功,验证码:{}",code);
        //6.返回ok
        return Result.ok();

    }
     /**
     * 登录功能
     * @param loginForm
     * @param session
     * @return
     */
    @Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {

        String phone = loginForm.getPhone();
        //1.校验手机号是否合法
        if(!RegexUtils.isPhoneInvalid(phone)){
            return Result.fail("手机号不合法");
        }
        //2.校验验证码是否一致
        String sessionCode = (String) session.getAttribute("code");
        String userCode = loginForm.getCode();
        //3.验证码不一致返回错误信息
        if(sessionCode == null ||!sessionCode.equals(userCode)){
            return Result.fail("验证码错误");
        }
        //4.根据手机号到数据库查询用户
        User user = query().eq("phone", phone).one();
        //5.若用户不存在则创建用户并保存到数据库
        if(user == null){
            user = createUserByPhone(phone);
        }
        //6.保存用户信息到session中
        UserDTO userDTO = new UserDTO();
        BeanUtils.copyProperties(user,userDTO);
        session.setAttribute("user",userDTO);

        //7.返回ok
        return Result.ok();
    }

(3)LoginInterceptor:(拦截请求,进行登录校验)

public class LoginInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //1.获取session
        HttpSession session = request.getSession();

        //2.获取session里面的user
        User user = (User) session.getAttribute("user");

        //3.用户不存在,则拦截
        if(user == null){
            //4.拦截,返回401
            response.setStatus(401);
            return false;
        }
        //5.存在,保存到ThreadLocal中
        UserDTO userDTO = new UserDTO();
        userDTO.setId(user.getId());
        userDTO.setIcon(user.getIcon());
        userDTO.setNickName(user.getNickName());
        UserHolder.saveUser(userDTO);

        //6.放行
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        //在页面渲染完成返回给用户前,删除用户信息,避免内存泄漏
        UserHolder.removeUser();
    }
}

(4)MVCconfig:(用于注册拦截器,使得拦截器生效)

@Configuration//由Spring创建的对象 可以使用 Autowired
public class MVCconfig implements WebMvcConfigurer {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
       registry.addInterceptor(new LoginInterceptor(stringRedisTemplate))
               .excludePathPatterns(
                       "/user/code",
                       "/user/login",
                       "/shop/**",
                       "/shop-type/**",
                       "/blog/hot",
                       "/upload/**",
                       "/voucher/**"
               );
    }
}

三、基于Session实现登录存在的共享问题

1.问题分析

session共享问题:多台Tomcat并不共享session存储空间,当请求切换到不同tomcat服务时导致数据丢失的问题。

        也就是说一台浏览器可能会被负载均衡到不同的服务器上,但是服务器的Session是不共享的,那当浏览器先被负载均衡到服务器A,后负载均衡到服务器B时,浏览器所携带的JSESSIONID只在服务器A生效,而服务器B找不到其对应的Session,也就无法获知用户的状态。这就是基于Session实现登录存在的共享问题。

2.解决方案:

session的替代方案应该满足以下条件:

(1)数据共享:

        首先必须满足数据可共享,否则还是会引发共享问题。这个时候就应该想到常用的思路“加一层”,加上一层中间件,上层都到这个中间件来存取数据,这样不就实现了数据共享吗。

(2)内存存储

        由于每次请求的时候,服务器都需要对请求进行校验,这个操作是十分频繁的,如果可以将数据存储在内存中,就可以极大地提高服务器处理请求的效率。

(3)key、value结构

        服务器应该根据服务器所携带key去找到其对应的value

通过以上分析,使用Redis来解决这个问题是再合适不过的了。

使用Redis实现登录功能总思路:

1.用户提交手机号,后端收到手机号之后生成验证码并以手机号为key,验证码为value,并设置过期时间,存储到redis中。存储完成之后发送验证码给用户。

2.用户填写验证码,浏览器发送协带用户填写的手机号和验证码的请求。后端接收请求,根据用户的手机号到redis中查找对应的验证码值,查找后将redis中的验证码和用户提交的验证码进行比较。

3.验证成功,则生成一个token,将token作为key,用户信息作为value(这里使用redis的hash结构存储),设置过期时间,存入redis中,并返回一个token。

4.此后,前端发送请求的时候,就携带这个token,服务器就可以根据这个token到redis中去获取用户的信息。

5.由于redis是单独的一层,所有浏览器拿到token之后都是去这个redis中查找数据,这就解决了用户被负载均衡到不同服务器时,session引发的共享问题。

四、基于Redis实现登录

1.流程:

简单来说就是把对session存取操作改为对redis的存取操作。

2.代码实现:

(1)Controller层

     /**
     * 发送手机验证码
     */
    @PostMapping("code")
    public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {
        //发送短信验证码并保存验证码
        return userService.sendCode(phone, session);
    }

    /**
     * 登录功能
     * @param loginForm 登录参数,包含手机号、验证码;或者手机号、密码
     */
    @PostMapping("/login")
    public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){
        // 实现登录功能
        return userService.login(loginForm, session);
    }

    @GetMapping("/me")
    public Result me(){
        //获取当前登录的用户并返回
        UserDTO user = UserHolder.getUser();
        return Result.ok(user);
    }

(2)Service层:(处理业务逻辑)

    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    /**
     * 发送短信验证码并保存验证码
     * @param phone
     * @param session
     * @return
     */
    @Override
    public Result sendCode(String phone, HttpSession session) {
        //1.校验手机号是否合法
        boolean phoneInvalid = RegexUtils.isPhoneInvalid(phone);

        //2.若不合法,返回错误信息
        if(!phoneInvalid){
            return Result.fail("手机号不合法");
        }

        //3.生成验证码 6位
        String code = RandomUtil.randomNumbers(6);

        //4.保存验证码到redis中
        stringRedisTemplate.opsForValue().set(RedisConstants.LOGIN_CODE_KEY+phone,code,RedisConstants.LOGIN_CODE_TTL, TimeUnit.MINUTES);

        //5.发送验证码 (这里可以结合阿里云提供的第三方服务来做,但是为了方便,输出一下就可以)
        log.debug("发送短信验证码成功,验证码:{}",code);

        //6.返回ok
        return Result.ok();
    }

    /**
     * 登录功能
     * @param loginForm
     * @param session
     * @return
     */
    @Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {

        String phone = loginForm.getPhone();
        //1.校验手机号是否合法
        if(!RegexUtils.isPhoneInvalid(phone)){
            return Result.fail("手机号不合法");
        }
        //2.TODO 从Redis获取验证码并校验验证码是否一致
        String reidsCode = stringRedisTemplate.opsForValue().get(RedisConstants.LOGIN_CODE_KEY+phone);
        String userCode = loginForm.getCode();
        //3.验证码不一致返回错误信息
        if(reidsCode == null ||!reidsCode.equals(userCode)){
            return Result.fail("验证码错误");
        }
        //4.根据手机号到数据库查询用户
        User user = query().eq("phone", phone).one();
        //5.若用户不存在则创建用户并保存到数据库
        if(user == null){
            user = createUserByPhone(phone);
        }
        //6.保存用户信息到redis中
        //6.1 生成随机token 作为登录令牌
        String token = UUID.randomUUID().toString(true);
        //6.2 将UserDTO转为 HashMap
        UserDTO userDTO = new UserDTO();
        BeanUtils.copyProperties(user,userDTO);

        Map<String, Object>usertMap = new HashMap<>();
        usertMap.put("id", userDTO.getId().toString());
        usertMap.put("nickName", userDTO.getNickName());
        usertMap.put("icon", userDTO.getIcon());
        //6.3 存储到redis中
        String tokenKey = RedisConstants.LOGIN_USER_KEY+token;
        stringRedisTemplate.opsForHash().putAll(tokenKey,usertMap);
        //6.4 设置有效期
        stringRedisTemplate.expire(tokenKey,RedisConstants.LOGIN_USER_TTL,TimeUnit.MINUTES);

        //7.返回token
        return Result.ok(token);
    }

    /**
     * 根据手机号创建用户
     * @param phone
     * @return
     */
    private User createUserByPhone(String phone) {
        User user = new User();
        user.setPhone(phone);
        user.setNickName(SystemConstants.USER_NICK_NAME_PREFIX + RandomUtil.randomString(6));
        save(user);
        return user;
    }

(3)LoginInterceptor:(拦截请求,进行登录校验)

      @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1.获取请求头中的token
        String token = request.getHeader("authorization");
        if(StringUtils.isEmpty(token)){
            //token 为空 拦截,返回401
            response.setStatus(401);
            return false;
        }

        //2.通过token获取redis中的用户信息
        String tokenKey = RedisConstants.LOGIN_USER_KEY + token;
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash()
                .entries(tokenKey);

        //3.用户不存在,则拦截
        if(userMap.isEmpty()){
            //4.拦截,返回401
            response.setStatus(401);
            return false;
        }
        //5. 将HashMap转为UserDTO
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);

        //6.存在,保存到ThreadLocal中
        UserHolder.saveUser(userDTO);

        //7.重置token有效期
        stringRedisTemplate.expire(tokenKey,RedisConstants.LOGIN_USER_TTL,TimeUnit.MINUTES)
        //8.放行
        return true;
    }

(4)MVCconfig:(用于注册拦截器,使得拦截器生效)

@Configuration//由Spring创建的对象 可以使用 Autowired
public class MVCconfig implements WebMvcConfigurer {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
       registry.addInterceptor(new LoginInterceptor(stringRedisTemplate))
               .excludePathPatterns(
                       "/user/code",
                       "/user/login",
                       "/shop/**",
                       "/shop-type/**",
                       "/blog/hot",
                       "/upload/**",
                       "/voucher/**"
               );
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2295849.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

STM32F1学习——USART串口通信

一、USART通用同步异步收发机 USART的全称是Universal Synchronous/Asynchronous Receiver Transmitter &#xff0c; 通用同步异步收发机&#xff0c;但由于他主要以异步通信为主&#xff0c;所以他也叫UART。它遵循TTL电平标准&#xff0c;是一种全双工异步通信标准&#xff…

Docker 部署 MinIO | 国内阿里镜像

一、导读 Minio 是个基于 Golang 编写的开源对象存储套件&#xff0c;基于Apache License v2.0开源协议&#xff0c;虽然轻量&#xff0c;却拥有着不错的性能。它兼容亚马逊S3云存储服务接口。可以很简单的和其他应用结合使用&#xff0c;例如 NodeJS、Redis、MySQL等。 二、…

【R语言】相关系数

一、cor()函数 cor()函数是R语言中用于计算相关系数的函数&#xff0c;相关系数用于衡量两个变量之间的线性关系强度和方向。 常见的相关系数有皮尔逊相关系数&#xff08;Pearson correlation coefficient&#xff09;、斯皮尔曼秩相关系数&#xff08;Spearmans rank corre…

DeepSeek-R1技术报告快速解读

相关论文链接如下&#xff1a; DeepSeekMath: Pushing the Limits of Mathematical Reasoning in Open Language ModelsDeepSeek-R1: Incentivizing Reasoning Capability in LLMs via Reinforcement Learning 文章目录 一、论文脑图二、论文解读2.1 研究背景2.2 研究方法2.3 …

基于SpringBoot+Vue实现航空票务管理系统

作者简介&#xff1a;Java领域优质创作者、CSDN博客专家 、CSDN内容合伙人、掘金特邀作者、阿里云博客专家、51CTO特邀作者、多年架构师设计经验、多年校企合作经验&#xff0c;被多个学校常年聘为校外企业导师&#xff0c;指导学生毕业设计并参与学生毕业答辩指导&#xff0c;…

让文物“活”起来,以3D数字化技术传承文物历史文化!

文物&#xff0c;作为不可再生的宝贵资源&#xff0c;其任何毁损都是无法逆转的损失。然而&#xff0c;当前文物保护与修复领域仍大量依赖传统技术&#xff0c;同时&#xff0c;文物管理机构和专业团队的力量相对薄弱&#xff0c;亟需引入数字化管理手段以应对挑战。 积木易搭…

java项目之美妆产品进销存管理系统的设计与开发源码(ssm+mysql)

项目简介 美妆产品进销存管理系统的设计与开发实现了以下功能&#xff1a; 美妆产品进销存管理系统的设计与开发的主要使用者分为管理员登录后修改个人的密码。产品分类管理中&#xff0c;对公司内的所有产品分类进行录入&#xff0c;也可以对产品分类进行修改和删除。产品管…

保姆级教程Docker部署Zookeeper模式的Kafka镜像

目录 一、安装Docker及可视化工具 二、Docker部署Zookeeper 三、单节点部署 1、创建挂载目录 2、运行Kafka容器 3、Compose运行Kafka容器 4、查看Kafka运行状态 5、验证生产消费 四、部署可视化工具 1、创建挂载目录 2、Compose运行Kafka-eagle容器 3、查看Kafka-e…

idea插件开发dom4j报错:SAXParser cannot be cast to class org.xml.sax.XMLReader

手打不易&#xff0c;如果转摘&#xff0c;请注明出处&#xff01; 注明原文&#xff1a;https://blog.csdn.net/q258523454/article/details/145512328 dom4j报错 idea插件使用到了dom4j依赖&#xff0c;但是报错&#xff1a; I will print the stack trace then carry on…

【Go语言圣经】第八节:Goroutines和Channels

DeepSeek 说 Goroutines 和 Channels 最近非常流行询问DeepSeek某些相关概念或热点的解释&#xff0c;因此在开始系统性地学习《Go语言圣经》之前&#xff0c;我首先向DeepSeek进行了提问。具体的Prompt如下&#xff1a; 有关Golang当中的Goroutines和Channels&#xff0c;我现…

第3章 使用 Vue 脚手架

第3章 使用 Vue 脚手架 3.1 初始化脚手架3.1.1 说明3.1.2. 具体步骤3.1.3 分析脚手架结构1 总结2 细节分析1 配置文件2 src文件1 文件结构分析2 例子 3 public文件4 最终效果 3.2 ref属性3.3 props配置项3.4 mixin混入3.5 插件3.6 scoped样式3.7 Todo-list 案例3.7.1 组件化编码…

XILINX硬件设计-(1)LVDS接口总结

1.LVDS差分信号电路原理 LVDS指的是低压差分信号&#xff0c;是一种电平标准。 差分信号在串行通信中有着非常广泛的应用&#xff0c;典型应用有PCIE中的gen1&#xff0c;gen2&#xff0c;gen3&#xff0c;gen4&#xff0c;gen5&#xff0c;SATA接口&#xff0c;USB接口等。 …

单张照片可生成写实3D头部模型!Adobe提出FaceLift,从单一的人脸图像中重建出360度的头部模型。

FaceLift是Adobe和加州大学默塞德分校推出的单图像到3D头部模型的转换技术,能从单一的人脸图像中重建出360度的头部模型。FaceLift基于两阶段的流程实现:基于扩散的多视图生成模型从单张人脸图像生成一致的侧面和背面视图;生成的视图被输入到GS-LRM重建器中,产出详细的3D高斯表…

【AI】DeepSeek知识类任务和推理能力均表现优秀

2024 年 12 月 26 日&#xff0c;杭州深度求索&#xff08;DeepSeek AI&#xff09;发布 DeepSeek-V3 并同步开源&#xff0c;据介绍&#xff0c;DeepSeek-V3 多项评测成绩超越了 Qwen2.5-72B 和 Llama-3.1-405B 等其他开源模型&#xff0c;并在性能上和世界顶尖的闭源模型 GPT…

编程领域的IO模型(BIO,NIO,AIO)

目前对于市面上绝大多数的应用来说&#xff0c;不能实现的业务功能太少了。更多的是对底层细节&#xff0c;性能优化的追求。其中IO就是性能优化中很重要的一环。Redis快&#xff0c;mysql缓冲区存在的意义。都跟IO有着密切关系。IO其实我们都在用&#xff0c;输入输出流这块。…

DeepSeek为何能爆火

摘要&#xff1a;近年来&#xff0c;DeepSeek作为一款新兴的社交媒体应用&#xff0c;迅速在年轻人群体中走红&#xff0c;引发了广泛关注。本文旨在探讨DeepSeek为何能在短时间内爆火&#xff0c;从而为我国社交媒体的发展提供参考。首先&#xff0c;通过文献分析&#xff0c;…

【AIGC】语言模型的发展历程:从统计方法到大规模预训练模型的演化

博客主页&#xff1a; [小ᶻ☡꙳ᵃⁱᵍᶜ꙳] 本文专栏: AIGC | ChatGPT 文章目录 &#x1f4af;前言&#x1f4af;语言模型的发展历程&#xff1a;从统计方法到大规模预训练模型的演化1 统计语言模型&#xff08;Statistical Language Model, SLM&#xff09;&#xff1a;统…

【04】Java+若依+vue.js技术栈实现钱包积分管理系统项目-若依框架二次开发准备工作-以及建立初步后端目录菜单列-优雅草卓伊凡商业项目实战

【04】Java若依vue.js技术栈实现钱包积分管理系统项目-若依框架二次开发准备工作-以及建立初步后端目录菜单列-优雅草卓伊凡商业项目实战 项目背景 本项目经费43000元&#xff0c;需求文档如下&#xff0c;工期25天&#xff0c;目前已经过了8天&#xff0c;时间不多了&#x…

机器学习:朴素贝叶斯分类器

贝叶斯决策论是概率框架下实施决策的基本方法,对分类任务来说,在所有相关概率都已知的理想情形下,贝叶斯决策论考虑如何基于这些概率和误判损失来选择最优的类别标记。 贝叶斯定理是贝叶斯决策论的基础&#xff0c;描述了如何根据新的证据更新先验概率&#xff0c;贝叶斯定理&…

DeepSeek 大模型每个版本的特点以及运用场景对比

deepseek 网页地址:DeepSeek | 深度求索 1. DeepSeek-V1 发布时间:2024年1月 参数规模:预训练数据量2TB,具体参数未明确公开,推测为数十亿级别 功能特点: 编码能力:支持多种编程语言(如Python、Java、C++),可生成高质量代码框架。 长上下文处理:支持128K上下文窗口,…