Spring_Session解决Session共享的问题(二十三)上

本文涉及的产品
云数据库 Redis 版,社区版 2GB
推荐场景:
搭建游戏排行榜
简介: 一. Spring-Session介绍 二. SpringBoot 整合 Spring-Session 使用 三. SpringSession 的详细使用 三.一 pom.xml 添加依赖 三.二 数据库信息 三.三 其他的相关信息 三.四 添加登录和权限拦截的过滤器 LoginInterceptor

一. Spring-Session介绍

一.一 Spring-Session使用的场景

HttpSession是通过Servlet容器进行创建和管理的,在单机环境中。通过Http请求创建的Session信息是存储在Web服务器内存中,如Tomcat/Jetty。


假如当用户通过浏览器访问应用服务器,session信息中保存了用户的登录信息,并且session信息没有过期失,效那么用户就一直处于登录状态,可以做一些登录状态的业务操作

1.png

但是现在很多的服务器都采用分布式集群的方式进行部署,一个Web应用,可能部署在几台不同的服务器上,通过LVS或者Nginx等进行负载均衡(一般使用Nginx+Tomcat实现负载均衡)。此时来自同一用户的Http请求将有可能被分发到不同的web站点中去(如:第一次分配到A站点,第二次可能分配到B站点)。那么问题就来了,如何保证不同的web站点能够共享同一份session数据呢?假如用户在发起第一次请求时候访问了A站点,并在A站点的session中保存了登录信息,当用户第二次发起请求,通过负载均衡请求分配到B站点了,那么此时B站点能否获取用户保存的登录的信息呢?答案是不能的,因为上面说明,Session是存储在对应Web服务器的内存的,不能进行共享,此时Spring-session就出现了,来帮我们解决这个session共享的问题!

2.png

一.二 如何进行Session共享

简单点说就是请求http请求经过Filter职责链,根据配置信息过滤器将创建session的权利

由tomcat交给了Spring-session中的SessionRepository,通过Spring-session创建会话,

并保存到对应的地方。

3.png

实际上实现Session共享的方案很多,其中一种常用的就是使用Tomcat、Jetty等服务器提供的Session共享功能,将Session的内容统一存储在一个数据库(如MySQL)或缓存(如Redis,Mongo)中

二. SpringBoot 整合 Spring-Session 使用

按照上一章节的方式 创建对应的项目 SpringBoot_Session

二.一 pom.xml 添加依赖

  <!--依赖 data-redis的依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!--不能忘记这个依赖-->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>
        <!--添加cache的依赖信息-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-cache</artifactId>
        </dependency>
        <!--添加 session的依赖-->
        <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-data-redis</artifactId>
        </dependency>

二.二 application.yml 配置redis和session

采用多环境配置

server:
  servlet:
    context-path: /Session
# 引入 数据库的相关配置
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/springboot?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=UTF-8&useSSL=false&allowMultiQueries=true
    username: root
    password: abc123
    # 配置thymeleaf的相关信息
    thymeleaf:
      # 开启视图解析
      enabled: true
      #编码格式
      encoding: UTF-8
      #前缀配置
      prefix: classpath:/templates/
      # 后缀配置
      suffix: .html
      #是否使用缓存 开发环境时不设置缓存
      cache: false
      # 格式为 HTML 格式
      mode: HTML5
      # 配置类型
      servlet:
        content-type: text/html
  # 配置Redis的使用
  redis:
    database: 15 # 所使用的数据库  默认是0
    host: 127.0.0.1  #所使用的redis的主机地址
    port: 6379  # 端口号  默认是 6379
    password: zk123 # 密码
    timeout: 5000 # 超时时间  5000毫秒
    # 连接池 lettuce 的配置
    lettuce:
      pool:
        max-active: 100
        min-idle: 10
        max-wait: 100000
  profiles:
    active: 8081   # 默认启用的端口号是 8081
  # 配置session的相关信息   
  session:
    store-type: redis  # 配置存储的类型
    timeout: 3600  # 配置过期时间
    redis:
      flush-mode: on_save # 保存时刷新
      namespace: springSession # 命令空间
#整合mybatis时使用的
mybatis:
  #包别名
  type-aliases-package: top.yueshushu.learn.pojo
  #映射文件路径
  mapper-locations: classpath:mybatis/mapper/**/*.xml
  configuration:
    #日志信息
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
#  多环境配置,默认为 8081 
---
server:
  port: 8081
spring:
  profiles: 8081
---
server:
  port: 8082
spring:
  profiles: 8082

二.三 启动类 SessionApplication

@MapperScan("top.yueshushu.learn.mapper")
@SpringBootApplication
//开启缓存
@EnableCaching
public class SessionApplication {
    public static void main(String[] args) {
        SpringApplication.run(SessionApplication.class,args);
        System.out.println("共享 Session");
    }
}

不要忘记 CacheConfig 和 RedisConfig 的配置信息

二.四 SessionController 创建和获取Session

创建一个Controller 类, 用于创建和获取对应的Session 信息

@RestController
public class SessionController {
    @Value("${server.port}")
    private String port;
    @RequestMapping("/createSession")
    public String createSession(HttpSession httpSession){
        String sessionId=httpSession.getId();
        httpSession.setAttribute("name",port+",两个蝴蝶飞"+sessionId);
        httpSession.setAttribute("sname",port+":abc");
        return sessionId+"创建端口号是:"+port+"的应用创建Session,属性是:"+httpSession.getAttribute("name").toString();
    }
    @RequestMapping("/getSession")
    public String getSession(HttpSession httpSession){
        return "访问端口号是:"+port+",获取Session属性是:"+httpSession.getAttribute("name").toString();
    }
}

二.五 测试Session共享

将项目进行打包, 然后启动项目.

4.png启动 8081 端口的项目

 java -jar learn-1.0-SNAPSHOT.jar --spring.profiles.active=8081

启动8082的端口的项目

java -jar learn-1.0-SNAPSHOT.jar --spring.profiles.active=8082

8081 项目 创建Session

5.png

8082 项目 试图获取Session (不是同一个项目,按照以前的逻辑是获取不到的。 但是添加了 Spring-session 是可以获取到的)

7.png

可以获取到 Session

查看Redis 的存储信息

8.png

会存储相关的信息, 但是值是乱码.

可以通过 fastjson 进行序列化,解决这个问题。

在config 包下,添加关于 Spring-Session的配置信息, RedisSessionConfig

1.首先要在 pom.xml 添加 fastjson的依赖

    <!--解决spring-session处理缓存时乱码的问题-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.69</version>
        </dependency>

2.RedisSessionConfig.java 中进行配置序列化方式

@Configuration
@EnableRedisHttpSession
public class RedisSessionConfig {
    @Bean
    public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
        // 使用 FastJsonRedisSerializer 来序列化和反序列化redis 的 value的值
        FastJsonRedisSerializer<Object> serializer = new FastJsonRedisSerializer<>(Object.class);
        ParserConfig.getGlobalInstance().addAccept("com.muzz");
        FastJsonConfig fastJsonConfig = new FastJsonConfig();
        fastJsonConfig.setCharset(StandardCharsets.UTF_8);
        serializer.setFastJsonConfig(fastJsonConfig);
        return serializer;
    }
}

删除以前的Redis数据,重新访问 createSession , 生成新的Session

9.png

解决乱码问题。

但是,不建议这么做. 老蝴蝶只是演示一下,项目里面,不用这一个

三. SpringSession 的详细使用

关于访问权限的问题,可以看老蝴蝶以前写的文章: RBAC

关于 静态资源的问题, 可以看老蝴蝶以前写的文章: SpringBoot静态资源整合Bootstrap(十)

三.一 pom.xml 添加依赖

     <!--依赖 data-redis的依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!--不能忘记这个依赖-->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>
        <!--添加cache的依赖信息-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-cache</artifactId>
        </dependency>
        <!--添加 session的依赖-->
        <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-data-redis</artifactId>
        </dependency>
        <!--解决spring-session处理缓存时乱码的问题-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.69</version>
        </dependency>
        <!--引入 spring-boot-starter-thymeleaf的依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <!--添加一个webjar jquery-->
        <dependency>
            <groupId>org.webjars</groupId>
            <artifactId>jquery</artifactId>
            <version>3.5.1</version>
        </dependency>
        <!--引入bootstrap-->
        <dependency>
            <groupId>org.webjars</groupId>
            <artifactId>bootstrap</artifactId>
            <version>3.4.1</version>
        </dependency>

三.二 数据库信息

在 springboot 数据库里面 添加 user 表, 创建两个用户

/*!40101 SET NAMES utf8 */;
-- 创建员工 user 表
CREATE TABLE `user` (
    `id` int(11) NOT NULL AUTO_INCREMENT,
    `name` varchar(15) DEFAULT NULL,
    `sex` varchar(20) DEFAULT NULL,
    `age` int(6) DEFAULT NULL,
    `description` varchar(50) DEFAULT NULL,
    `account` varchar(100) DEFAULT NULL,
    `password` varchar(100) DEFAULT NULL,
    PRIMARY KEY (`id`),
    UNIQUE KEY `user_un_account` (`account`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- 插入员工信息数据
insert into user(name,sex,age,description,account,password)
values ('两个蝴蝶飞','男',26,'一个快乐的程序员','yzl','123456'),
       ('周小欢','女',22,'一个小坏蛋','zxh','123456');

三.三 其他的相关信息

application.yml 配置信息不需要改变, 启动类不需要改变,

创建User 类和 对应的 mapper, service 等基础的服务( 与Mybatis一样)

提供一个 根据员工的 账号 account 和 密码 password 的查询服务

UserServiceImpl.java

  @Override
    public User findByAccountAndPassword(String name, String password) {
        return userMapper.findByAccountAndPassword(name,password);
    }

UserMapper.xml

 <select id="findByAccountAndPassword" resultType="top.yueshushu.learn.pojo.User">
        select * from user where account=#{account}
        and password=#{password}
    </select>

三.四 添加登录和权限拦截的过滤器 LoginInterceptor

放置在 interceptor 包下.

LoginInterceptor.java类:

package top.yueshushu.learn.interceptor;
import org.springframework.util.CollectionUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import top.yueshushu.learn.pojo.User;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.util.ArrayList;
import java.util.List;
/**
 *
 * @author 两个蝴蝶飞
 *
 * 登录和授权拦截器
 */
public class LoginInterceptor implements HandlerInterceptor {
  //不需要登录验证的url
  private static List<String> noLoginValidateUrl;
  //不需要权限验证的url
  private static List<String> noPriValidateUrl;
  //跳转到的登录页面
  private static String LOGIN_URL;
  //没有权限的界面
  private static String NO_PRIVILEGE_URL;
  static{
    noLoginValidateUrl=new ArrayList<String>();
    //静态资源
    noLoginValidateUrl.add("/static/");
    noLoginValidateUrl.add("/webjars/");
    noLoginValidateUrl.add("/templates/");
    noLoginValidateUrl.add("/login.html");
    noLoginValidateUrl.add("/noPrivilege.html");
    //登录页面
    noLoginValidateUrl.add("/toLogin");
    //登录方法
    noLoginValidateUrl.add("/login");
    noPriValidateUrl=new ArrayList<String>();
    LOGIN_URL="/login.html";
    NO_PRIVILEGE_URL="/noPrivilege.html";
  }
  @Override
  public void afterCompletion(HttpServletRequest arg0, HttpServletResponse arg1, Object arg2, Exception arg3)
      throws Exception {
    // TODO 自动生成的方法存根
  }
  @Override
  public void postHandle(HttpServletRequest arg0, HttpServletResponse arg1, Object arg2, ModelAndView arg3)
      throws Exception {
    // TODO 自动生成的方法存根
  }
  @Override
  public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object arg2) throws Exception {
    //获取Session
    HttpSession session=req.getSession();
    //请求路径
    String realPath=req.getRequestURI();
    System.out.println("地址是:"+realPath);
    //验证是否在 不需要验证登录的url里面
    if(isContain(realPath,1)){
      return true;
    }
    //如果为空,表示没有登录
    if(session.getAttribute("loginUser")==null){
      req.getRequestDispatcher(LOGIN_URL).forward(req,resp);
      return false;
    }else{
      //最后一个 //为权限
      String privilegeUrl=realPath.substring(realPath.lastIndexOf("/"));
      //如果不为空,表示登录了。
      //重新获取全部权限 , 需要缓存, 这儿不用缓存。
      User user=(User)session.getAttribute("loginUser");
      List<String> privileges=(List<String>)session.getAttribute("privilegeList_"+user.getId());
      boolean isHavePri=true;
      if(CollectionUtils.isEmpty(privileges)||!privileges.contains(privilegeUrl)){
        isHavePri=false;
      }
      if(isHavePri){
        //放行
        return true;
      }else{
        req.getRequestDispatcher(NO_PRIVILEGE_URL).forward(req,resp);
        return false;
      }
    }
  }
  private boolean isContain(String realPath,int type){
    List<String> urls;
    if(type==1){
      urls=noLoginValidateUrl;
    }else{
      urls=noPriValidateUrl;
    }
    boolean flag=false;
    for(String url:urls){
      //包括,返回-1
      if(realPath.indexOf(url)!=-1){
        flag=true;
        break;
      }
    }
    return flag;
  }
}
相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
8月前
|
存储 NoSQL Java
Spring Session分布式会话管理
Spring Session分布式会话管理
54 0
|
4月前
|
存储 NoSQL Redis
spring-session-core排除某些接口不设置session
spring-session-core排除某些接口不设置session
39 0
|
8月前
|
Java Spring
Spring Boot 中的 Session 是什么,如何使用
Spring Boot 中的 Session 是什么,如何使用
|
8月前
|
缓存 NoSQL Java
Spring Session MongoDB管理会话
Spring Session MongoDB管理会话
63 0
|
10月前
|
安全 Java Spring
SpringBoot整合Spring Security,使用Session方式的验证码(六)
这里用到了一个常量类,到时候做验证码判断时用的比较多:
92 0
|
10月前
|
设计模式 缓存 NoSQL
简述 Spring Session 集成 Redis 底层实现及自定义扩展配置
简述 Spring Session 集成 Redis 底层实现及自定义扩展配置
354 0
|
12月前
|
NoSQL Java Redis
Spring Session - 源码解读
Spring Session - 源码解读
248 0
|
12月前
|
存储 JSON NoSQL
Spring Session - 使用Spring Session从零到一构建分布式session
Spring Session - 使用Spring Session从零到一构建分布式session
149 0
|
12月前
|
存储 NoSQL Java
Spring Session - Cookie VS Session VS Token 以及 Session不一致问题的N种解决方案
Spring Session - Cookie VS Session VS Token 以及 Session不一致问题的N种解决方案
120 0
|
存储 NoSQL Java
Spring Session中存放于Redis中的PRINCIPAL_NAME_INDEX_NAME没有设置过期时间
Spring Session中存放于Redis中的PRINCIPAL_NAME_INDEX_NAME没有设置过期时间
245 0