前段时间, 写了篇 < 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+8&useSSL=false
- spring.datasource.username=root
- spring.datasource.password=wangwei456
- spring.datasource.driver-class-name=com.MySQL.cj.jdbc.Driver
注意, 新版 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
来源: https://www.cnblogs.com/davidwang456/p/10361550.html