帅气的 Spring Session 功能,基于 Redis 实现分布式会话,还可以整合 Spring Security!

Posted 芋道源码

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了帅气的 Spring Session 功能,基于 Redis 实现分布式会话,还可以整合 Spring Security!相关的知识,希望对你有一定的参考价值。

做积极的人,而不是积极废人!

源码精品专栏

 




摘要: 原创出处 http://www.iocoder.cn/Spring-Boot/Distributed-Session/ 「芋道源码」欢迎转载,保留摘要,谢谢!

  • 1. 概述
  • 2. Spring Session
  • 3. 快速入门 Spring Session + Redis
  • 4. 快速入门 Spring Session + MongoDB
  • 5. 整合 Spring Security
  • 6. 整合 Shiro
  • 7. 自定义 sessionid
  • 666. 彩蛋

本文在提供完整代码示例,可见 https://github.com/YunaiV/SpringBoot-Labs 的 lab-26 目录。

原创不易,给点个 Star 嘿,一起冲鸭!

1. 概述

艿艿信奉的话很多,其中很重要的一条:在考虑高性能之前,一定要做高可用。很多时候,我们常常陷入追求一个功能或者系统的高性能,却忽略了高可用。

为什么在这篇文章的开头提到这个段呢?对于任何系统,无论多小的访问量,一定要做系统的高可用。那么在我们部署生产环境下的 Tomcat 等 Web 容器的时候,一定是需要部署多个节点。此时,Session 的一致性就成为一个问题。为什么呢?

Session 的一致性,简单来理解,就是相同 sessionid 在多个 Web 容器下,Session 的数据要一致。

我们先以用户使用浏览器,Web 服务器为单台 TomcatA 举例子。

  • 浏览器在第一次访问 Web 服务器 TomcatA 时,TomcatA 会发现请求的 Cookie 中 不存在 sessionid ,所以创建一个 sessionid 为 X 的 Session ,同时将该 sessionid 写回给浏览器的 Cookie 中。
  • 浏览器在下一次访问 Web 服务器 TomcatA 时,TomcatA 会发现请求的 Cookie 中 已存在 sessionid 为 X ,则直接获得 X 对应的 Session 。

友情提示,Tomcat 产生的 sessionid 为 jsessionid 。

如果胖友对 Cookie 和 Session 的概念不是很清晰,建议可以先看下 《彻底理解 Cookie、Session、Token》 文章。

我们再以用户使用浏览器,Web 服务器为两台 TomcatA、TomcatB 举例子。

  • 接上述例子,浏览器已经访问 TomcatA ,获得 sessionid 为 X 。同时,在多台 Tomcat 的情况下,我们需要采用 nginx 做负载均衡。
  • 浏览器又发起一次请求访问 Web 服务器,Nginx 负载均衡转发请求到 TomcatB 上。TomcatB 会发现请求的 Cookie 中 已存在 sessionid 为 X ,则直接获得 X 对应的 Session 。结果呢,找不到 X 对应的 Session ,只好创建一个 sessionid 为 X 的 Session 。
  • 此时,虽然说浏览器的 sessionid 是 X ,但是对应到两个 Tomcat 中两个 Session 。那么,如果在 TomcatA 上做的 Session 修改,TomcatB 的 Session 还是原样,这样就会出现 Session 不一致的问题。

既然会出现 Session 不一致的问题,我们就要想办法让它们一致。一般来说,有三种方案。

第一种,Session 黏连。

使用 Nginx 实现会话黏连,将相同 sessionid 的浏览器所发起的请求,转发到同一台服务器。这样,就不会存在多个 Web 服务器创建多个 Session 的情况,也就不会发生 Session 不一致的问题。

不过,这种方式目前基本不被采用。因为,如果一台服务器重启,那么会导致转发到这个服务器上的 Session 全部丢失。

具体怎么实现这种方式,可以看看 《Nginx 第三方模块 —— nginx-sticky-module 的使用(基于cookie的会话保持)》 文章。

第二种,Session 复制。

Web 服务器之间,进行 Session 复制同步。仅仅适用于实现 Session 复制的 Web 容器,例如说 Tomcat 、Weblogic 等等。

不过,这种方式目前基本也不被采用。试想一下,如果我们有 5 台 Web 服务器,所有的 Session 都要同步到每一个节点上,一个是效率低,一个是浪费内存。

具体怎么实现这种方式,可以看看 [《Session 共享 —— Tomcat 集群 Session 复制》](session 共享-tomcat集群session复制) 文章。

第三种,Session 外部化存储。

不同于上述的两种方案,Session 外部化存储,考虑不再采用 Web 容器的内存中存储 Session ,而是将 Session 存储外部化,持久化到 mysql、Redis、MongoDB 等等数据库中。这样,Tomcat 就可以无状态化,专注提供 Web 服务或者 API 接口,未来拓展扩容也变得更加容易。

而实现 Session 外部化存储也有两种方式:

① 基于 Tomcat、Jetty 等 Web 容器自带的拓展,使用读取外部存储器的 Session 管理器。例如说:

  • 《Redisson Tomcat会话管理器(Tomcat Session Manager)》 ,实现将 Tomcat 使用 Redis 存储 Session 。
  • 《Jetty 集群配置 Session 存储到 MySQL、MongoDB》 ,实现 Jetty 使用 MySQL、MongoDB 存储 Session 。

② 基于应用层封装 HttpServletRequest 请求对象,包装成自己的 RequestWrapper 对象,从而让实现调用 HttpServletRequest#getSession() 方法时,获得读写外部存储器的 SessionWrapper 对象。例如说,稍后我们会看到的本文的主角 Spring Session 。

  • Spring Session 提供了 SessionRepositoryFilter 过滤器,它会过滤请求时,将请求 HttpServletRequest 对象包装成 SessionRepositoryRequestWrapper 对象。代码如下:

    // SessionRepositoryFilter.java

    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // sessionRepository 是访问外部数据源的操作类,例如说访问 Redis、MySQL 等等
        request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);
        

        // 将请求和响应进行包装成 SessionRepositoryRequestWrapper 和 SessionRepositoryResponseWrapper 对象
        SessionRepositoryFilter<S>.SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryFilter.SessionRepositoryRequestWrapper(request, response, this.servletContext);
        SessionRepositoryFilter.SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryFilter.SessionRepositoryResponseWrapper(wrappedRequest, response);

        // 继续执行下一个过滤器
        try {
            filterChain.doFilter(wrappedRequest, wrappedResponse);
        } finally {
            // 请求结束,提交 Session 到外部数据源
            wrappedRequest.commitSession();
        }

    }
  • 调用 SessionRepositoryRequestWrapper#getSession() 方法时,返回的是自己封装的 HttpSessionWrapper 对象。代码如下:

    // SessionRepositoryFilter#SessionRepositoryRequestWrapper.java

     @Override
     public HttpSessionWrapper getSession() {
      return getSession(true);
     }
  • 后续,我们调用 HttpSessionWrapper 的方法,例如说 HttpSessionWrapper#setAttribute(String name, Object value) 方法,访问的就是外部数据源,而不是内存中。

当然 ① 和 ② 两种方案思路是类似且一致的,只是说拓展的提供者和位置不同。

以上是关于帅气的 Spring Session 功能,基于 Redis 实现分布式会话,还可以整合 Spring Security!的主要内容,如果未能解决你的问题,请参考以下文章

spring session使用日志

spring security 基于session的认证授权

spring 基于session方式的bean创建

Spring Cloud——基于Dubbo的分布式Session解决方案

Spring Cloud微服务安全实战_5-6_基于session的SSO优缺点以及适用场景

Spring Security---验证码详解