spring实现自定义session、springboot实现自定义session、自定义sessionid的key、value、实现分布式会话

一、原始方案

自定义生成sessionid的值

修改tomcat 的org.apache.catalina.util.HttpServletRequest 包下的生成方法

/**
* Generate and return a new session identifier.
*/
@Override
public String generateSessionId() {
return generateSessionId(jvmRoute);
}

二、使用spring-session框架

maven

<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-core</artifactId>
</dependency> <dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-bom</artifactId>
<version>Corn-SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

自定义生成

import org.springframework.session.MapSession;
import org.springframework.session.MapSessionRepository;
import org.springframework.session.Session; import java.time.Duration;
import java.util.Map; /**
* @Author 绫小路
* @Date 2021/3/10
* @Description 继承 MapSessionRepository 表示将session存储到map中
*/
public class MySessionRepository extends MapSessionRepository {
private Integer defaultMaxInactiveInterval; public MySessionRepository(Map<String, Session> sessions) {
super(sessions);
} public void setDefaultMaxInactiveInterval(int defaultMaxInactiveInterval) {
this.defaultMaxInactiveInterval = defaultMaxInactiveInterval;
} @Override
public MapSession createSession() {
//自定义生成id 解码即可看到 byte[] bytes = new BASE64Decoder().decodeBuffer("MTYxNTM1Nzg0OTI2NQ==");
String id = String.valueOf(System.currentTimeMillis());
MapSession result = new MapSession(id); if (this.defaultMaxInactiveInterval != null) {
result.setMaxInactiveInterval(Duration.ofSeconds(this.defaultMaxInactiveInterval));
}
return result;
}
}

配置

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.session.Session;
import org.springframework.session.SessionRepository;
import org.springframework.session.config.annotation.web.http.EnableSpringHttpSession;
import org.springframework.session.web.http.CookieSerializer;
import org.springframework.session.web.http.DefaultCookieSerializer; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; /**
* @Author 绫小路
* @Date 2021/3/10
*/
@EnableSpringHttpSession
@Configuration
public class MyConfig {
public static Map<String, Session> sessions = new ConcurrentHashMap<>(); @Bean
public SessionRepository mySessionRepository() {
return new MySessionRepository(sessions);
} @Bean
public CookieSerializer cookieSerializer() {
//默认会将cookie进行 Base64 decode value
DefaultCookieSerializer serializer = new DefaultCookieSerializer();
serializer.setCookieName("JSESSIONID");
serializer.setCookiePath("/");
//允许跨域
serializer.setDomainNamePattern("^.+?\\.(\\w+\\.[a-z]+)$"); //cookie 的值不进行base64 编码
serializer.setUseBase64Encoding(false);
return serializer;
}
}

application.properties

server.servlet.session.cookie.name=aa

效果

覆盖 CookieSerializer @Bean

三、通过包装请求会话进行高度自定义

原理是对请求会话进行包装自定义,能够高度支配会话,自由进行自定义开发。例如spring-session原理也是对请求会话进行包装,所以可以通过自定义进行对session的存储,例如存储到内存、redis、数据库、nosql等等。

首选实现HttpSession,并对它进行序列化,其中我添加了自定义id生成

package top.lingkang.testdemo;

import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils; import javax.servlet.ServletContext;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionBindingEvent;
import javax.servlet.http.HttpSessionBindingListener;
import javax.servlet.http.HttpSessionContext;
import java.io.Serializable;
import java.util.*; /**
* @author lingkang
* Created by 2022/1/24
*/
public class MyHttpSession implements HttpSession, Serializable {
private static int nextId = 1;
private String id;
private final long creationTime;
private int maxInactiveInterval;
private long lastAccessedTime;
private final ServletContext servletContext;
private final Map<String, Object> attributes;
private boolean invalid;
private boolean isNew; public MyHttpSession(String id){
this((ServletContext) null);
this.id=id;
} public MyHttpSession() {
this((ServletContext) null);
} public MyHttpSession(@Nullable ServletContext servletContext) {
this(servletContext, (String) null);
} public MyHttpSession(@Nullable ServletContext servletContext, @Nullable String id) {
this.creationTime = System.currentTimeMillis();
this.lastAccessedTime = System.currentTimeMillis();
this.attributes = new LinkedHashMap();
this.invalid = false;
this.isNew = true;
this.servletContext = null;
this.id = id != null ? id : Integer.toString(nextId++);
} public long getCreationTime() {
this.assertIsValid();
return this.creationTime;
} public String getId() {
return this.id;
} public String changeSessionId() {
this.id = Integer.toString(nextId++);
return this.id;
} public void access() {
this.lastAccessedTime = System.currentTimeMillis();
this.isNew = false;
} public long getLastAccessedTime() {
this.assertIsValid();
return this.lastAccessedTime;
} public ServletContext getServletContext() {
return this.servletContext;
} public void setMaxInactiveInterval(int interval) {
this.maxInactiveInterval = interval;
} public int getMaxInactiveInterval() {
return this.maxInactiveInterval;
} public HttpSessionContext getSessionContext() {
throw new UnsupportedOperationException("getSessionContext");
} public Object getAttribute(String name) {
this.assertIsValid();
Assert.notNull(name, "Attribute name must not be null");
return this.attributes.get(name);
} public Object getValue(String name) {
return this.getAttribute(name);
} public Enumeration<String> getAttributeNames() {
this.assertIsValid();
return Collections.enumeration(new LinkedHashSet(this.attributes.keySet()));
} public String[] getValueNames() {
this.assertIsValid();
return StringUtils.toStringArray(this.attributes.keySet());
} public void setAttribute(String name, @Nullable Object value) {
this.assertIsValid();
Assert.notNull(name, "Attribute name must not be null");
if (value != null) {
Object oldValue = this.attributes.put(name, value);
if (value != oldValue) {
if (oldValue instanceof HttpSessionBindingListener) {
((HttpSessionBindingListener) oldValue).valueUnbound(new HttpSessionBindingEvent(this, name, oldValue));
} if (value instanceof HttpSessionBindingListener) {
((HttpSessionBindingListener) value).valueBound(new HttpSessionBindingEvent(this, name, value));
}
}
} else {
this.removeAttribute(name);
} } public void putValue(String name, Object value) {
this.setAttribute(name, value);
} public void removeAttribute(String name) {
this.assertIsValid();
Assert.notNull(name, "Attribute name must not be null");
Object value = this.attributes.remove(name);
if (value instanceof HttpSessionBindingListener) {
((HttpSessionBindingListener) value).valueUnbound(new HttpSessionBindingEvent(this, name, value));
} } public void removeValue(String name) {
this.removeAttribute(name);
} public void clearAttributes() {
Iterator it = this.attributes.entrySet().iterator(); while (it.hasNext()) {
Map.Entry<String, Object> entry = (Map.Entry) it.next();
String name = (String) entry.getKey();
Object value = entry.getValue();
it.remove();
if (value instanceof HttpSessionBindingListener) {
((HttpSessionBindingListener) value).valueUnbound(new HttpSessionBindingEvent(this, name, value));
}
} } public void invalidate() {
this.assertIsValid();
this.invalid = true;
this.clearAttributes();
} public boolean isInvalid() {
return this.invalid;
} private void assertIsValid() {
Assert.state(!this.isInvalid(), "The session has already been invalidated");
} public void setNew(boolean value) {
this.isNew = value;
} public boolean isNew() {
this.assertIsValid();
return this.isNew;
} public Serializable serializeState() {
HashMap<String, Serializable> state = new HashMap();
Iterator it = this.attributes.entrySet().iterator(); while (it.hasNext()) {
Map.Entry<String, Object> entry = (Map.Entry) it.next();
String name = (String) entry.getKey();
Object value = entry.getValue();
it.remove();
if (value instanceof Serializable) {
state.put(name, (Serializable) value);
} else if (value instanceof HttpSessionBindingListener) {
((HttpSessionBindingListener) value).valueUnbound(new HttpSessionBindingEvent(this, name, value));
}
} return state;
} public void deserializeState(Serializable state) {
Assert.isTrue(state instanceof Map, "Serialized state needs to be of type [java.util.Map]");
this.attributes.putAll((Map) state);
}
}

再创建一个请求包装类MyServletRequestWrapper

package top.lingkang.testdemo;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpSession; /**
* @author lingkang
* Created by 2022/1/24
*/
public class MyServletRequestWrapper extends HttpServletRequestWrapper { private HttpSession session; public MyServletRequestWrapper(HttpServletRequest request) {
super(request);
} @Override
public HttpSession getSession() {
return session;
} public void setSession(HttpSession session){
this.session=session;
}
}

最后通过拦截器进行包装类替换,注意,应该经该拦截器放在最前面。

package top.lingkang.testdemo;

import org.springframework.stereotype.Component;

import javax.servlet.*;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID; /**
* @author lingkang
* Created by 2022/1/24
*/
@Component
public class ClusterSessionFilter implements Filter {
private Map<String, MyHttpSession> sessionMap = new HashMap<>(); @Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
MyHttpSession myHttpSession = null;
String cookieName = "custom-cookie-name"; // 获取cookie
String cookieValue = getCookieValue(cookieName, request.getCookies());
if (cookieValue != null) {
myHttpSession = sessionMap.get(cookieValue);
} if (myHttpSession == null) {
// 自定义生成一个唯一id
String id = UUID.randomUUID().toString();
// 生成了id需要添加cookie
HttpServletResponse response = (HttpServletResponse) servletResponse;
Cookie cookie = new Cookie(cookieName, id);
cookie.setPath("/");
response.addCookie(cookie); myHttpSession = new MyHttpSession(id);
} // 包装类
MyServletRequestWrapper myServletRequestWrapper = new MyServletRequestWrapper(request);
myServletRequestWrapper.setSession(myHttpSession); System.out.println(myHttpSession.getId()); filterChain.doFilter(myServletRequestWrapper, servletResponse); // 将会话存储到内存,也可以选择存储到redis等
sessionMap.put(myServletRequestWrapper.getSession().getId(), (MyHttpSession) myServletRequestWrapper.getSession());
} private String getCookieValue(String name, Cookie[] cookies) {
if (cookies == null)
return null;
for (Cookie cookie : cookies) {
if (name.equals(cookie.getName())) {
return cookie.getValue();
}
}
return null;
}
}

需要注意的是,上面的替换方案并没有做session淘汰机制,因为存储在内存中,不做淘汰机制会造成内存溢出

如果将会话存储到redis可以这样,即分布式会话存储方案:

package top.lingkang.testdemo;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component; import javax.servlet.*;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.UUID;
import java.util.concurrent.TimeUnit; /**
* @author lingkang
* Created by 2022/1/24
*/
@Component
public class ClusterSessionFilter implements Filter {
@Autowired
private RedisTemplate redisTemplate; @Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
MyHttpSession myHttpSession = null;
String cookieName = "custom-cookie-name"; // 获取cookie
String cookieValue = getCookieValue(cookieName, request.getCookies());
if (cookieValue != null) {
Object o = redisTemplate.opsForValue().get(cookieValue);
if (o != null) {
myHttpSession = (MyHttpSession) o;
}
} if (myHttpSession == null) {
// 自定义生成一个唯一id
String id = UUID.randomUUID().toString();
// 生成了id需要添加cookie
HttpServletResponse response = (HttpServletResponse) servletResponse;
Cookie cookie = new Cookie(cookieName, id);
cookie.setPath("/");
response.addCookie(cookie); myHttpSession = new MyHttpSession(id);
} // 包装类
MyServletRequestWrapper myServletRequestWrapper = new MyServletRequestWrapper(request);
myServletRequestWrapper.setSession(myHttpSession); filterChain.doFilter(myServletRequestWrapper, servletResponse); // 将会话存储到内存,也可以选择存储到redis等
redisTemplate.opsForValue().set(myHttpSession.getId(), myServletRequestWrapper.getSession(),1800000, TimeUnit.MILLISECONDS);
} private String getCookieValue(String name, Cookie[] cookies) {
if (cookies == null)
return null;
for (Cookie cookie : cookies) {
if (name.equals(cookie.getName())) {
return cookie.getValue();
}
}
return null;
} }

redis的过期机制相当于session淘汰机制,同时又引入了新问题,就是极限情况下的空读问题:get请求要执行3分钟,而session在1分钟后到期,等执行完get再更新会话时发现session被淘汰了。解决方案:获取会话前先预判一下session剩余时间,若session的剩余时间少于5分钟,则直接淘汰这个会话,让用户重新登录。合理的时间分配也很重要,存储在其他地方也要考虑这个极限问题
贴出普通访问:

@RestController
public class WebController {
@Autowired
private HttpServletRequest request; @GetMapping("")
public Object index() {
request.getSession().setAttribute("a", System.currentTimeMillis());
return "create session";
} @GetMapping("get")
public Object get() {
HttpSession session = request.getSession();
return session.getAttribute("a");
}
}

后记:通过上面的会话存储可以做分布式集群了,理论上单体应用集群的扩充上限为redis集群的读写上限,假设redis写并发10w/s,那么你的应用集群并发处理能达10w/s。
若对session进一步优化,除去每次更新最后访问,则为读多写少,理论上集群可以无限扩展。
若使用数据库存储可以使用序列化二进制存储。
基于最后一点原理,我开发了分布式会话框架:
https://gitee.com/lingkang_top/final-session

spring自定义session分布式session的更多相关文章

  1. spring boot 2.x 系列 —— spring boot 实现分布式 session

    文章目录 一.项目结构 二.分布式session的配置 2.1 引入依赖 2.2 Redis配置 2.3 启动类上添加@EnableRedisHttpSession 注解开启 spring-sessi ...

  2. SpringBoot,Security4, redis共享session,分布式SESSION并发控制,同账号只能登录一次

    由于集成了spring session ,redis 共享session,导致SpringSecurity单节点的session并发控制失效, springSession 号称 无缝整合httpses ...

  3. Spring Boot(十一)Redis集成从Docker安装到分布式Session共享

    一.简介 Redis是一个开源的使用ANSI C语言编写.支持网络.可基于内存亦可持久化的日志型.Key-Value数据库,并提供多种语言的API,Redis也是技术领域使用最为广泛的存储中间件,它是 ...

  4. 170222、使用Spring Session和Redis解决分布式Session跨域共享问题

    使用Spring Session和Redis解决分布式Session跨域共享问题 原创 2017-02-27 徐刘根 Java后端技术 前言 对于分布式使用Nginx+Tomcat实现负载均衡,最常用 ...

  5. 基于Spring Boot/Spring Session/Redis的分布式Session共享解决方案

    分布式Web网站一般都会碰到集群session共享问题,之前也做过一些Spring3的项目,当时解决这个问题做过两种方案,一是利用nginx,session交给nginx控制,但是这个需要额外工作较多 ...

  6. Spring Session实现分布式session的简单示例

    前面有用 tomcat-redis-session-manager来实现分布式session管理,但是它有一定的局限性,主要是跟tomcat绑定太紧了,这里改成用Spring Session来管理分布 ...

  7. 使用Spring Session和Redis解决分布式Session跨域共享问题

    http://blog.csdn.net/xlgen157387/article/details/57406162 使用Spring Session和Redis解决分布式Session跨域共享问题

  8. Tornado 自定义session,与一致性哈希 ,基于redis 构建分布式 session框架

    Tornado 自定义session,与一致性哈希 ,基于redis 构建分布式 session import tornado.ioloop import tornado.web from myhas ...

  9. Spring Session解决分布式Session问题的实现原理

    使用Spring Session和Redis解决分布式Session跨域共享问题 上一篇介绍了如何使用spring Session和Redis解决分布式Session跨域共享问题,介绍了一个简单的案例 ...

  10. spring boot:使用redis cluster集群作为分布式session(redis 6.0.5/spring boot 2.3.1)

    一,为什么要使用分布式session? HpptSession默认使用内存来管理Session,如果将应用横向扩展将会出现Session共享问题, 所以我们在创建web集群时,把session保存到r ...

随机推荐

  1. Dubbo3应用开发—Dubbo序列化方案(Kryo、FST、FASTJSON2、ProtoBuf序列化方案的介绍和使用)

    Dubbo序列化方案(Kryo.FST.FASTJSON2.ProtoBuf序列化方案的介绍和使用) 序列化简介 序列化是Dubbo在RPC中非常重要的一个组成部分,其核心作用就是把网络传输中的数据, ...

  2. 解密IP分片与重组:数据传输中的关键技术

    引言 在上一章节中,我们详细讨论了IP的分类和无分类原则的原理以及其在网络通信中的应用.IP分片与重组是在数据包传输过程中起到关键作用的机制.当数据包的大小超过网络链路的MTU(最大传输单元)限制时, ...

  3. IntelliJ IDEA安装中文插件

    1.运行IntelliJ IDEA程序2.点击左上角"File"//文件3.点击下拉的"Settings" //设置4.点击"Plugins" ...

  4. 若依vue分离版(ruoyi-vue)跳过token验证,设置白名单

    找到SecurityConfig类的configure方法 如图所示 在设置白名单后还需要把接口上的权限标识符去掉.然后需要重启一下项目,热加载不行,会报错.

  5. 高性能日志脱敏组件:已支持 log4j2 和 logback 插件

    项目介绍 日志脱敏是常见的安全需求.普通的基于工具类方法的方式,对代码的入侵性太强,编写起来又特别麻烦. sensitive提供基于注解的方式,并且内置了常见的脱敏方式,便于开发. 同时支持 logb ...

  6. 如何在云服务上快速拥有洛甲WAF(Web防火墙)

    如何在云服务上快速拥有洛甲WAF(Web防火墙) 洛甲WAF是基于openresty的web防火墙,通过配合后台保护您的数据安全,详情参考节点服务器 luojiawaf_lua(nginx+lua) ...

  7. Keepalived高可用软件概述

    Keepalived高可用软件概述: 1)互联网主要的高可用软件:Keepalived.Hearttbeat.其中Keepalived是轻量级的,Keepalived是一款开源.免费的实现网站.数据库 ...

  8. 【本博客所有关于git文章迭代汇总】git操作(暂存,回退,绑定远程等),看这一篇就够了

    1.git常用操作 git 小白操作,无非是clone,然后拉取,提交分支,第一次clone的时候,关联远程分支可能会遇到问题,可以看第四条git关联远程分支 # 在当前目录新建一个Git代码库 $ ...

  9. Python 数学函数和 math 模块指南

    Python 提供了一组内置的数学函数,包括一个广泛的数学模块,可以让您对数字执行数学任务. 内置数学函数.min() 和 max() 函数可用于在可迭代对象中查找最低或最高值: 示例:查找可迭代对象 ...

  10. Java 7之基础 - 强引用、弱引用、软引用、虚引用(转)

    载自:http://blog.csdn.net/mazhimazh/article/details/19752475 1.强引用(StrongReference) 强引用是使用最普遍的引用.如果一个对 ...