zoukankan      html  css  js  c++  java
  • 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);
        }
    }

    ---

  • 相关阅读:
    C#图片处理示例(裁剪,缩放,清晰度,水印)
    lucene4.5近实时搜索
    mongo 多条件 查询
    Lucene:QueryParser
    Lucene的中文分词器IKAnalyzer
    Lucene为不同字段指定不同分词器(转)
    Thrift初用小结
    lucene4.0与之前版本的一些改变
    lucene 资料
    Mongodb快速入门之使用Java操作Mongodb
  • 原文地址:https://www.cnblogs.com/xy-ouyang/p/12709416.html
Copyright © 2011-2022 走看看