本篇博客的讲述流程:
先看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 redisTemplate(RedisConnectionFactory cf) {
RedisTemplate redisTemplate = new RedisTemplate();
redisTemplate.setConnectionFactory(cf);
return redisTemplate;
}
@Bean
public CacheManager cacheManager(RedisTemplate 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 httpSessionListeners = new ArrayList();
@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 SessionRepositoryFilter extends ExpiringSession> springSessionRepositoryFilter(
SessionRepository sessionRepository) {
SessionRepositoryFilter sessionRepositoryFilter = new SessionRepositoryFilter(
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
RedisTemplate
public class WebContextInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
......
@Override
protected Filter[] getServletFilters() {
return new Filter[] {new CharacterEncodingFilter("UTF-8", true),
new DelegatingFilterProxy("springSessionRepositoryFilter")};
}
}
Spring-session没有几个Bean,相关的组件基本都简略的介绍完了。
总结: