发现问题
目前的项目使用spring-session + redis 实现的session共享,在开发PC端的时候发现,两个应用之间访问请求头会出现两个session,自然会被踢出登录。
为什么?
session即是cookie 应该是唯一的才对,为什么会出现两个同名session,一定path或者domain不同才会这样,仔细一看果然,发现path不同,应用a的path为/a 应用b的path为/b
到底为什么?
开始扒源码,看看spring是怎么创建这个session的。之前已经看过spring-session的源码了解了实现原理,知道了我们配置的过滤其实就是org.springframework.session.web.http.SessionRepositoryFilter
先看下OncePerRequestFilter1
2
3
4
5
6
7
8
9
10
11abstract class OncePerRequestFilter implements Filter{
public final void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain){
...
doFilterInternal(httpRequest, httpResponse, filterChain);
...
}
protected abstract void doFilterInternal(
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException;
}
SessionRepositoryFilter中的关键代码1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58public class SessionRepositoryFilter<S extends ExpiringSession> extends OncePerRequestFilter {
...
private MultiHttpSessionStrategy httpSessionStrategy = new CookieHttpSessionStrategy();
...
//doFilterInternal是OncePerRequestFilter中定义的抽象方法,在doFilter中调用
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
request.setAttribute(SESSION_REPOSITORY_ATTR, sessionRepository);
//包装Request 和 Response
SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper(request, response, servletContext);
SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryResponseWrapper(wrappedRequest,response);
HttpServletRequest strategyRequest = httpSessionStrategy.wrapRequest(wrappedRequest, wrappedResponse);
HttpServletResponse strategyResponse = httpSessionStrategy.wrapResponse(wrappedRequest, wrappedResponse);
try {
filterChain.doFilter(strategyRequest, strategyResponse);
} finally {
wrappedRequest.commitSession();
}
}
...
//Request的包装类
private final class SessionRepositoryRequestWrapper extends HttpServletRequestWrapper {
...
public HttpSession getSession(boolean create) {
HttpSessionWrapper currentSession = getCurrentSession();
if(currentSession != null) {
return currentSession;
}
//获取sessionid
String requestedSessionId = getRequestedSessionId();
if(requestedSessionId != null) {
S session = sessionRepository.getSession(requestedSessionId);
if(session != null) {
this.requestedSessionIdValid = true;
currentSession = new HttpSessionWrapper(session, getServletContext());
currentSession.setNew(false);
setCurrentSession(currentSession);
return currentSession;
}
}
if(!create) {
return null;
}
S session = sessionRepository.createSession();
currentSession = new HttpSessionWrapper(session, getServletContext());
setCurrentSession(currentSession);
return currentSession;
}
//获取sessionid
public String getRequestedSessionId() {
//这里发现是通过CookieHttpSessionStrategy获取的sessionid
return httpSessionStrategy.getRequestedSessionId(this);
}
...
}
}
SessionRepositoryFilter中doFilterInternal方法包装request和response,通过包装的request获取包装的session,
sessionid默认通过CookieHttpSessionStrategy的getRequestedSessionId方法获取
CookieHttpSessionStrategy1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84public final class CookieHttpSessionStrategy implements MultiHttpSessionStrategy, HttpSessionManager {
private static final String SESSION_IDS_WRITTEN_ATTR = CookieHttpSessionStrategy.class.getName().concat(".SESSIONS_WRITTEN_ATTR");
static final String DEFAULT_ALIAS = "0";
static final String DEFAULT_SESSION_ALIAS_PARAM_NAME = "_s";
private Pattern ALIAS_PATTERN = Pattern.compile("^[\\w-]{1,50}$");
private String cookieName = "SESSION";
private String sessionParam = DEFAULT_SESSION_ALIAS_PARAM_NAME;
...
public String getRequestedSessionId(HttpServletRequest request) {
Map<String,String> sessionIds = getSessionIds(request);
String sessionAlias = getCurrentSessionAlias(request);
return sessionIds.get(sessionAlias);
}
...
public Map<String,String> getSessionIds(HttpServletRequest request) {
//这里看到了名为SESSION的cookie 。在看如何创建的cookie
Cookie session = getCookie(request, cookieName);
String sessionCookieValue = session == null ? "" : session.getValue();
Map<String,String> result = new LinkedHashMap<String,String>();
StringTokenizer tokens = new StringTokenizer(sessionCookieValue, " ");
if(tokens.countTokens() == 1) {
result.put(DEFAULT_ALIAS, tokens.nextToken());
return result;
}
while(tokens.hasMoreTokens()) {
String alias = tokens.nextToken();
if(!tokens.hasMoreTokens()) {
break;
}
String id = tokens.nextToken();
result.put(alias, id);
}
return result;
}
private Cookie createSessionCookie(HttpServletRequest request,
Map<String, String> sessionIds) {
//创建名SESSION的cookie
Cookie sessionCookie = new Cookie(cookieName,"");
if(isServlet3Plus) {
sessionCookie.setHttpOnly(true);
}
sessionCookie.setSecure(request.isSecure());
//设置path 之前发现cookiePath不同,那么这就是根源代码了。
sessionCookie.setPath(cookiePath(request));
// 这个TODO 是spring-session框架的作者留下的。。。。要不要设置domain?感觉需要
// TODO set domain?
if(sessionIds.isEmpty()) {
sessionCookie.setMaxAge(0);
return sessionCookie;
}
if(sessionIds.size() == 1) {
String cookieValue = sessionIds.values().iterator().next();
sessionCookie.setValue(cookieValue);
return sessionCookie;
}
StringBuffer buffer = new StringBuffer();
for(Map.Entry<String,String> entry : sessionIds.entrySet()) {
String alias = entry.getKey();
String id = entry.getValue();
buffer.append(alias);
buffer.append(" ");
buffer.append(id);
buffer.append(" ");
}
buffer.deleteCharAt(buffer.length()-1);
sessionCookie.setValue(buffer.toString());
return sessionCookie;
}
//这里的path实现是 子域+"/"
private static String cookiePath(HttpServletRequest request) {
return request.getContextPath() + "/";
}
}
CookieHttpSessionStrategy 负责创建名为SESSION的cookie。path:为子域+”/“,domain:默认
找到了问题根源
spring-session在多应用之间默认不能做session共享因为cookie path不同。
如何解决?
在看SessionRepositoryFilter ,SessionRepositoryFilter提供了setHttpSessionStrategy方法1
2
3
4
5
6
7
8
9
10public class SessionRepositoryFilter<S extends ExpiringSession> extends OncePerRequestFilter {
...
public void setHttpSessionStrategy(HttpSessionStrategy httpSessionStrategy) {
if(httpSessionStrategy == null) {
throw new IllegalArgumentException("httpSessionStrategy cannot be null");
}
this.httpSessionStrategy = new MultiHttpSessionStrategyAdapter(httpSessionStrategy);
}
...
}
既然提供了set方法,那么就可以替换这个默认的cookie实现类
在看RedisHttpSessionConfiguration 这个配置类1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class RedisHttpSessionConfiguration implements ImportAware, BeanClassLoaderAware {
...
public <S extends ExpiringSession> SessionRepositoryFilter<? extends ExpiringSession> springSessionRepositoryFilter(SessionRepository<S> sessionRepository, ServletContext servletContext) {
SessionRepositoryFilter<S> sessionRepositoryFilter = new SessionRepositoryFilter<S>(sessionRepository);
sessionRepositoryFilter.setServletContext(servletContext);
if(httpSessionStrategy != null) {
sessionRepositoryFilter.setHttpSessionStrategy(httpSessionStrategy);
}
return sessionRepositoryFilter;
}
...
false) (required =
public void setHttpSessionStrategy(HttpSessionStrategy httpSessionStrategy) {
this.httpSessionStrategy = httpSessionStrategy;
}
...
}
spring 在创建SessionRepositoryFilter类的时候,会判断httpSessionStrategy是否为空,如果不为空便调用setHttpSessionStrategy设置。
这样我们便可以使用自定义的实现类
实现
public final class CookieHttpSessionStrategy implements MultiHttpSessionStrategy, HttpSessionManager
CookieHttpSessionStrategy 是final的无法继承。
自定义一个CustomCookieHttpSessionStrategy 其他代码不变,将cookiePath方法稍微改一下直接返回”/“1
2
3
4//path 直接返回/
private static String cookiePath(HttpServletRequest request) {
return "/";
}
配置文件1
2
3
4
5<bean class="com.sf.portal.pcam.framework.CustomCookieHttpSessionStrategy" id="customCookieHttpSessionStrategy"></bean>
<bean class="org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration">
<property name="maxInactiveIntervalInSeconds" value="1800"></property>
<property name="httpSessionStrategy" ref="customCookieHttpSessionStrategy"></property>
</bean>