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

        在学习项目的过程中遇到了使用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/**"
               );
    }
}

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【目标检测】模型验证:K-Fold 交叉验证

K-Fold 交叉验证 1、引言1.1 K 折交叉验证概述 2、配置2.1 数据集2.2 安装包 3、 实战3.1 生成物体检测数据集的特征向量3.2 K 折数据集拆分3.3 保存记录3.4 使用 K 折数据分割训练YOLO 4、总结 1、引言 我们将利用YOLO 检测格式和关键的Python 库&#xff08;如 sklearn、pan…

深度学习系列--04.梯度下降以及其他优化器

目录 一.梯度概念 1.一元函数 2.二元函数 3.几何意义上的区别 二.梯度下降 1.原理 2.步骤 3.示例代码&#xff08;Python&#xff09; 4.不同类型的梯度下降 5.优缺点 三.动量优化器&#xff08;Momentum&#xff09; 适用场景 1.复杂地形的优化问题 2.数据具有噪声的问…

π0开源了且推出自回归版π0-FAST——打造机器人动作专用的高效Tokenizer:比扩散π0的训练速度快5倍但效果相当

前言 过去的半个多月 对于大模型 deepseek火爆全球&#xff0c;我对其的解读也写成了整整一个系列 详见《火爆全球的DeepSeek系列模型》&#xff0c;涉及对GRPO、MLA、V3、R1的详尽细致深入的解读 某种意义来讲&#xff0c;deepseek 相当于把大模型的热度 又直接拉起来了——…

导航守卫router.beforeEach

router.beforeEach 是一个全局前置守卫&#xff0c;在每次路由跳转之前都会触发。 //index.jsrouter.beforeEach((to, from, next) > {// 打印即将要进入的目标路由信息console.log(即将要进入的目标路由信息:, to)// 打印当前正要离开的路由信息console.log(当前正要离开的…

[ESP32:Vscode+PlatformIO]添加第三方库 开源库 与Arduino导入第三方库的区别

前言 PlatformIO与Arduino在添加第三方库方面的原理存在显著差异 在PlatformIO中&#xff0c;第三方库的使用是基于项目&#xff08;工程&#xff09;的。具体而言&#xff0c;只有当你为一个特定的项目添加了某个第三方库后&#xff0c;该项目才能使用该库。这些第三方库的文…

了解AI绘图,Stable Diffusion的使用

AI绘图对GPU算力要求较高。 个人电脑配置可参考&#xff1a; CPU&#xff1a;14600kf 盒装 显卡&#xff1a;RTX 4080金属大师 OC&#xff0c;16G显存 主板&#xff1a;z790吹雪d4 内存&#xff1a;芝奇皇家戟4000c18,162G 硬盘&#xff1a;宏基gm7000 1T 散热&#xff1a;追风…

linux环境自动化golang项目启动脚本解析

一.场景介绍 当在本地创建了golang项目,修改了代码功能,怎么在远程测试服务器上更新该功能呢,可以使用下面的步骤来解决该问题(这只是其中一种方法): (1).推送最新代码到远程仓库 (2).在测试服务器上创建该项目并拉取最新代码 (3).创建deploy.sh脚本 (4).运行deploy.sh脚本 二.…

归一化与伪彩:LabVIEW图像处理的区别

在LabVIEW的图像处理领域&#xff0c;归一化&#xff08;Normalization&#xff09;和伪彩&#xff08;Pseudo-coloring&#xff09;是两个不同的概念&#xff0c;虽然它们都涉及图像像素值的调整&#xff0c;但目的和实现方式截然不同。归一化用于调整像素值的范围&#xff0c…

基于DeepSeek API和VSCode的自动化网页生成流程

1.创建API key 访问官网DeepSeek &#xff0c;点击API开放平台。 在开放平台界面左侧点击API keys&#xff0c;进入API keys管理界面&#xff0c;点击创建API key按钮创建API key&#xff0c;名称自定义。 2.下载并安装配置编辑器VSCode 官网Visual Studio Code - Code Editing…

Open WebUI项目源码学习记录(从0开始基于纯CPU环境部署一个网页Chat服务)

感谢您点开这篇文章:D&#xff0c;鼠鼠我是一个代码小白&#xff0c;下文是学习开源项目Open WebUI过程中的一点笔记记录&#xff0c;希望能帮助到你&#xff5e; 本人菜鸟&#xff0c;持续成长&#xff0c;能力不足有疏漏的地方欢迎一起探讨指正&#xff0c;比心心&#xff5e…

SSM仓库物品管理系统 附带详细运行指导视频

文章目录 一、项目演示二、项目介绍三、运行截图四、主要代码1.用户登录代码&#xff1a;2.保存物品信息代码&#xff1a;3.删除仓库信息代码&#xff1a; 一、项目演示 项目演示地址&#xff1a; 视频地址 二、项目介绍 项目描述&#xff1a;这是一个基于SSM框架开发的仓库…

Python微博动态爬虫

本文是刘金路的《语言数据获取与分析基础》第十章的扩展&#xff0c;详细解释了如何利用Python进行微博爬虫&#xff0c;爬虫内容包括微博指定帖子的一级评论、评论时间、用户名、id、地区、点赞数。 整个过程十分明了&#xff0c;就是用户利用代码模拟Ajax请求&#xff0c;发…

时序数据库:Influxdb详解

文章目录 一、简介1、简介2、官网 二、部署1、安装2、配置&#xff08;1&#xff09;用户初始化 三、入门&#xff08;Web UI&#xff09;1、加载数据&#xff08;1&#xff09;上传数据文件&#xff08;2&#xff09;代码接入模板 2、管理存储桶&#xff08;1&#xff09;创建…

unity学习32:角色相关1,基础移动控制

目录 0 应用商店 1 角色上新增CharacterController 组件 1.1 角色上新增CharacterController 组件 1.2 如果没有这个则会报错 2 速度 2.1 默认速度&#xff0c;按帧率计算 2.2 修改速度为按时间计算 2.3 movespeed&#xff0c;基础是1米/秒&#xff0c;这个就是每 move…

Centos Ollama + Deepseek-r1+Chatbox运行环境搭建

Centos Ollama Deepseek-r1Chatbox运行环境搭建 内容介绍下载ollama在Ollama运行DeepSeek-r1模型使用chatbox连接ollama api 内容介绍 你好&#xff01; 这篇文章简单讲述一下如何在linux环境搭建 Ollama Deepseek-r1。并在本地安装的Chatbox中进行远程调用 下载ollama 登…

mysql8.0使用pxc实现高可用

环境准备 准备三台虚拟机&#xff0c;其对应的主机名和IP地址为 pxc-1192.168.190.129pxc-2192.168.190.133pxc-3192.168.190.134 解析,都要做解析 测试 下载pxc的安装包&#xff0c; 官网&#xff1a;https://www.percona.com/downloads 选择8.0的版本并下载&#xff0c;…

LabVIEW污水生化处理在线监测

污水处理是环保领域的重要工作&#xff0c;传统污水处理方法在监测方面存在实时性差、操作不便等问题。为解决这些问题&#xff0c;本项目设计并实现了一套基于LabVIEW的污水生化处理在线监测平台&#xff0c;能够实时监测污水处理过程中的关键参数&#xff0c;如温度、pH值、溶…

【AI学习】关于 DeepSeek-R1的几个流程图

遇见关于DeepSeek-R1的几个流程图&#xff0c;清晰易懂形象直观&#xff0c;记录于此。 流程图一 来自文章《Understanding Reasoning LLMs》&#xff0c; 文章链接&#xff1a;https://magazine.sebastianraschka.com/p/understanding-reasoning-llms?continueFlagaf07b1a0…

vs封装dll 给C#使用

一&#xff0c;vs创建控制台应用 创建控制台应用得好处时&#xff0c;我们可以自己测试接口&#xff0c;如果接口没有问题&#xff0c;改成dll重新编译一遍就可以。 二&#xff0c; 创建一个c 类&#xff0c;将所需提供得功能 封装到类中。 这样可以将 所有功能&#xff0c;进…

ubuntu20使用tigervnc远程桌面配置记录

一、安装tigervnc sudo apt install tigervnc-common sudo apt install tigervnc-standalone-server二、增加配置文件 安装完后新增配置文件&#xff1a;vim ~/.vnc/xstartup #!/bin/sh #Uncomment the following two lines for normal desktop: #unset SESSION_MANAGER #ex…