zoukankan      html  css  js  c++  java
  • spring-session用mysql实现session共享实践

    前段时间,写了篇<spring-session-data-redis解决session共享的问题>文章,介绍了spring-session使用redis存储实现session共享的内部机制,

    遇到很多人提问到jdbc的实现或者为什么只有理论没有实践?索性就实践一下jdbc方式。

    1.spring-session实现了使用不同存储介质保存共享的session的api

    spring-session实现session共享的实现有三种:

    jdbc:使用数据库做介质,支持多种数据,如oracle,sqlserver,mysql等,对应的实现类

    RedisOperationsSessionRepository

     

                      <图一 支持的数据库类型及脚本>

    redis:使用redis做存储介质,对应的实现类为:

    JdbcOperationsSessionRepository

    Hazelcast:使用内存做存储介质,对应的实现类为:

    HazelcastSessionRepository

    Hazelcast作为一个高度可扩展的数据分发和集群平台,提供了高效的、可扩展的分布式数据存储、数据缓存。Hazelcast是开源的,在分布式技术方面,Hazelcast提供了十分友好的接口供开发者选择,如Map,Queue,ExecutorService, Lock和Jcache。
    Hazelcast的稳定性很高,分布式应用可以使用Hazelcast进行存储数据、同步数据、发布订阅消息等。Hazelcast是基于Java开发的,其客户端有Java, C/C++, .NET以及REST。Hazelcast同时也支持memcache协议。它很好的支持了Hibernate,可以很容易的在当今流行的数据库系统中应用。

    2.spring-session-jdbc使用mysql保存共享session的实现实践

    2.0 准备工作

      如<图一 支持的数据库类型及脚本>所示,找到schema-mysql.sql脚本,放入sql-client客户端执行,如sqlyog,红色部分是我自己添加的,为了可以重复执行的。

    DROP TABLE IF EXISTS SPRING_SESSION_ATTRIBUTES;
    DROP TABLE IF EXISTS SPRING_SESSION;
    CREATE TABLE SPRING_SESSION (
        PRIMARY_ID CHAR(36) NOT NULL,
        SESSION_ID CHAR(36) NOT NULL,
        CREATION_TIME BIGINT NOT NULL,
        LAST_ACCESS_TIME BIGINT NOT NULL,
        MAX_INACTIVE_INTERVAL INT NOT NULL,
        EXPIRY_TIME BIGINT NOT NULL,
        PRINCIPAL_NAME VARCHAR(100),
        CONSTRAINT SPRING_SESSION_PK PRIMARY KEY (PRIMARY_ID)
    ) ENGINE=INNODB ROW_FORMAT=DYNAMIC;
    
    CREATE UNIQUE INDEX SPRING_SESSION_IX1 ON SPRING_SESSION (SESSION_ID);
    CREATE INDEX SPRING_SESSION_IX2 ON SPRING_SESSION (EXPIRY_TIME);
    CREATE INDEX SPRING_SESSION_IX3 ON SPRING_SESSION (PRINCIPAL_NAME);
    
    CREATE TABLE SPRING_SESSION_ATTRIBUTES (
        SESSION_PRIMARY_ID CHAR(36) NOT NULL,
        ATTRIBUTE_NAME VARCHAR(200) NOT NULL,
        ATTRIBUTE_BYTES BLOB NOT NULL,
        CONSTRAINT SPRING_SESSION_ATTRIBUTES_PK PRIMARY KEY (SESSION_PRIMARY_ID, ATTRIBUTE_NAME),
        CONSTRAINT SPRING_SESSION_ATTRIBUTES_FK FOREIGN KEY (SESSION_PRIMARY_ID) REFERENCES SPRING_SESSION(PRIMARY_ID) ON DELETE CASCADE
    ) ENGINE=INNODB ROW_FORMAT=DYNAMIC;

    此时创建了两张表

      SPRING_SESSION

      SPRING_SESSION_ATTRIBUTES

     通过PRIMARY_ID关联,执行结果如下:

    2.1 使用sts创建spring-boot项目,项目名称session-jdbc,使用jdbc,mysql,web(方便测试)的start,创建完项目如下:

    2.2 添加spring-session-jdbc依赖

            <dependency>
                <groupId>org.springframework.session</groupId>
                <artifactId>spring-session-jdbc</artifactId>
            </dependency>

    2.3 配置jdbc属性application.properties

    spring.session.store-type=JDBC
    spring.datasource.url=jdbc:mysql://localhost:3306/www?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=false
    spring.datasource.username=root
    spring.datasource.password=wangwei456
    spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
    spring.session.jdbc.cleanup-cron=0 * * * * *

    注意,新版mysql的url需要增加

    serverTimezone选项

    而且,driver-class-name也变为com.mysql.cj.jdbc.Driver,否则有报警。

     2.4 创建测试controller

    package com.example.demo;
    
    import javax.servlet.http.HttpSession;
    
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    @RequestMapping("/jdbc")
    public class TestController {
        @RequestMapping("/welcome")
        public String welcome(HttpSession httpSession) {
            httpSession.setAttribute("hello", "world");
            return "hello world !";
    
        }
    }

    2.5 测试

    启动spring-boot项目session-jdbc,访问http://localhost:8080/jdbc/welcome/,返回"hello world".

    查看数据库数据信息

    SPRING_SESSION表信息

    SPRING_SESSION_ATTRIBUTES表信息

    2.6 Q&A

       2.6.1 session_id是如何产生的?

      断点调试进入

    此时的cookie和请求的一致。

    进入HttpSessionAdapter的setAttribute属性方法,再调用JdbcOperationsSessionRepository的setAttribute方法

    @Override
            public void setAttribute(String attributeName, Object attributeValue) {
                boolean attributeExists = (this.delegate.getAttribute(attributeName) != null);
                boolean attributeRemoved = (attributeValue == null);
                if (!attributeExists && attributeRemoved) {
                    return;
                }
                if (attributeExists) {
                    if (attributeRemoved) {
                        this.delta.merge(attributeName, DeltaValue.REMOVED, (oldDeltaValue,
                                deltaValue) -> (oldDeltaValue == DeltaValue.ADDED) ? null
                                        : deltaValue);
                    }
                    else {
                        this.delta.merge(attributeName, DeltaValue.UPDATED,
                                (oldDeltaValue,
                                        deltaValue) -> (oldDeltaValue == DeltaValue.ADDED)
                                                ? oldDeltaValue
                                                : deltaValue);
                    }
                }
                else {
                    this.delta.merge(attributeName, DeltaValue.ADDED,
                            (oldDeltaValue, deltaValue) -> (oldDeltaValue == DeltaValue.ADDED)
                                    ? oldDeltaValue
                                    : DeltaValue.UPDATED);
                }
                this.delegate.setAttribute(attributeName, value(attributeValue));
                if (PRINCIPAL_NAME_INDEX_NAME.equals(attributeName) ||
                        SPRING_SECURITY_CONTEXT.equals(attributeName)) {
                    this.changed = true;
                }
            }

    调用内部类JdbcSession的构造方法

            JdbcSession() {
                this.delegate = new MapSession();
                this.isNew = true;
                this.primaryKey = UUID.randomUUID().toString();
            }

    其中primarykey的产生,是随机产生的,而jdbcSession的一个成员变量delegate是MapSession的实例

        /**
         * Creates a new instance with a secure randomly generated identifier.
         */
        public MapSession() {
            this(generateId());
        }

    而generateId()方法,调用了随机uuid的产生

        private static String generateId() {
            return UUID.randomUUID().toString();
        }

    2.6.2 为何和cookie的不一致?

    response中返回的cookie值为:NDM1Mjg1YjAtMmVjMi00MmQ0LWJjY2UtYzFjY2U4NzQzNDNm

    数据库中sessionId为435285b0-2ec2-42d4-bcce-c1cce874343f

    为什么会这样呢?

        @Override
        public List<String> readCookieValues(HttpServletRequest request) {
            Cookie[] cookies = request.getCookies();
            List<String> matchingCookieValues = new ArrayList<>();
            if (cookies != null) {
                for (Cookie cookie : cookies) {
                    if (this.cookieName.equals(cookie.getName())) {
                        String sessionId = (this.useBase64Encoding
                                ? base64Decode(cookie.getValue())
                                : cookie.getValue());
                        if (sessionId == null) {
                            continue;
                        }
                        if (this.jvmRoute != null && sessionId.endsWith(this.jvmRoute)) {
                            sessionId = sessionId.substring(0,
                                    sessionId.length() - this.jvmRoute.length());
                        }
                        matchingCookieValues.add(sessionId);
                    }
                }
            }
            return matchingCookieValues;
        }

    从代码中我们可以知道,中间查了一个base64

    435285b0-2ec2-42d4-bcce-c1cce874343f base64加密后即为:NDM1Mjg1YjAtMmVjMi00MmQ0LWJjY2UtYzFjY2U4NzQzNDNm

    3.总结

       1.浏览器第一次请求的时候,服务端创建一个session,并且将名为SESSION的属性作为响应cookie返回浏览器

       2. 浏览器保存名称为SESSION的cookie作为唯一标识,下次访问服务端时带上此cookie

       3.服务端session中的名为SESSION的属性和浏览器保存的cookie是不一致的,为保密原因,进行了base64加密。

       4.spring-session使用了简单的配置,可以实现session持久化到数据库。

    参考文献:

    【1】https://angelbill3.iteye.com/blog/2342989

  • 相关阅读:
    Linux 如何查看当前目录
    Docker快速入手实战笔记
    【ssh】ssh登录出现‘The authenticity of host ‘IP’ can't be established.’的问题
    【AFL(七)】afl-fuzz.c小改——输出文件夹暂存
    【steam】Steam背景美化——长展柜终极指南
    【AFL(六)】AFL源码中的那些头文件
    【AFL(五)】文件变异策略
    【Latex】详细的简易教程——写在论文开始之前
    【Latex】论文写作工具:VScode 2019 + latex workshop
    【AFL(四)】afl-cmin修改:文件夹相关操作鲁棒性
  • 原文地址:https://www.cnblogs.com/davidwang456/p/10361550.html
Copyright © 2011-2022 走看看