Spring-session+Redis 实现SSO其源码简析和注意事项

本篇博客的讲述流程:

  1. 列出配置实现
  2. 对配置上的关键点做解释说明
  3. 讲述配置实现的注意事项

先看Redis连接池配置:

import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;

/**
 * @author JJB
 * @version $Id$
 * @since 2016年12月5日 下午4:24:01
 */
@Configuration
@EnableCaching(proxyTargetClass = true)
public class RedisCacheContextConfig extends CachingConfigurerSupport {

    @Bean //使用默认的配置,具体可查看JedisConnectionFactory类的变量默认值
    public JedisConnectionFactory redisConnectionFactory() {
        return new JedisConnectionFactory();
    }

    @Bean
    public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory cf) {
        RedisTemplate<String, String> redisTemplate = new RedisTemplate<String, String>();
        redisTemplate.setConnectionFactory(cf);
        return redisTemplate;
    }

    @Bean
    public CacheManager cacheManager(RedisTemplate<String, String> redisTemplate) {
        RedisCacheManager cacheManager = new RedisCacheManager(redisTemplate);
        cacheManager.setDefaultExpiration(3000);
        return cacheManager;
    }

}

接下来看Spring-session整合配置类:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
import org.springframework.session.events.SessionCreatedEvent;
import org.springframework.session.events.SessionDeletedEvent;
import org.springframework.session.events.SessionExpiredEvent;
import org.springframework.session.web.http.DefaultCookieSerializer;

/**
 * Spring-session整合配置类
 *
 * @author wb-jjb318191
 * @create 2018-01-25 15:06
 */
@Configuration
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 120)
public class SpringSessionConfiguration {

    private static final Logger LOGGER = LoggerFactory.getLogger(SpringSessionConfiguration.class);

    /**
     * Spring-session-redis执行线程池
     *
     * @return
     */
    @Bean
    public ThreadPoolTaskScheduler springSessionRedisTaskExecutor() {
        ThreadPoolTaskScheduler taskSchedule = new ThreadPoolTaskScheduler();
        taskSchedule.setPoolSize(3);
        return taskSchedule;
    }

    @Bean
    public DefaultCookieSerializer defaultCookieSerializer() {
        DefaultCookieSerializer defaultCookieSerializer = new DefaultCookieSerializer();
        defaultCookieSerializer.setCookiePath("/");
        return defaultCookieSerializer;
    }

    /**
     * Redis内session过期事件监听
     *
     * @param expiredEvent
     */
    @EventListener
    public void onSessionExpired(SessionExpiredEvent expiredEvent) {
        String sessionId = expiredEvent.getSessionId();
        LOGGER.info(expiredEvent.getSession().getAttribute("user"));
        LOGGER.info("[{}]session过期", sessionId);
    }

    /**
     * Redis内session删除事件监听
     *
     * @param deletedEvent
     */
    @EventListener
    public void onSessionDeleted(SessionDeletedEvent deletedEvent) {
        String sessionId = deletedEvent.getSessionId();
        LOGGER.info(deletedEvent.getSession().getAttribute("user"));
        LOGGER.info("删除session[{}]", sessionId);
    }

    /**
     * Redis内session保存事件监听
     *
     * @param createdEvent
     */
    @EventListener
    public void onSessionCreated(SessionCreatedEvent createdEvent) {
        String sessionId = createdEvent.getSessionId();
        LOGGER.info(createdEvent.getSession().getAttribute("user"));
        LOGGER.info("保存session[{}]", sessionId);
    }

}

@EnableRedisHttpSession,这个注解的最重要作用就是引入Spring-session的内在配置类:RedisHttpSessionConfiguration,其继承自SpringHttpSessionConfiguration,我们先看看这两个配置类里的关键处:

@Configuration
public class SpringHttpSessionConfiguration implements ApplicationContextAware {

    private CookieHttpSessionStrategy defaultHttpSessionStrategy = new CookieHttpSessionStrategy();

    private boolean usesSpringSessionRememberMeServices;

    private ServletContext servletContext;

    private CookieSerializer cookieSerializer;

    private HttpSessionStrategy httpSessionStrategy = this.defaultHttpSessionStrategy;

    private List<HttpSessionListener> httpSessionListeners = new ArrayList<HttpSessionListener>();

    @PostConstruct
    public void init() {
        if (this.cookieSerializer != null) {
            this.defaultHttpSessionStrategy.setCookieSerializer(this.cookieSerializer);
        }
        else if (this.usesSpringSessionRememberMeServices) {
            DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
            cookieSerializer.setRememberMeRequestAttribute(
                    SpringSessionRememberMeServices.REMEMBER_ME_LOGIN_ATTR);
            this.defaultHttpSessionStrategy.setCookieSerializer(cookieSerializer);
        }
    }

    @Bean
    public SessionEventHttpSessionListenerAdapter sessionEventHttpSessionListenerAdapter() {
        return new SessionEventHttpSessionListenerAdapter(this.httpSessionListeners);
    }

    @Bean
    public <S extends ExpiringSession> SessionRepositoryFilter<? extends ExpiringSession> springSessionRepositoryFilter(
            SessionRepository<S> sessionRepository) {
        SessionRepositoryFilter<S> sessionRepositoryFilter = new SessionRepositoryFilter<S>(
                sessionRepository);
        sessionRepositoryFilter.setServletContext(this.servletContext);
        if (this.httpSessionStrategy instanceof MultiHttpSessionStrategy) {
            sessionRepositoryFilter.setHttpSessionStrategy(
                    (MultiHttpSessionStrategy) this.httpSessionStrategy);
        }
        else {
            sessionRepositoryFilter.setHttpSessionStrategy(this.httpSessionStrategy);
        }
        return sessionRepositoryFilter;
    }

    @Autowired(required = false)
    public void setCookieSerializer(CookieSerializer cookieSerializer) {
        this.cookieSerializer = cookieSerializer;
    }

}

SessionRepositoryFilter,这个类应该是Spring-session里最关键的Bean了,他是一个Filter,他的作用就是封装HttpServietRequest,HttpServletResponse,改变其获取Session的行为,原始的获取Session方式是从服务器容器内获取,而SessionRepositoryFilter将其改变为从其他地方获取,比如从整合的Redis内,当不存在Session时,创建一个封装过的Session,设置到Redis中,同时将此Session关联的Cookie注入到返回结果中,可看其内部的Request和Session的包装类:

private final class SessionRepositoryRequestWrapper
            extends HttpServletRequestWrapper {
        private Boolean requestedSessionIdValid;
        private boolean requestedSessionInvalidated;
        private final HttpServletResponse response;
        private final ServletContext servletContext;

        private SessionRepositoryRequestWrapper(HttpServletRequest request,
                HttpServletResponse response, ServletContext servletContext) {
            super(request);
            this.response = response;
            this.servletContext = servletContext;
        }

        /** 更新Session内的数据及最近访问时间到Redis中,若session过期,则清除浏览器cookie的sessionId值
         * Uses the HttpSessionStrategy to write the session id to the response and
         * persist the Session.
         */
        private void commitSession() {
            HttpSessionWrapper wrappedSession = getCurrentSession();
            if (wrappedSession == null) {
                if (isInvalidateClientSession()) {
                    SessionRepositoryFilter.this.httpSessionStrategy
                            .onInvalidateSession(this, this.response);
                }
            }
            else {
                S session = wrappedSession.getSession();
                SessionRepositoryFilter.this.sessionRepository.save(session);
                if (!isRequestedSessionIdValid()
                        || !session.getId().equals(getRequestedSessionId())) {
                    SessionRepositoryFilter.this.httpSessionStrategy.onNewSession(session,
                            this, this.response);
                }
            }
        }

        //重写获取session的方法,服务区容器内不存在当前请求相关的session,但是请求内含有
        //session=***形式的Cookie时,尝试通过此sessionId从Redis内获取相关的Session信息
        //这就是实现SSO的关键之处
        public HttpSessionWrapper getSession(boolean create) {
            HttpSessionWrapper currentSession = getCurrentSession();
            if (currentSession != null) {
                return currentSession;
            }
            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)"));
            }
            S session = SessionRepositoryFilter.this.sessionRepository.createSession();
            session.setLastAccessedTime(System.currentTimeMillis());
            currentSession = new HttpSessionWrapper(session, getServletContext());
            setCurrentSession(currentSession);
            return currentSession;
        }

        @Override
        public HttpSessionWrapper getSession() {
            return getSession(true);
        }

        /**
         * Allows creating an HttpSession from a Session instance.
         *
         * @author Rob Winch
         * @since 1.0
         */
        private final class HttpSessionWrapper extends ExpiringSessionHttpSession<S> {

            HttpSessionWrapper(S session, ServletContext servletContext) {
                super(session, servletContext);
            }

            //重写session失效方法,在设置Session失效的同时删除Redis数据库内Session信息
            public void invalidate() {
                super.invalidate();
                SessionRepositoryRequestWrapper.this.requestedSessionInvalidated = true;
                setCurrentSession(null);
                SessionRepositoryFilter.this.sessionRepository.delete(getId());
            }
        }
    }

再看SpringHttpSessionConfiguration内的CookieSerializer:

    @Autowired(required = false)
    public void setCookieSerializer(CookieSerializer cookieSerializer) {
        this.cookieSerializer = cookieSerializer;
    }

CookieSerializer 这个类的作用就是生成Cookie,然后设置到Response中,这样浏览器就会将这个Cookie的信息写入到客户端中,同时在session过期时会清除之前设置的cookie值。我们在自己的配置类中定义了一个Bean DefaultCookieSerializer,同时设置其cookiePath属性为”/”:

    /**
     * 自定义返回给前端的Cookie的项目根路径
     *
     * @return
     */
    @Bean
    public DefaultCookieSerializer defaultCookieSerializer() {
        DefaultCookieSerializer defaultCookieSerializer = new DefaultCookieSerializer();
        defaultCookieSerializer.setCookiePath("/");
        return defaultCookieSerializer;
    }

在这里需要注意,当我们没有自定义一个CookieSerializer时,Spring会使用默认的CookieSerializer,也就是直接new DefaultCookieSerializer()的形式,这样的话其设置Cookie的Path属性时会使用当前项目的根路径+”/”

public class DefaultCookieSerializer implements CookieSerializer {
    ...
    private String getCookiePath(HttpServletRequest request) {
        if (this.cookiePath == null) {
            return request.getContextPath() + "/";
        }
        return this.cookiePath;
    }
    ...
}

也就是说如果我当前项目的应用路径是/project,那么Cookie里的Path就是/project/ :

这里写图片描述

如果我的项目应用路径是/demo,那么Cookie的Path就是/demo/ 了。

在此处先说下客户端浏览器上的Cookie的作用原理,对于浏览器上的Cookie来说,Domain和Path是决定当前请求是否要携带这个Cookie的关键。如果当前请求的域名与Domain一致或是Domain的子域名,且域名后的应用名称与Path一致,那么当前请求就会携带上此Cookie的数据。如果两个选项有一个不符合,则不会带上Cookie的数据。

默认的CookieSerializer会设置Cookie的Path为当前应用路径加上”/”,也就是说如果我一台服务器上部署了多个子应用,不同应用的路径不同,则他们生成的Cookie的Path不同,那么就导致子应用间的Cookie不能共享,而子应用共享Cookie是实现SSO的关键,所以我们需要自定义一个CookieSerializer,让所有子应用的Path均相同。如果子应用不是部署在同一台服务器上,那么需要设置他们的Domain相同,使用统一的二级域名,比如baidu.com,csdn.cn等,同时设置其Path均相同,这样才是单点登录实现的关键。设置Cookie的Domain时有点要注意,只能设置为当前访问域名或者是其父级域名,若设置为子级域名则不会发生作用,若设置为其他域名,比如当前应用的域名为www.baidu.com,但你设置Domain为www.csdn.com,这种设置形式不能实现,这是浏览器对Cookie的一种限定。

接下来看RedisHttpSessionConfiguration这个配置类。在讲解其之前说点其他的。Spring-session不是只能整合Redis,还可以整合很多其他的,最直接的显示就是有多个和@EnableRedisHttpSession功能相同的注解,比如@EnableHazelcastHttpSession,@EnableJdbcHttpSession,@EnableMongoHttpSession,@EnableGemFireHttpSession,@EnableSpringHttpSession,其实他们的配置类都继承了SpringHttpSessionConfiguration,也就是说根本上还是通过封装Request,Response的形式改变获取Session的行为,感兴趣的可以自行查看他们的各自实现方法,这里只讲整合Redis的形式。

既然是整合Redis,那么肯定需要操作Redis的相关对象,RedisHttpSessionConfiguration这个配置类里定义的Bean基本都和Redis相关:

@Configuration
@EnableScheduling
public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguration
        implements EmbeddedValueResolverAware, ImportAware {
    ......
    @Bean
    public RedisMessageListenerContainer redisMessageListenerContainer(
            RedisConnectionFactory connectionFactory,
            RedisOperationsSessionRepository messageListener) {
        ......
        container.addMessageListener(messageListener,
                Arrays.asList(new PatternTopic("__keyevent@*:del"),
                        new PatternTopic("__keyevent@*:expired")));
        container.addMessageListener(messageListener, Arrays.asList(new PatternTopic(
                messageListener.getSessionCreatedChannelPrefix() + "*")));
        return container;
    }

    @Bean
    public RedisTemplate<Object, Object> sessionRedisTemplate(
            RedisConnectionFactory connectionFactory) {
        ......
    }

    @Bean
    public RedisOperationsSessionRepository sessionRepository(
            @Qualifier("sessionRedisTemplate") RedisOperations<Object, Object> sessionRedisTemplate,
            ApplicationEventPublisher applicationEventPublisher) {
        ......
    }

    @Autowired(required = false)
    @Qualifier("springSessionRedisTaskExecutor")
    public void setRedisTaskExecutor(Executor redisTaskExecutor) {
        this.redisTaskExecutor = redisTaskExecutor;
    }

    @Autowired(required = false)
    @Qualifier("springSessionRedisSubscriptionExecutor")
    public void setRedisSubscriptionExecutor(Executor redisSubscriptionExecutor) {
        this.redisSubscriptionExecutor = redisSubscriptionExecutor;
    }

    ......
}

RedisTemplate

public class WebContextInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
    ......
    @Override
    protected Filter[] getServletFilters() {
        return new Filter[] {new CharacterEncodingFilter("UTF-8", true),
            new DelegatingFilterProxy("springSessionRepositoryFilter")};
    }
}

Spring-session没有几个Bean,相关的组件基本都简略的介绍完了。

总结:

  1. 多个子应用必须有相同的父级域名(至少二级域名),或者域名相同,同时抹去返回Cookie的Path,实现共享Cookie,这点是实现单点登录(SSO)的前提。
  2. 注意尽量让Spring-session内的过滤器的位置靠前,因为他重新包装了Http请求的相关对象,所以在这个过滤器之后的Request,Response,Session对象都是其封装过后的对象,如果其之前的Filter里获取了Session等信息,那么他们和之后的Session就不是一个对象了。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值