分布式环境下,用户登录后的状态信息,一般有两种解决方案:
Token
步骤:
header
传递到各个服务,客户端服务接收到请求时候,解析header
即可2.服务之间的feign
远程调用,通过网关层传递的请求中的header
信息,解析出用户信息的同时,利用fein
将用户信息透传到其他服务
Session
单体服务下,session
可以很好的帮助我们解决用户身份的会话状态问题,因为此刻我们只有一个web
服务器,但是分布式环境下,跨服务器的session
是无法共享单一服务器的session
。对此,除去token
,通常的解决方案大致有以下三种:
tomcat
的tomcat-redis-session-manager
,基于jetty
的jetty-session-redis
session
和cookie
管理),在需要使用会话的时候都从自己的工具类中获取,后台存储可以考虑使用redis
来实现spring session
,基于spring
框架,利用spring-data-redis
连接池,替换servlet
的会话管理,实现脱离容器而不用改变代码spring-session
简而言之,spring Session 提供了 一套API 和实现,用于管理用户的 Session 信息,基于此概念,其具有如下特性:
Session
持久化在外部存储介质中,通过配置自行切换(redis
,mongo
)- 控制
sessionid
如何在客户端和服务器之间进行交换,便于编写Restful API
(从HTTP
头信息中获取sessionid
,而不必再依赖于cookie
)- 非
Web
请求的处理代码中,能够访问session
数据- 支持每个浏览器上使用多个
session
Spring-session
具体提供了如下四个模块:
Spring Session Core、Spring Session Data Redis、Spring Session JDBC 、Spring Session Hazelcast
实现session
管理器的时候,有两个必须要解决的核心问题:
如何(利用外部存储介质)构建集群环境下高可用的session
?
Spring Session
定义了一组标准的接口,可以通过实现这些接口简介访问底层的数据存储
org.springframework.session.Session
定义session
的基本功能:设置、移除接口,(不关心实现具体介质)
org.springframework.session.ExpiringSession
session
接口的扩展、提供判断session
是否过期(典型实现类:RedisSession
)
org.springframework.session.SessionRepository
定义了创建、报错、删除及检索session
的方法,将Session
实例真正保存到数据库存储的逻辑就是这个接口的实现中编码完成的(典型实现类:RedisOperationSessionRepository
)
对于传入的请求该如何确定用哪个session
实例,归根结底,关键问题在于sessionId
如何传递?
Spring Session
定义了一个接口两个默认实现类
HttpSessionStrategy
接口CookieHttpSessionStrategy
实现类(使用cookie
将请求与session id
关联)HeaderHttpSessionStrategy
实现类(使用header
将请求与session id
关联)http
的支持)SessionRepositoryRequestWrapper
SessionRepositoryResponseWrapper
Spring-session
对HTTP
的支持是通过标准的servlet filter
来实现的,这个filter
必须要配置为拦截所有的web
应用请求,并且它应该是filter
链中的第一个filter
。Spring Session filter
会确保随后调用javax.servlet.http.HttpServletRequest
的getSession()
方法时,都会返回Spring Session
的HttpSession
实例,而不是应用服务器默认的HttpSession
官方原话是这样的:
我们没有使用
Tomcat
的HttpSession
,而是将值持久化到Redis
中。
Spring
会话将HttpSession
替换为Redis
支持的实现。当
springsecurity
的SecurityContextPersistenceFilter
将SecurityContext
保存到HttpSession
时,它就会持久化到Redis
中。创建新的
HttpSession
时,Spring Session
会在浏览器中创建一个名为Session
的cookie
。
该cookie
包含会话的ID
。您可以查看cookies
(使用Chrome
或Firefox
)。
也就是说,我们是需要借助spring-security
的过滤器和上下文协助我们完成对session
在redis
存储
一、启动项目
1.SpringHttpSessionConfiguration
注册过滤器,注册session
处理过滤器
@Bean
public <S extends Session> SessionRepositoryFilter<? extends Session> springSessionRepositoryFilter(
SessionRepository<S> sessionRepository) {
SessionRepositoryFilter<S> sessionRepositoryFilter = new SessionRepositoryFilter<>(sessionRepository);
sessionRepositoryFilter.setHttpSessionIdResolver(this.httpSessionIdResolver);
return sessionRepositoryFilter;
}
2.引入RedisHttpSessionConfiguration
注册RedisIndexedSessionRepository
,设置session
的存储配置、机制和模式
@Bean
public RedisIndexedSessionRepository sessionRepository() {
RedisTemplate<Object, Object> redisTemplate = createRedisTemplate();
RedisIndexedSessionRepository sessionRepository = new RedisIndexedSessionRepository(redisTemplate);
sessionRepository.setApplicationEventPublisher(this.applicationEventPublisher);
if (this.indexResolver != null) {
sessionRepository.setIndexResolver(this.indexResolver);
}
if (this.defaultRedisSerializer != null) {
sessionRepository.setDefaultSerializer(this.defaultRedisSerializer);
}
sessionRepository.setDefaultMaxInactiveInterval(this.maxInactiveIntervalInSeconds);
if (StringUtils.hasText(this.redisNamespace)) {
sessionRepository.setRedisKeyNamespace(this.redisNamespace);
}
sessionRepository.setFlushMode(this.flushMode);
sessionRepository.setSaveMode(this.saveMode);
int database = resolveDatabase();
sessionRepository.setDatabase(database);
this.sessionRepositoryCustomizers
.forEach((sessionRepositoryCustomizer) -> sessionRepositoryCustomizer.customize(sessionRepository));
return sessionRepository;
}
二、启动成功后,接受处理URL请求
SessionRepositoryFilter
重写HttpServletRequestWrapper
的getSession()
方法
RedisIndexedSessionRepository
初始化session
@Override
public RedisSession createSession() {
MapSession cached = new MapSession();
if (this.defaultMaxInactiveInterval != null) {
cached.setMaxInactiveInterval(Duration.ofSeconds(this.defaultMaxInactiveInterval));
}
RedisSession session = new RedisSession(cached, true);
session.flushImmediateIfNecessary();
return session;
}
public final class MapSession implements Session, Serializable {
/**
* Default {@link #setMaxInactiveInterval(Duration)} (30 minutes).
*/
public static final int DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS = 1800;
private String id;
private final String originalId;
private Map<String, Object> sessionAttrs = new HashMap<>();
private Instant creationTime = Instant.now();
private Instant lastAccessedTime = this.creationTime;
/**
* Defaults to 30 minutes.
*/
private Duration maxInactiveInterval = Duration.ofSeconds(DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS);
/**
* Creates a new instance with a secure randomly generated identifier.
*/
public MapSession() {
this(generateId());
}
private static String generateId() {
return UUID.randomUUID().toString();
}
}
至此,大致可以知道sessionId
其实就是UUID
的值,有兴趣的可以debug
验证
问题:如果session已经创建,那么分布式服务器如何知道用户已经登录了呢?
再回到SessionRepositoryFilter
的getSession()
方法
关注S requestedSession = getRequestedSession();
方法
逻辑一:requestedSession
为null
直接走创建createSession()
方法,即生成一个uuid
的主键的MapSession
对象
逻辑二:requestedSession
不为null
@Override
public HttpSessionWrapper getSession(boolean create) {
HttpSessionWrapper currentSession = getCurrentSession();
if (currentSession != null) {
return currentSession;
}
S requestedSession = getRequestedSession();
......省略部分代码.....
S session = SessionRepositoryFilter.this.sessionRepository.createSession();
session.setLastAccessedTime(Instant.now());
currentSession = new HttpSessionWrapper(session, getServletContext());
setCurrentSession(currentSession);
return currentSession;
}
我们从S requestedSession = getRequestedSession();
方法开始,一直追踪进去,会发现,实际生效的方法是在DefaultCookieSerializer
里面,也就是说,request
请求所在的客户端,是存在用户登录的sessionId
信息的,请求过来的时候,浏览器会去请求携带的cookie
中获取,然后进行Base64
的解析!
private S getRequestedSession() {
if (!this.requestedSessionCached) {
##########注意啦,这里是服务器解析请求中的cookie信息,解析session啦#############
List<String> sessionIds = SessionRepositoryFilter.this.httpSessionIdResolver.resolveSessionIds(this);
#####拿到请求sessionId之后,利用sessionRepository[redis]来对比对,匹配中一个,说明在线#######
for (String sessionId : sessionIds) {
if (this.requestedSessionId == null) {
this.requestedSessionId = sessionId;
}
S session = SessionRepositoryFilter.this.sessionRepository.findById(sessionId);
if (session != null) {
this.requestedSession = session;
this.requestedSessionId = sessionId;
break;
}
}
this.requestedSessionCached = true;
}
return this.requestedSession;
}
DefaultCookieSerializer
解析request
的Session
信息
@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;
}
解析出request
的但是这还不够,这只能判断,客户端存有用户登录的信息key
,不能保证在服务器端也是登录的状态,那么这就需要做一个对比:request
携带的session
信息----> 分布式服务redis
中存储的session
集合做比对,肯定这个项目,不仅仅只有你一个人登录,对吧,别人也可以登录,这样,redis
里面存在很多的用户登录信息,但是,我们只需要找出其中一个,能够与request
匹配的session
就可以证明,这个请求的request
客户端,已经登录!比对逻辑,也是在源码中,我们继续看上面代码的注释,可以找到逻辑证明!
RedisIndexedSessionRepository
中有对SessionRepositoryFilter.this.sessionRepository.findById(sessionId)
做具体的实现,关注类中的以下2个方法:
private RedisSession getSession(String id, boolean allowExpired) {
Map<Object, Object> entries = getSessionBoundHashOperations(id).entries();
if (entries.isEmpty()) {
return null;
}
MapSession loaded = loadSession(id, entries);
if (!allowExpired && loaded.isExpired()) {
return null;
}
RedisSession result = new RedisSession(loaded, false);
result.originalLastAccessTime = loaded.getLastAccessedTime();
return result;
}
private MapSession loadSession(String id, Map<Object, Object> entries) {
MapSession loaded = new MapSession(id);
for (Map.Entry<Object, Object> entry : entries.entrySet()) {
String key = (String) entry.getKey();
if (RedisSessionMapper.CREATION_TIME_KEY.equals(key)) {
loaded.setCreationTime(Instant.ofEpochMilli((long) entry.getValue()));
}
else if (RedisSessionMapper.MAX_INACTIVE_INTERVAL_KEY.equals(key)) {
loaded.setMaxInactiveInterval(Duration.ofSeconds((int) entry.getValue()));
}
else if (RedisSessionMapper.LAST_ACCESSED_TIME_KEY.equals(key)) {
loaded.setLastAccessedTime(Instant.ofEpochMilli((long) entry.getValue()));
}
else if (key.startsWith(RedisSessionMapper.ATTRIBUTE_PREFIX)) {
loaded.setAttribute(key.substring(RedisSessionMapper.ATTRIBUTE_PREFIX.length()), entry.getValue());
}
}
return loaded;
}
Session
生命周期session
RedisSession
在创建时设置3个变量createTime、maxInactiveInterval、lastAccessedTime
Session
应该通过getSession(boolean create)
方法来获取session
数据【create
标识session
不存在时是否创建session
】,具体步骤如下:
getSession
方法首先请求的.CURRENT_SESSION
属性来获取currentSession
request
取出sessionId
【该步骤依赖具体HttpSessionStrategy
】spring:session:sessions:[sessionId]
的值,同时根据lastAccessedTime
和MaxInactiveIntervalInSeconds
来判断这个session
是否过期request
中没有sessionId
,说明用户是第一次访问,会根据不同的实现来创建一个新的session
session
spring session
在访问有效期内,每次访问都会更新lastAccessedTime
的值,过期时间为lastAccessedTime
+maxInactiveInterval
,也就是在有效期内每访问一次,有效期就向后延长maxInactiveInterval
,对于过期数据,一般有如下三种删除策略
session
的数据结构spring-session
在redis
的数据结构
Set
类型spring:session:expireations:
表示经过的分钟数
Set
集合的member
为expires:[sessionId]
,表示members
会在min
分钟后延期
String
类型的spring:session:sessions:expires:[sessionId]
:
该数据的ttl
标识剩余的过期时间,即maxInactiveInterval
Hash
类型的spring:session:sessions:[sessionId]
session
保存的数据,记录了createTime
,maxInactiveInterval
,lastAccessedTime
,attribute
前面两个数据是用于session
过期管理的辅助数据结构
spring-session
要求redis
版本在2.8以上session
存储在redis
的key
是spring:session:
,但如果多个系统使用一个redis
,会有冲突,此时需要配置redisNamespace
的值session
中保存一个对象,必须实现Serializable
接口session
的域不同会生成新的session
的,需进行网关代理、负载均衡、或者自定义HttpSessionStrategy
策略并进行跨域处理
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
<dependency>
<groupId>org.springframework.securitygroupId>
<artifactId>spring-security-webartifactId>
<version>5.3.6.RELEASEversion>
dependency>
<dependency>
<groupId>org.springframework.sessiongroupId>
<artifactId>spring-session-data-redisartifactId>
<version>2.4.0version>
dependency>
<dependency>
<groupId>org.springframework.sessiongroupId>
<artifactId>spring-session-coreartifactId>
<version>2.4.0version>
dependency>
@Slf4j
// redisNamespace不同服务设置不同的namcespace
//maxInactiveIntervalInSeconds-默认失效时间1800->30分钟,cleanupCron->定期清理会话cron表达式
@EnableRedisHttpSession(redisNamespace = "spring:session:client", maxInactiveIntervalInSeconds = 1800)
@Configuration
public class RedisHttpSessionConfig {
/**
* 更换序列化器
*/
@Bean("springSessionDefaultRedisSerializer")
public RedisSerializer setSerializer(){
return new GenericJackson2JsonRedisSerializer();
}
@Bean
public CookieSerializer cookieSerializer() {
DefaultCookieSerializer serializer = new DefaultCookieSerializer();
//serializer.setCookieName("SESSIONID");
serializer.setCookiePath("/");
//serializer.setDomainNamePattern("^.+?\\.(\\w+\\.[a-z]+)$");
return serializer;
}
@Bean
public SpringHttpSessionConfiguration springHttpSessionConfiguration() {
SpringHttpSessionConfiguration ss = new SpringHttpSessionConfiguration();
ss.setCookieSerializer(cookieSerializer());
log.info("加载自定义SpringHttpSessionConfiguration");
return ss;
}
}
yaml
配置文件spring:
session:
store-type=redis: redis
redis:
host: 47.105.158.78
port: 6399
timeout: 20s
# 数据库索引
database: 0
jedis:
pool:
#连接池最大连接数(使用负值表示没有限制)
max-active: 300
#连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: -1s
#连接池中的最大空闲连接
max-idle: 100
#连接池中的最小空闲连接
min-idle: 20
//创建session
@GetMapping("createSession")
public JsonResult<Object> createSession(HttpSession session, String key, String value) {
log.info("创建session:key:value:{}:{}", key, value);
session.setAttribute(key, value);
return JsonResult.ok(true);
}
// 获取session
@GetMapping("getSession")
public JsonResult<Object> getSession(HttpServletRequest request, String key) {
HttpSession session = request.getSession();
Object attribute = session.getAttribute(key);
Map<String, Object> resultMap = Maps.newHashMapWithExpectedSize(3);
resultMap.put("sessionId", session.getId());
resultMap.put("maxInactiveInterval", session.getMaxInactiveInterval());
String lastAccessedTime = LocalDateUtils.dateConvertToString(new Date(session.getLastAccessedTime()), LocalDateUtils.DATE_TIME_FORMAT);
resultMap.put("lastAccessedTime", lastAccessedTime);
String createTime = LocalDateUtils.dateConvertToString(new Date(session.getCreationTime()), LocalDateUtils.DATE_TIME_FORMAT);
resultMap.put("createdTime", createTime);
//这个key的过期时间为 Session 的最大过期时间 +5 分钟。
resultMap.put("attribute", attribute);
return JsonResult.ok(resultMap);
}