Session原理
- 浏览器第一次访问服务器,登录成功后服务器会保存一个session对象(可看成map对象),session对象里以键值对的方式保存了用户的登录信息,并且每一个session对象都会对应一个独特的sessionId
- 登录成功后服务器会返回一个JsessionId=xxx的Cookie给浏览器,浏览器下次访问时带上这个cookie,服务器就可以根据sessionid判断用户有没有登录。
session共享问题
-
服务器发回来的cookie默认作用域domain是本服务器域名,例如让认证服务器返回的是auth.gulimall.com,而cookie作用域不同是不能共享的,也就是说auth.gulimall.com的作用域不能作用于gulimall.com。
-
分布式下session共享问题,对于集群里的服务器(服务有多个相同的),有以下几种解决方案:
-
使用springSession解决(底层使用redis)(推荐使用) ,对于不同服务下不用域名的问题,使用自定义cookie的作用域domain解决。
-
session复制,让集群中的服务器互相同步session
优点:web-server(tomcat)原生支持,只需要修改tomcat配置文件即可
缺点:1. session同步需要传输数据,占用集群之间的网络带宽
2.任意一台服务器都需要保存所有服务器的session数据,如果session数据量大的话会导致内存浪费严重,内存限制。
3.客户端存储,把数据存储在浏览器中的cookie里。
优点:1. 服务器不需要保存session,节省服务器资源
缺点:1.不安全,存在泄漏、篡改、窃取等安全问题
2. 浏览器cookie长度有限制,一般为4k,不同浏览器限制不同,如果session数据过大,浏览器会保存不了
SpringSession原理:
首先导入 RedisHttpSessionConfiguration 配置类,这个类在容器中添加了 RedisIndexedSessionRepository 组件, 这个组件就是用来存让装饰后的session的bean,相当于是redis操作session的dao[增删改查],同时它又继承了 SpringHttpSessionConfiguration 配置类, 这个类对包装后的Cookie进行了初始化.当服务启动的时候 这个类会自动注入 SessionRepository [我们自己写的配置文件就实现了这个接口]这个组件又在容器中注册了一个springSessionRepositoryFilter 过滤器 ;这个过滤器将原生的request、response、session包装成 SessionRepositoryRequestWrapper,以后对session的操作都将在redis进行了
源码一步步分析:
- @EnableRedi
- @Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(RedisHttpSessionConfiguration.class)
@Configuration(proxyBeanMethods = false)
public @interface EnableRedisHttpSession
RedisHttpSessionConfiguration extends SpringHttpSessionConfiguration
这个类是用来配置redis作为存储的分布式session配置类,类似的还有RedissonHttpSessionConfiguration用来做分布式锁
@Bean
public RedisIndexedSessionRepository sessionRepository
这个配置类向容器中加入了一个RedisIndexedSessionRepository
,这个组件的作用是存储“装饰后的”RedisSessionRepository,相当于操作redis的session dao接口。
- 同时还继承了SpringHttpSessionConfiguration
- SpringHttpSessionConfiguration会初始化一个cookie序列化器,这为我们配置自定义cookie做了一个铺垫
@PostConstruct
public void init() {
CookieSerializer cookieSerializer = (this.cookieSerializer != null) ? this.cookieSerializer
: createDefaultCookieSerializer();
this.defaultHttpSessionIdResolver.setCookieSerializer(cookieSerializer);
}
- 还向容器中注入了一个SessionRepositoryFilter,相当于注入了一个filter
@Bean
public <S extends Session> SessionRepositoryFilter<? extends Session> springSessionRepositoryFilter(
SessionRepository<S> sessionRepository) {
SessionRepositoryFilter<S> sessionRepositoryFilter = new SessionRepositoryFilter<>(sessionRepository);
sessionRepositoryFilter.setHttpSessionIdResolver(this.httpSessionIdResolver);
return sessionRepositoryFilter;
}
里面有个方法覆盖了父类filter的doFilterInternal
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);
SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper(request, response);
SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryResponseWrapper(wrappedRequest,
response);
try {
filterChain.doFilter(wrappedRequest, wrappedResponse);
}
finally {
wrappedRequest.commitSession();
}
}
而父类作为一个filter在调用dofilter的时候调用了这个doFilterInternal方法
@Override
public final void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
if (!(request instanceof HttpServletRequest) || !(response instanceof HttpServletResponse)) {
throw new ServletException("OncePerRequestFilter just supports HTTP requests");
}
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
String alreadyFilteredAttributeName = this.alreadyFilteredAttributeName;
boolean hasAlreadyFilteredAttribute = request.getAttribute(alreadyFilteredAttributeName) != null;
if (hasAlreadyFilteredAttribute) {
if (DispatcherType.ERROR.equals(request.getDispatcherType())) {
doFilterNestedErrorDispatch(httpRequest, httpResponse, filterChain);
return;
}
// Proceed without invoking this filter...
filterChain.doFilter(request, response);
}
else {
// Do invoke this filter...
request.setAttribute(alreadyFilteredAttributeName, Boolean.TRUE);
try {
doFilterInternal(httpRequest, httpResponse, filterChain);
}
finally {
// Remove the "already filtered" request attribute for this request.
request.removeAttribute(alreadyFilteredAttributeName);
}
}
}
这相当于将request、response等封装为装饰过后的SessionRepositoryRequestWrapper、SessionRepositoryResponseWrapper对象,filter过了之后将包装后的request,response对象传给controller层。
在Controller中想要后的session对象会通过request.getSesison()方法,而这个request对象是包装过后的,之后看看包装后的对象是怎么实现的。
@Override
public HttpSessionWrapper getSession(boolean create) {
HttpSessionWrapper currentSession = getCurrentSession();
if (currentSession != null) {
return currentSession;
}
S requestedSession = getRequestedSession();
if (requestedSession != null) {
if (getAttribute(INVALID_SESSION_ID_ATTR) == null) {
requestedSession.setLastAccessedTime(Instant.now());
this.requestedSessionIdValid = true;
currentSession = new HttpSessionWrapper(requestedSession, getServletContext());
currentSession.markNotNew();
setCurrentSession(currentSession);
return currentSession;
}
}
else {
// This is an invalid session id. No need to ask again if
// request.getSession is invoked for the duration of this request
if (SESSION_LOGGER.isDebugEnabled()) {
SESSION_LOGGER.debug(
"No session found by id: Caching result for getSession(false) for this HttpServletRequest.");
}
setAttribute(INVALID_SESSION_ID_ATTR, "true");
}
if (!create) {
return null;
}
if (SESSION_LOGGER.isDebugEnabled()) {
SESSION_LOGGER.debug(
"A new session was created. To help you troubleshoot where the session was created we provided a StackTrace (this is not an error). You can prevent this from appearing by disabling DEBUG logging for "
+ SESSION_LOGGER_NAME,
new RuntimeException("For debugging purposes only (not an error)"));
}
S session = SessionRepositoryFilter.this.sessionRepository.createSession();
session.setLastAccessedTime(Instant.now());
currentSession = new HttpSessionWrapper(session, getServletContext());
setCurrentSession(currentSession);
return currentSession;
}
注意getRequestedSession方法()获取session对象
private S getRequestedSession() {
if (!this.requestedSessionCached) {
List<String> sessionIds = SessionRepositoryFilter.this.httpSessionIdResolver.resolveSessionIds(this);
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;
}
之后看sessionRepository.findById(),这里通过调用前面注入的RedisIndexedSessionRepository的findById方法获得session
@Override
public RedisSession findById(String id) {
return getSession(id, false);
}
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;
}
之后调用loadSession方法
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;
}
最后返回一个map对象,里面存的数据就是redis保存的session数据。
总结:SpringSession的核心是装饰模式,通过包装一个新的request,response对象去进去操作。
public class SessionRepositoryFilter<S extends Session> extends OncePerRequestFilter
abstract class OncePerRequestFilter implements Filter