spring session 关键组件的UML图
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
//在request的属性中放入它的“session仓库”
request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);
//将httprequest包装一下
SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper(
request, response, this.servletContext);
//将response添加多session的相关信息
SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryResponseWrapper(
wrappedRequest, response);
//将这个处理器本身放入Request的属性中
HttpServletRequest strategyRequest = this.httpSessionStrategy
.wrapRequest(wrappedRequest, wrappedResponse);
//包装成多session的Response
HttpServletResponse strategyResponse = this.httpSessionStrategy
.wrapResponse(wrappedRequest, wrappedResponse);
//执行过滤器链,这样后续的所有web组件得到的都是被包装过的Request和response了
try {
filterChain.doFilter(strategyRequest, strategyResponse);
}
//无论直接结果如何,都将我们的mock的session放入Response,并将此session存入我们的SessionRepository.
finally {
wrappedRequest.commitSession();
}
}
CookieHttpSessionStrategy
其主要作用就是利用Cookie去实现spring“自制session”的相关方法。
默认的CookieSerializer,目前就只有一个默认实现
public class DefaultCookieSerializer implements CookieSerializer {
//默认spring-session往Cookie中写入的代表写session的ID名,作用类似web容器默认的“jsessionId”
private String cookieName = "SESSION";
private Boolean useSecureCookie;
private boolean useHttpOnlyCookie = isServlet3();
private String cookiePath;
private int cookieMaxAge = -1;
private String domainName;
private Pattern domainNamePattern;
//从set方法可知,这个值的作用是,如果你想追踪session值是在哪个jvm上写入的(比如日志追踪),可以设置它,一般没什么用。
private String jvmRoute;
private boolean useBase64Encoding;
private String rememberMeRequestAttribute;
//将request里面的所有Cookie取出,筛选出“SESSION开头的spring session需要的Cookie值,根据需要切掉JvmRoute和解码,返回”
public List readCookieValues(HttpServletRequest request) {
Cookie[] cookies = request.getCookies();
List 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;
}
public void writeCookieValue(CookieValue cookieValue) {
HttpServletRequest request = cookieValue.getRequest();
HttpServletResponse response = cookieValue.getResponse();
String requestedCookieValue = cookieValue.getCookieValue();
//取出要设置的放入session中的值,如果设置了jvmRoute,在末尾拼接上。
String actualCookieValue = this.jvmRoute == null ? requestedCookieValue
: requestedCookieValue + this.jvmRoute;
//创建我们要代替jsessionId的cookie,如果设置useBase64Encoding了,还会将SESSION键的值进行base64编码。
Cookie sessionCookie = new Cookie(this.cookieName, this.useBase64Encoding
? base64Encode(actualCookieValue) : actualCookieValue);
//根据request的设置设置cookie是否必须使用https协议
sessionCookie.setSecure(isSecureCookie(request));
//设置cookie的path
sessionCookie.setPath(getCookiePath(request));
//根据request的domain设置cookie
String domainName = getDomainName(request);
if (domainName != null) {
sessionCookie.setDomain(domainName);
}
//设置是否只供http协议
if (this.useHttpOnlyCookie) {
sessionCookie.setHttpOnly(true);
}
//如果要写的值是空的,Cookie不保存
if ("".equals(requestedCookieValue)) {
sessionCookie.setMaxAge(0);
}
//有spring security 实现记住我的功能需要的属性键值对时,cookie永久保存
else if (this.rememberMeRequestAttribute != null
&& request.getAttribute(this.rememberMeRequestAttribute) != null) {
// the cookie is only written at time of session creation, so we rely on
// session expiration rather than cookie expiration if remember me is enabled
sessionCookie.setMaxAge(Integer.MAX_VALUE);
}
else {
sessionCookie.setMaxAge(this.cookieMaxAge);
}
//包含“SESSION=XXXX”的Cookie值放入Response最后~
response.addCookie(sessionCookie);
}
……忽略其他辅助方法
}
所以DefaultCookieSerializer的主要作用就是操作Cookie的读写,应为是并不是依赖原来的jsessionId所以,特别写了这个CookieSerializer。
CookieHttpSessionStrategy
这个实现了HttpSessionStrategy和RequestResponsePostProcessor2个接口的功能。
- HttpSessionStrategy的3个接口
- getRequestedSessionId 获取sessionId 既获取spring session设置在Cookie中的键SESSION的值,既spring session的ID。这里的读取利用了CookieSerializer工具类的读取方法。
- onNewSession 在session被创建时被调用,作用???
- onInvalidateSession 想要删除session时调用的方法
- HttpSessionManager
- getCurrentSessionAlias
就是取得当前session的别名,如果你没有用多session,默认就是0。
如果你的url里面传入了查询参数_s=数字,就会返回这个数字别名。
比如"mockUrl?_s=2"那么,返回的就是2。 - getSessionIds
找到SESSION里面所有的session别名作为key,sessionId作为value返回。 - encodeURL
??????将url和别名拼接在一起 - getNewSessionAlias
返回一个新的session别名,阅读源码可以知道其实就是在原来的别名基础上+1,比如你已经在SESSSION 键里面有 0 12312012-2321-23,别名就是0了,现在新增一个别名,就是返回1.
- getCurrentSessionAlias
同一个浏览器,支持多个session
SessionRepositoryFilter
SessionRepositoryRequestWrapper
这个类就是用来代替容器自身HttpServletRequest的实现。这里有2个方法是重点。
- getSession 获取session的方法,显然这个方法会被重写。
@Override
public HttpSessionWrapper getSession(boolean create) {
//getCurrentSession的逻辑就是去request里面查找有没有Session相关的键值对:SessionRepository.CURRENT_SESSION=HttpSessionWrapper。
//有的话说明此Request已经有session了返回即可。
HttpSessionWrapper currentSession = getCurrentSession();
if (currentSession != null) {
return currentSession;
}
//通过HttpSessionStrategy提供的功能去查找当前Request是否有COOKIE值SESSION=XXXX,得到sessionId。
String requestedSessionId = getRequestedSessionId();
//如果有,并且request中没有SessionRepository.invalidSessionId这个键,则通过sessionRepository去查出存储的session,
//将spring的session包装成HttpSessionWrapper,放入Request中。
//既键值对:SessionRepository.CURRENT_SESSION=HttpSessionWrapper
//这里如果没能通过此sessionId从SessionRepository中找出对应的session,则设置SessionRepository.invalidSessionId为true,下次同样的sessionId就别找了。
if (requestedSessionId != null
&& getAttribute(INVALID_SESSION_ID_ATTR) == null) {
S session = getSession(requestedSessionId);
if (session != null) {
this.requestedSessionIdValid = true;
currentSession = new HttpSessionWrapper(session, getServletContext());
currentSession.setNew(false);
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 、 HttpServletRequest.");
}
setAttribute(INVALID_SESSION_ID_ATTR, "true");
}
}
//上面的逻辑就是通过request里面header头里的sessionId去查找对应的Session的逻辑。如果create设置为false,那到这就完了。下面是创建新session的逻辑。
if (!create) {
return null;
}
//debug日志记录
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)"));
}
//通过SessionRepository创建新的session,包装成HttpSessionWrapper,同样将此Session设置到Request的属性中,方便下次使用。
S session = SessionRepositoryFilter.this.sessionRepository.createSession();
session.setLastAccessedTime(System.currentTimeMillis());
currentSession = new HttpSessionWrapper(session, getServletContext());
setCurrentSession(currentSession);
return currentSession;
//注意这整个getSession的逻辑里面没有涉及到session最后的持久化,这个逻辑实在commitSession中完成的。
}
- commitSession 在SessionRepositoryFilter拦截器执行的最后会被执行。因为这方法并不打算给用户使用,所以是private的。
private void commitSession() {
HttpSessionWrapper wrappedSession = getCurrentSession();
//从request里面拿出存的session,如果没有或者此sessionId已被标记为失效,就调用HttpSessionStrategy#onInvalidateSession.
//主要作用是设置Response的Cookie,删除失效的sessionId。
if (wrappedSession == null) {
if (isInvalidateClientSession()) {
SessionRepositoryFilter.this.httpSessionStrategy
.onInvalidateSession(this, this.response);
}
}
//如果有session,保存到SessionRepository中
//接着判断这个Session是否是request请求来的时候的那个session,如果不是,说明是新的session。
//调用HttpSessionStrategy#onNewSession,将此sessionId加入Cookie值中。
else {
S session = wrappedSession.getSession();
SessionRepositoryFilter.this.sessionRepository.save(session);
if (!isRequestedSessionIdValid()
|| !session.getId().equals(getRequestedSessionId())) {
SessionRepositoryFilter.this.httpSessionStrategy.onNewSession(session,
this, this.response);
}
}
}
SpringBoot中使用spring-session
如果没有使用springboot,我们需要自己去初始化SessionRepositoryFilter。如果使用了SpringBoot。比如用Redis做为SessionRepository存储session。
在注册文件中引入@EnableRedisHttpSession注解即可。这个注解的实际作用就是引入RedisHttpSessionConfiguration这个预先写好的配置类。其继承于SpringHttpSessionConfiguration。
观察SpringHttpSessionConfiguration的源码可得,它的主要作用就是初始化我们上面谈到的那些组件,CookieSerializer,HttpSessionStrategy等。这是所有SessionRepository实现都需要的组件,所以放在基类中。