【SSO单点登录04】分布式session-springsession的应用

引言

现在项目多采用分布式集群的方式进行部署,而如何实现session共享成为分布式负载均衡的一个关键(毕竟,如果session都无法共享,就别提分布式了)

解决方式1-JWT

上期我们讲如何通过黑名单机制和妥善的保存secret进行JWT的主动注销和防篡改,实现其高可用性,以及通过redis实现session共享来实现分布式的应用。
我们可以使用JWT,采用统一的加密解密方式(加密算法,secret等),来实现session共享

解决方式2-nginx负载均衡

一般采用nginx+tomcat实现负载均衡。此时用户http请求可能均衡打到集群中的每一个站点中,假设在站点A中进行了用户认证和登录,A站点保存了用户的session信息,但B站点并没有得到用户的session信息,当请求通过负载均衡来到B站点时,用户在b站点是没有登录的状态,无法实现登录状态在集群服务器中的共享

nginx的ip_hash

解决这一问题不难,我们可以通过hash算法将用户的每次请求都打在同一服务器上,用nginx的ip_hash可以使得某个ip的用户,只固定访问某个特定的服务器,这样就不会请求到其他服务器,但请求有可能打到单一服务器,造成单一服务器宕机,违背了分布式应用中负载均衡的初衷。

于是,我们可以引入一个新的框架来接管会话的session数据,把session数据保存到服务器以外的一个统一的地方,上述问题自然就迎刃而解了。于是,springsession应运而生。

解决方式3-springsession

Spring-Session官方文档:springsession

springsession的特性

Spring Session提供了一套创建和管理Servlet HttpSession的方案,默认采用外置的Redis来存储Session数据,以此来解决Session共享的问题。

Spring Session提供以下特性:

  • API和用于管理用户会话的实现;
  • 允许以应用程序容器(即Tomcat)中性的方式替换HttpSession;
  • Spring Session 让支持集群会话变得不那么繁琐
  • Spring session支持在单个浏览器实例中管理多个用户的会话。
  • Spring Session 允许在headers 中提供会话ID以使用RESTful API。

实现原理

Spring-Session的实现就是设计一个过滤器SessionRepositoryFilter
SessionRepositoryFilter 会先拦截到请求,将 request 和 response 对象转换成 SessionRepositoryRequestWrapper 和SessionRepositoryResponseWrapper 。后续当第一次调用 request 的getSession方法时,会调用到 SessionRepositoryRequestWrapper 的getSession方法。

如果从request中的属性中查找不到session,再通过cookie拿到sessionid去redis中查找,如果差查不到,就直接创建一个redissession对象,并同步到redis中。将创建销毁session的过程从服务器转移到redis中去。

部分源码

/**
     * HttpServletRequest getSession()实现
     */
    @Override
    public HttpSessionWrapper getSession() {
        return getSession(true);
    }

    @Override
    public HttpSessionWrapper getSession(boolean create) {
        HttpSessionWrapper currentSession = getCurrentSession();
        if (currentSession != null) {
            return currentSession;
        }
        //从当前请求获取sessionId
        String requestedSessionId = getRequestedSessionId();
        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 this HttpServletRequest.");
                }
                setAttribute(INVALID_SESSION_ID_ATTR, "true");
            }
        }
        if (!create) {
            return null;
        }
        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)"));
        }
        //为当前请求创建session
        S session = SessionRepositoryFilter.this.sessionRepository.createSession();
        //更新时间
        session.setLastAccessedTime(System.currentTimeMillis());
        //对Spring session 进行包装(包装成HttpSession)
        currentSession = new HttpSessionWrapper(session, getServletContext());
        setCurrentSession(currentSession);
        return currentSession;
    }

    /**
     * 根据sessionId获取session
     */
    private S getSession(String sessionId) {
        S session = SessionRepositoryFilter.this.sessionRepository
                .getSession(sessionId);
        if (session == null) {
            return null;
        }
        session.setLastAccessedTime(System.currentTimeMillis());
        return session;
    }

    /**
     * 从当前请求获取sessionId
     */
    @Override
    public String getRequestedSessionId() {
        return SessionRepositoryFilter.this.httpSessionStrategy
                .getRequestedSessionId(this);
    }

    private void setCurrentSession(HttpSessionWrapper currentSession) {
        if (currentSession == null) {
            removeAttribute(CURRENT_SESSION_ATTR);
        }
        else {
            setAttribute(CURRENT_SESSION_ATTR, currentSession);
        }
    }
    /**
     * 获取当前请求session
     */
    @SuppressWarnings("unchecked")
    private HttpSessionWrapper getCurrentSession() {
        return (HttpSessionWrapper) getAttribute(CURRENT_SESSION_ATTR);
    }

总结

springsession提供了很好的共享session方案,但通过源码查看,发现每次getSession()都是获取本地session缓存,如何保证redis中的session与本地缓存中的一致性呢?下期我们通过应用springsession来探究。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值