SpringSession源码解析

spring session 关键组件的UML图

image.png
    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<String> readCookieValues(HttpServletRequest request) {
        Cookie[] cookies = request.getCookies();
        List<String> matchingCookieValues = new ArrayList<String>();
        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个接口的功能。

  1. HttpSessionStrategy的3个接口
    1. getRequestedSessionId 获取sessionId 既获取spring session设置在Cookie中的键SESSION的值,既spring session的ID。这里的读取利用了CookieSerializer工具类的读取方法。
    2. onNewSession 在session被创建时被调用,作用???
    3. onInvalidateSession 想要删除session时调用的方法
  2. HttpSessionManager
    1. getCurrentSessionAlias
      就是取得当前session的别名,如果你没有用多session,默认就是0。
      如果你的url里面传入了查询参数_s=数字,就会返回这个数字别名。
      比如"mockUrl?_s=2"那么,返回的就是2。
    2. getSessionIds
      找到SESSION里面所有的session别名作为key,sessionId作为value返回。
    3. encodeURL
      ??????将url和别名拼接在一起
    4. getNewSessionAlias
      返回一个新的session别名,阅读源码可以知道其实就是在原来的别名基础上+1,比如你已经在SESSSION 键里面有 0 12312012-2321-23,别名就是0了,现在新增一个别名,就是返回1.
同一个浏览器,支持多个session
SessionRepositoryFilter
SessionRepositoryRequestWrapper

这个类就是用来代替容器自身HttpServletRequest的实现。这里有2个方法是重点。

  1. 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中完成的。
      }

  1. 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实现都需要的组件,所以放在基类中。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,133评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,682评论 3 390
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,784评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,508评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,603评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,607评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,604评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,359评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,805评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,121评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,280评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,959评论 5 339
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,588评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,206评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,442评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,193评论 2 367
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,144评论 2 352

推荐阅读更多精彩内容