springboot中整合使用spring-session

springboot中整合使用spring-session

spring-session

它可以替代 HttpSesession。而且改动极小,对应用透明。底层可以使用内存,Redis等存储Session信息。通过Redis这种方式可以做到Session共享,在集群环境中所有节点共享Session。

文档
https://docs.spring.io/spring-session/docs/current/reference/html5/

SpringBoot整合

使用 spring-session-data-redis,一定要先整合redis到项目。

依赖

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

配置

配置类:RedisSessionProperties

spring:
  session:
    timeout: 1800 # session的过期时间,单位是秒
    store-type: REDIS # session的存储类型,枚举
    redis:
      namespace: "spring:session" # session存储在redis中的命名空间
      flush-mode: IMMEDIATE # 刷出模式,枚举:ON_SAVE ,IMMEDIATE
      cleanup-cron: "0 * * * * *" # 定时清理过期session任务的`cron`表达式

关于 spring.session.redis.flush-mode

ON_SAVE

只有当 SessionRepository.save(Session) 方法被调用时, 才会将session中的数据同步到redis中.
在web 应用中, 当请求完成响应后, 才开始同步. 也就是说在执行response 之前session数据都是缓存在本地的。

IMMEDIATE

当执行SessionRepository.createSession()时, 会将session数据同步到redis中;
当对session的attribute进行set/remove 等操作时, 也会同步session中的数据到redis中。它是实时同步的

使用

使用并需要修改什么,像平时一样。获取到 Servlet的Session就是了。

controller

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

@RequestMapping("/test")
@Controller
public class TestController {
	
	@GetMapping("/session")
	public ModelAndView session(HttpServletRequest request) {
		
		HttpSession httpSession = request.getSession();
		
		// 是否新创建的 true
		System.out.println(httpSession.isNew());
		
		// 实现类 org.springframework.session.web.http.SessionRepositoryFilter$SessionRepositoryRequestWrapper$HttpSessionWrapper
		System.out.println(httpSession.getClass().getName());
		
		// 写入数据到session(底层使用redis存储)
		httpSession.setAttribute("name", "SpringBoot中文社区");
		
		return new ModelAndView("test/test");
	}
}

View

<!DOCTYPE html>
<html>
	<head>
		<meta charset="UTF-8">
		<title>Test</title>
	</head>
	<body>
		Hello ${name}	
	</body>
</html>

image

Cookie属性的修改

默认情况下,客户端使用Cookie来存储会话id

查看响应信息,可以看到设置了session的cookie

Connection: keep-alive
Content-Encoding: gzip
Content-Language: zh-CN
Content-Type: text/html;charset=UTF-8
Date: Thu, 17 Oct 2019 08:57:07 GMT
Server: nginx
Set-Cookie: PHPSESSIONID=Y2YwMDM1YjctMjBiYy00OWRiLWI5NGItZjFmNDU4ZDcxNThm; Max-Age=36000; Expires=Thu, 17 Oct 2019 18:57:07 GMT; Path=/; HttpOnly; SameSite=Lax
Transfer-Encoding: chunked

可以通过配置修改cookie的信息。

server:
  servlet:
	session:
	  cookie:
	    name: PHPSESSIONID #cookie名称
	    domain:  # 域
	    path:  # 路径
	    comment: # 备注
	    httpOnly: # 是否仅用于http传输
	    secure: # 是否仅在SSL的情况下传输
	    maxAge: # 生命周期

也可以通过配置修改cookie信息

自定义 CookieSerializer 到IOC。

@Bean
public CookieSerializer cookieSerializer() {
	DefaultCookieSerializer serializer = new DefaultCookieSerializer();
	serializer.setCookieName("JSESSIONID"); 
	serializer.setCookiePath("/"); 
	serializer.setDomainNamePattern("^.+?\\.(\\w+\\.[a-z]+)$"); 
	return serializer;
}

Session的索引

通俗的理解就是,可以建立key和session的索引,根据某些key获取到session。

FindByIndexNameSessionRepository

需求:根据用户id,获取到它的会话

用户登录成功后,把它的id以固定的key存入到Session

value仅仅接受字符串

@Autowired
FindByIndexNameSessionRepository<? extends Session> sessions;
.... 代码省略
Integer userId = user.getId();
// 往session中存入用户的id信息
request.getSession().setAttribute(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, userId + "");

根据用户id检索它的session信息

@Autowired
FindByIndexNameSessionRepository<? extends Session> sessions;
.... 代码省略
Integer userId = user.getId();
// 返回该用户的所有的有效session
Map<String, ? extends Session> sessions = this.sessions.findByPrincipalName(userId + "");
for (Session session : sessions.values()) {
	// 根据sessionId删除
	this.sessions.deleteById(session.getId());
}

FindByIndexNameSessionRepository 接口源码

public interface FindByIndexNameSessionRepository<S extends Session> extends SessionRepository<S> {

	String PRINCIPAL_NAME_INDEX_NAME =FindByIndexNameSessionRepository.class.getName()
			.concat(".PRINCIPAL_NAME_INDEX_NAME");

	Map<String, S> findByIndexNameAndIndexValue(String indexName, String indexValue);
	
	default Map<String, S> findByPrincipalName(String principalName) {

		return findByIndexNameAndIndexValue(PRINCIPAL_NAME_INDEX_NAME, principalName);

	}
}

可以通过findByIndexNameAndIndexValue方法自己建立key和session的索引信息。

事件的监听

session过期,销毁事件依赖于redis的key过期通知。事件对象通过spring的事件订阅发布机制来发布

事件对象

SessionCreatedEvent				创建
SessionDestroyedEvent
	|-SessionExpiredEvent		过期
	|-SessionDeletedEvent		删除(用户主动 invalidate())

监听器的实现

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.session.events.SessionCreatedEvent;
import org.springframework.session.events.SessionDeletedEvent;
import org.springframework.session.events.SessionExpiredEvent;
import org.springframework.stereotype.Component;

@Component
public class SpringSessionListener {
	
	private static final Logger LOGGER = LoggerFactory.getLogger(SpringSessionListener.class);
	
	@EventListener(SessionCreatedEvent.class)
	@Async
	public void sessionCreatedEvent(SessionCreatedEvent sessionCreatedEvent) {
		if (LOGGER.isDebugEnabled()) {
			LOGGER.debug("session 创建:{}", sessionCreatedEvent.getSessionId());
		}
	}
	
	@EventListener(SessionExpiredEvent.class)
	public void sessionExpiredEvent(SessionExpiredEvent sessionCreatedEvent) {
		if (LOGGER.isDebugEnabled()) {
			LOGGER.debug("session 到期:{}", sessionCreatedEvent.getSessionId());
		}
	}
	
	@EventListener(SessionDeletedEvent.class)
	public void sessionDeletedEvent(SessionDeletedEvent sessionCreatedEvent) {
		if (LOGGER.isDebugEnabled()) {
			LOGGER.debug("session 删除:{}", sessionCreatedEvent.getSessionId());
		}
	}
}

使用Servlet的监听器

比较麻烦,需要自己通过代码配置添加

  • 需要添加SessionEventHttpSessionListenerAdapter到ioc, 通过这个bean的构造函数, 添加多个 HttpSessionListener 实现
    SessionEventHttpSessionListenerAdapter
    SessionEventHttpSessionListenerAdapter(List<HttpSessionListener> listeners)

  • 但是这个Bean其实框架已经自动添加了, 再次添加会导致异常

  • 曲线救国, 从IOC里面读取到这个bean, 通过反射, 对私有属性 listeners 添加监听器

@Configuration
public class SpringSessionConfiguration {
	
	private static final Logger LOGGER = LoggerFactory.getLogger(SpringSessionConfiguration.class);
	
	@Autowired SessionEventHttpSessionListenerAdapter sessionEventHttpSessionListenerAdapter;
	
	@PostConstruct
	public void addHttpSessionListener() {
		try {
			Field field = SessionEventHttpSessionListenerAdapter.class.getDeclaredField("listeners");
			field.setAccessible(Boolean.TRUE);
			
			@SuppressWarnings("unchecked")
			List<HttpSessionListener> listeners = (List<HttpSessionListener>) field.get(sessionEventHttpSessionListenerAdapter);
			listeners.add(new SessionListener());
			
			if (LOGGER.isDebugEnabled()) {
				LOGGER.debug("添加SESSION监听器");
			}
			
		} catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) {
			e.printStackTrace();
		}
	}
	
	
//	@Bean //BeanDefinitionOverrideException
//	public SessionEventHttpSessionListenerAdapter sessionEventHttpSessionListenerAdapter() {
//		SessionEventHttpSessionListenerAdapter sessionEventHttpSessionListenerAdapter = new SessionEventHttpSessionListenerAdapter(Arrays.asList(new SessionListener()));
//		return sessionEventHttpSessionListenerAdapter;
//	}
}

使用Http Header来解析session id

默认客户端使用Cookie来存储session id。但是对于一些客户端来说,cookie不一定方便,可以通过 http header来携带cookie的id。

Session的解析依赖于一个接口: HttpSessionIdResolver

实现类
CookieHttpSessionIdResolver 使用Cookie(默认)
HeaderHttpSessionIdResolver 使用Header

配置通过 HttpHeader 来解析session id

@Bean
public HttpSessionIdResolver httpSessionIdResolver() {
	return HeaderHttpSessionIdResolver.xAuthToken();  // 使用 X-Auth-Token 解析Cookie
}

HeaderHttpSessionIdResolver 还支持自定义header的名称,代码及简单,可以自己阅读学习。

spring-session在redis中数据的存储结构

spring:session:expirations:1570672200000
spring:session:index:org.springframework.session.FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME:1
spring:session:sessions:d82bf2bb-deb3-474c-89bb-96c2bfa646c4
spring:session:sessions:expires:94b2ce1f-053e-4c20-a8b7-f4d69102a114

TODO