• 场景应用:利用Redis实现分布式Session


    原理:Redis实现分布式Session

    web开发session

    在web开发中,我们会把用户的登录信息存储在session里。而session是依赖于cookie的,即服务器创建session时会给它分配一个唯一的ID,并且在响应时创建一个cookie用于存储这个SESSIONID。当客户端收到这个cookie之后,就会自动保存这个SESSIONID,并且在下次访问时自动携带这个SESSIONID,届时服务器就可以通过这个SESSIONID得到与之对应的session,从而识别用户的身。

    如下图:

    在这里插入图片描述

    分布式session同步问题

    现在的互联网应用,基本都是采用分布式部署方式,即将应用程序部署在多台服务器上,并通过nginx做统一的请求分发。而服务器与服务器之间是隔离的,它们的session是不共享的,这就存在session同步的问题了,如下图:

    在这里插入图片描述
    如果客户端第一次访问服务器,请求被分发到了服务器A上,则服务器A会为该客户端创建session。如果客户端再次访问服务器,请求被分发到服务器B上,则由于服务器B中没有这个session,所以用户的身份无法得到验证,从而产生了不一致的问题。

    分布式session解决方案

    解决这个问题的办法有很多,比如可以协调多个服务器,让他们的session保持同步。也可以在分发请求时做绑定处理,即将某一个IP固定分配给同一个服务器。但这些方式都比较麻烦,而且性能上也有一定的消耗。

    更合理的方式就是采用类似于Redis这样的高性能缓存服务器,来实现分布式session

    从上面的叙述可知,我们使用session保存用户的身份信息,本质上是要做两件事情。第一是保存用户的身份信息,第二是验证用户的身份信息。如果利用其它手段实现这两个目标,那么就可以不用session,或者说我们使用的是广义上的session了。

    具体实现的思路如下图,我们在服务端增加两段程序:
    在这里插入图片描述

    第一是创建令牌的程序,就是在用户初次访问服务器时,给它创建一个唯一的身份标识,并且使用cookie封装这个标识再发送给客户端。那么当客户端下次再访问服务器时,就会自动携带这个身份标识了,这和SESSIONID的道理是一样的,只是改由我们自己来实现了。另外,在返回令牌之前,我们需要将它存储起来,以便于后续的验证。而这个令牌是不能保存在服务器本地的,因为其他服务器无法访问它。因此,我们可以将其存储在服务器之外的一个地方,那么Redis便是一个理想的场所

    第二是验证令牌的程序,就是在用户再次访问服务器时,我们获取到了它之前的身份标识,那么我们就要验证一下这个标识是否存在了。验证的过程很简单,我们从Redis中尝试获取一下就可以知道结果

    实战:Redis实现分布式Session

    技术栈:Spring Session

    Spring Session是Spring的项目之一,Spring Session把servlet容器实现的httpSession替换为spring-session,专注于解决session管理问题。

    Spring Session提供了集群Session(Clustered Sessions)功能,默认采用外置的Redis来存储Session数据,以此来解决Session共享的问题。

    spring-session提供对用户session管理的一系列api和实现。提供了很多可扩展、透明的封装方式用于管理httpSession/WebSocket的处理。

    Spring Session支持功能

    • 轻易把session存储到第三方存储容器,框架提供了redis、jvm的map、mongo、gemfire、hazelcast、jdbc等多种存储session的容器的方式。这样可以独立于应用服务器的方式提供高质量的集群。

    • 同一个浏览器同一个网站,支持多个session问题。 从而能够很容易地构建更加丰富的终端用户体验。

    • Restful API,不依赖于cookie。可通过header来传递jessionID 。控制session id如何在客户端和服务器之间进行交换,这样的话就能很容易地编写Restful API,因为它可以从HTTP 头信息中获取session id,而不必再依赖于cookie

    • WebSocket和spring-session结合,同步生命周期管理。当用户使用WebSocket发送请求的时候

    Spring Session实战

    项目目录如下:
    在这里插入图片描述

    步骤1:依赖包

    首先要添加依赖包:因为是web应用。我们加入springboot的常用依赖包web,加入SpringSession、redis的依赖包,移支持把session存储在redis。

    这里因为是把seesion存储在redis,这样每个服务登录都是去查看redis中数据进行验证的,所有是分布式的。 这里要引入spring-session-data-redis和spring-boot-starter-redis

    
    <project xmlns="http://maven.apache.org/POM/4.0.0"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0modelVersion>
    
        <groupId>org.examplegroupId>
        <artifactId>redis-session-projectartifactId>
        <version>1.0-SNAPSHOTversion>
    
        <dependencies>
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-webartifactId>
            dependency>
    
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-testartifactId>
                <version>2.3.11.RELEASEversion>
                <scope>testscope>
            dependency>
    
            <dependency>
                <groupId>org.projectlombokgroupId>
                <artifactId>lombokartifactId>
                <version>1.18.8version>
            dependency>
    
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-redisartifactId>
                <version>1.4.7.RELEASEversion>
            dependency>
    
            <dependency>
                <groupId>org.springframework.sessiongroupId>
                <artifactId>spring-session-data-redisartifactId>
                <version>2.3.3.RELEASEversion>
            dependency>
    
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-webartifactId>
                <version>RELEASEversion>
                <scope>compilescope>
            dependency>
        dependencies>
    project>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    步骤2:启动类与配置文件

    编写启动类:

    package com.yyl;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    
    @SpringBootApplication
    public class RedisSessionApplication {
        public static void main(String[] args) {
            SpringApplication.run(RedisSessionApplication.class, args);
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    添加配置文件:

    spring.application.name=spring-boot-redis
    server.port=9090
    # 设置session的存储方式,采用redis存储
    spring.session.store-type=redis
    # session有效时长为15分钟
    server.servlet.session.timeout=PT15M
    
    ## Redis 配置
    ## Redis数据库索引
    spring.redis.database=1
    ## Redis服务器地址
    spring.redis.host=127.0.0.1
    ## Redis服务器连接端口
    spring.redis.port=6379
    ## Redis服务器连接密码(默认为空)
    spring.redis.password=
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    步骤3:实现逻辑

    创建测试实体类User

    模拟初始化用户数据,用map创建,这里主要测试redis就不连数据库了

    @Slf4j
    @RestController
    @RequestMapping(value = "/user")
    public class UserController {
    
        Map<String, User> userMap = new HashMap<>();
    
        public UserController() {
            //初始化1个用户,用于模拟登录
            User u1=new User(1,"user1","user1");
            userMap.put("user1",u1);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    登录:登录接口,根据用户名和密码登录,这里进行验证,如果验证登录成功,使用 session.setAttribute(session.getId(), user);把相关信息放到session中。

    @GetMapping(value = "/login")
    public String login(String username, String password, HttpSession session) {
        //模拟数据库的查找
        User user = this.userMap.get(username);
        if (user != null) {
            if (!password.equals(user.getPassword())) {
                return "用户名或密码错误!!!";
            } else {
                session.setAttribute(session.getId(), user);
                log.info("登录成功{}",user);
            }
        } else {
            return "用户名或密码错误!!!";
        }
        return "登录成功!!!";
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    在这里插入图片描述
    查找用户:模拟通过用户名查找用户

    /**
     * 通过用户名查找用户
     */
    @GetMapping(value = "/find/{username}")
    public User find(@PathVariable String username) {
        User user=this.userMap.get(username);
        log.info("通过用户名={},查找出用户{}",username,user);
        return user;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    获取session

    /**
     * 拿当前用户的session
     */
    @GetMapping(value = "/session")
    public String session(HttpSession session) {
        log.info("当前用户的session={}",session.getId());
        return session.getId();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    退出登录

    /**
     * 退出登录
     */
    @GetMapping(value = "/logout")
    public String logout(HttpSession session) {
        log.info("退出登录session={}",session.getId());
        session.removeAttribute(session.getId());
        return "成功退出!!";
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    步骤4:编写session拦截器

    session拦截器的作用:验证当前用户发来的请求是否有携带sessionid,如果没有携带,提示用户重新登录。

    package com.yyl.interceptor;
    
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.servlet.HandlerInterceptor;
    
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import javax.servlet.http.HttpSession;
    
    @Slf4j
    @Configuration
    public class SecurityInterceptor implements HandlerInterceptor {
    
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            HttpSession session = request.getSession();
            // 验证当前session是否存在,存在返回true true代表能正常处理业务逻辑
            if (session.getAttribute(session.getId()) != null) {
                log.info("session拦截器,session={},验证通过", session.getId());
                return true;
            }
            // session不存在,返回false,并提示请重新登录。
            response.setCharacterEncoding("UTF-8");
            response.setContentType("application/json; charset=utf-8");
            response.getWriter().write("请登录!!!!!");
            log.info("session拦截器,session={},验证失败", session.getId());
    
            return false;
        }
    }
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    步骤5:把拦截器注入到拦截器链中
    package com.yyl.config;
    
    import com.yyl.interceptor.SecurityInterceptor;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
    
    @Configuration
    public class SessionCofig implements WebMvcConfigurer {
    
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(new SecurityInterceptor())
                    //排除拦截的2个路径
                    .excludePathPatterns("/user/login")
                    .excludePathPatterns("/user/logout")
                    //拦截所有URL路径
                    .addPathPatterns("/**");
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    步骤6:测试

    登录user1用户:http://127.0.0.1:9090/user/login?username=user1&password=user1
    在这里插入图片描述
    在这里插入图片描述

    查询user1用户session:http://127.0.0.1:9090/user/session
    在这里插入图片描述
    在这里插入图片描述

    退出登录: http://127.0.0.1:9090/user/logout

    在这里插入图片描述
    在这里插入图片描述

    在本地浏览器我们也能找到存的session信息

    在这里插入图片描述

    测试源码

    https://download.csdn.net/download/weixin_45525272/86501938

  • 相关阅读:
    操作系统 线程的创建
    Unity的UnityStats: 属性详解与实用案例
    03-Zookeeper客户端使用
    JavaEE初阶——计算机是如何工作的
    【算法】重载sort的cmp的题
    &3 在浏览器中查看请求报文和响应报文
    excel 动态列导出
    【基础知识】PID(比例微分积分)控制
    基于Golang和WebSocket打造自已的反向代理
    计算机毕业设计Java中华美食文化网站(源码+系统+mysql数据库+Lw文档)
  • 原文地址:https://blog.csdn.net/weixin_45525272/article/details/126572629