木心

毕竟几人真得鹿,不知终日梦为鱼

导航

springboot 整合 spring session 实现 session 共享

录:

1、分布式架构下的 session 共享问题
2、springboot 整合 spring session 的整合过程
3、简读 Spring Session 源码

 

1、分布式架构下的 session 共享问题    <--返回目录

1.1、session 的作用:

  因为 HTTP 是无状态的协议,web 服务器为了区分记住用户的状态,会为每个用户创建一个会话,存储用户的相关信息,以便在后面的请求中,可以定位到同一个上下文。

  例如用户在登陆之后,在进行页面跳转的时候,存储在 session 中的信息会一直保持,如果用户还没有 session,那么服务器会创建一个 session 对象,直到会话过期或主动放弃(退出),服务器才会把 session 终止掉。

  配合客户端(浏览器)的使用,一般会使用 cookie 来管理 session。

 

1.2、分布式架构中的 session 问题

  单服务器架构下,session 直接保存在服务器中,是一点问题都没有的。随着分布式架构的流行,单个服务器已经不能满足系统的需要了,通常都会把系统部署多个实例,通过负载均衡把请求分发到其中的一个实例上。这样同一个用户的请求可能被分发到不同的实例上,比如第一次请求访问实例 A,创建了 session,但是下一次访实例 B,这个时候就会出现取不到 session 的情况。于是,分布式架构中,session 共享就成了一个很大的问题。

 

1.3、分布式架构下的 session 共享问题的解决方案

  1)不要有 session:大家可能觉得我说了句废话,但是确实在某些场景下,是可以没有 session 的,其实在很多接口类系统当中,都提倡【API无状态服务】;也就是每一次的接口访问,都不依赖于 session、不依赖于前一次的接口访问;

    - 不用 session,比如可以使用 token;

  2)存入 cookie 中:将 session 存储到 cookie 中,但是缺点也很明显,例如每次请求都得带着 session,数据存储在客户端本地,是有风险的;

    - 即把用户信息等数据直接存到 cookie,这样显然是不安全的;

  3)session 同步:对个服务器之间同步session,这样可以保证每个服务器上都有全部的session信息,不过当服务器数量比较多的时候,同步是会有延迟甚至同步失败;

  4)使用Nginx(或其他负载均衡软硬件)中的ip绑定策略,同一个ip只能在指定的同一个机器访问,但是这样做风险也比较大,而且也是去了负载均衡的意义;

  5)我们现在的系统会把 session 放到 Redis 中存储,虽然架构上变得复杂,并且需要多访问一次Redis,但是这种方案带来的好处也是很大的:实现session共享,可以水平扩展(增加Redis服务器),服务器重启session不丢失(不过也要注意session在Redis中的刷新/失效机制),不仅可以跨服务器session共享,甚至可以跨平台(例如网页端和APP端)。

 

  下面介绍上面解决方案 5 的一个实现:使用 Spring Session。

 

2、springboot 整合 spring session 的整合过程    <--返回目录

参考:SpringBoot 2 整合 Spring Session 最简操作

测试结果:

  1)访问 http://localhost:8080/demo/add/username/zs,向 session 中添加属性 username=zs

 

   2) 访问 http://localhost:8081/demo/get/username, 获取 session 中属性 username

 

   可以看到生成的 包含 sessionId 的 cookie 的 path是 “/项目名”。所以,如果两个项目的项目名不同,则 cookie 不能传递过去。这时需要自定义配置 cookie 的 path。

 

3、简读 Spring Session 源码    <--返回目录

  Spring Session 原理是:实现一个过滤器,将 原生的 request, response, session 等进行装饰,并通过 "filterChain.doFilter(wrappedRequest, wrappedResponse);" 进行掉包,从而开发者在程序中得到的 request, response, session 都是调包后的装饰对象。

// 过滤器 
SessionRepositoryFilter {
    @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 {
            // write the session id to the response and persist the Session
            wrappedRequest.commitSession();
        }
    }
}
// HttpSessionWrapper#commitSession()
// write the session id to the response and persist the Session
private void commitSession() {
    HttpSessionWrapper wrappedSession = getCurrentSession();
    if (wrappedSession == null) {
        if (isInvalidateClientSession()) {
            SessionRepositoryFilter.this.httpSessionIdResolver.expireSession(this, this.response);
        }
    }
    else {
        S session = wrappedSession.getSession();
        clearRequestedSessionCache();
        SessionRepositoryFilter.this.sessionRepository.save(session);
        String sessionId = session.getId();
        if (!isRequestedSessionIdValid() || !sessionId.equals(getRequestedSessionId())) {
            // 写 cookie 到 client
            SessionRepositoryFilter.this.httpSessionIdResolver.setSessionId(this, this.response, sessionId);
        }
    }
}

// 写 cookie 到 client
public class DefaultCookieSerializer implements CookieSerializer {
    @Override
    public void writeCookieValue(CookieValue cookieValue) {
        HttpServletRequest request = cookieValue.getRequest();
        HttpServletResponse response = cookieValue.getResponse();
        StringBuilder sb = new StringBuilder();
        sb.append(this.cookieName).append('=');
        String value = getValue(cookieValue);
        if (value != null && value.length() > 0) {
            validateValue(value);
            sb.append(value);
        }
        int maxAge = getMaxAge(cookieValue);
        if (maxAge > -1) {
            sb.append("; Max-Age=").append(cookieValue.getCookieMaxAge());
            ZonedDateTime expires = (maxAge != 0) ? ZonedDateTime.now(this.clock).plusSeconds(maxAge)
                    : Instant.EPOCH.atZone(ZoneOffset.UTC);
            sb.append("; Expires=").append(expires.format(DateTimeFormatter.RFC_1123_DATE_TIME));
        }
        String domain = getDomainName(request);
        if (domain != null && domain.length() > 0) {
            validateDomain(domain);
            sb.append("; Domain=").append(domain);
        }
        String path = getCookiePath(request);
        if (path != null && path.length() > 0) {
            validatePath(path);
            sb.append("; Path=").append(path);
        }
        if (isSecureCookie(request)) {
            sb.append("; Secure");
        }
        if (this.useHttpOnlyCookie) {
            sb.append("; HttpOnly");
        }
        if (this.sameSite != null) {
            sb.append("; SameSite=").append(this.sameSite);
        }
        response.addHeader("Set-Cookie", sb.toString());
    }
}

 

  "HttpSession session = request.getSession(); " 的底层实现过程:

// 创建session
// 类型是 SessionRepositoryFilter$SessionRepositoryRequestWrapper$HttpSessionWrapper
SessionRepositoryRequestWrapper extends HttpServletRequestWrapper {
    @Override
    public HttpSessionWrapper getSession() {
        return getSession(true);
    }
    
    @Override
    public HttpSessionWrapper getSession(boolean create) {
        HttpSessionWrapper currentSession = getCurrentSession();
        if (currentSession != null) {
            return currentSession;
        }
        S requestedSession = getRequestedSession();
        // 这里代码省略。。。
        if (!create) {
            return null;
        }
        // session 底层结构:MapSession
        S session = SessionRepositoryFilter.this.sessionRepository.createSession();
        session.setLastAccessedTime(Instant.now());
        // 使用 HttpSessionWrapper 包装 MapSession
        currentSession = new HttpSessionWrapper(session, getServletContext());
        setCurrentSession(currentSession);
        return currentSession;
    }
        
}

// session 的底层结构:MapSession
public class RedisIndexedSessionRepository {

    @Override
    public RedisSession createSession() {
        MapSession cached = new MapSession();
        if (this.defaultMaxInactiveInterval != null) {
            cached.setMaxInactiveInterval(Duration.ofSeconds(this.defaultMaxInactiveInterval));
        }
        RedisSession session = new RedisSession(cached, true);
        session.flushImmediateIfNecessary();
        return session;
    }
}

  

  "session.setAttribute(name, value);" 的底层实现过程:

// 设置属性
class HttpSessionAdapter<S extends Session> implements HttpSession {
    @Override
    public void setAttribute(String name, Object value) {
        checkState();
        Object oldValue = this.session.getAttribute(name);
        // 
        this.session.setAttribute(name, value);
        // 这里代码省略。。。
    }

}

RedisIndexedSessionRepository {
    @Override
    public void setAttribute(String attributeName, Object attributeValue) {
        // 这个 cached 就是底层结构 MapSession
        this.cached.setAttribute(attributeName, attributeValue);
        this.delta.put(getSessionAttrNameKey(attributeName), attributeValue);
        // 根据配置 spring.session.redis.flush-mode=on_save/immediate 判断
        flushImmediateIfNecessary();
    }
    
    private void flushImmediateIfNecessary() {
        if (RedisIndexedSessionRepository.this.flushMode == FlushMode.IMMEDIATE) {
            save();
        }
    }
    
    private void save() {
        saveChangeSessionId();
        saveDelta();
    }

    /**
     * Saves any attributes that have been changed and updates the expiration of this
     * session.
     */
    private void saveDelta() {
        if (this.delta.isEmpty()) {
            return;
        }
        String sessionId = getId();
        getSessionBoundHashOperations(sessionId).putAll(this.delta);
        String principalSessionKey = getSessionAttrNameKey(
                FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME);
        String securityPrincipalSessionKey = getSessionAttrNameKey(SPRING_SECURITY_CONTEXT);
        if (this.delta.containsKey(principalSessionKey) || this.delta.containsKey(securityPrincipalSessionKey)) {
            if (this.originalPrincipalName != null) {
                String originalPrincipalRedisKey = getPrincipalKey(this.originalPrincipalName);
                RedisIndexedSessionRepository.this.sessionRedisOperations.boundSetOps(originalPrincipalRedisKey)
                        .remove(sessionId);
            }
            Map<String, String> indexes = RedisIndexedSessionRepository.this.indexResolver.resolveIndexesFor(this);
            String principal = indexes.get(PRINCIPAL_NAME_INDEX_NAME);
            this.originalPrincipalName = principal;
            if (principal != null) {
                String principalRedisKey = getPrincipalKey(principal);
                RedisIndexedSessionRepository.this.sessionRedisOperations.boundSetOps(principalRedisKey)
                        .add(sessionId);
            }
        }

        this.delta = new HashMap<>(this.delta.size());

        Long originalExpiration = (this.originalLastAccessTime != null)
                ? this.originalLastAccessTime.plus(getMaxInactiveInterval()).toEpochMilli() : null;
        RedisIndexedSessionRepository.this.expirationPolicy.onExpirationUpdated(originalExpiration, this);
    }
}

---

posted on 2020-04-15 23:13  wenbin_ouyang  阅读(1800)  评论(0编辑  收藏  举报