一、场景
在集群条件下,比如利用NGINX转发HTTP请求到多个TOMCAT,需要保证每个用户的SESSION在每个TOMCAT下都是一样的。否则会出现需要重复登录的情况,影响用户体验和浪费服务器性能。
二、方案
1、利用Ngnix的ip_hash负载均衡算法每次都将同一用户的所有请求转发至同一台服务器上。优点是简便快捷,缺点是如果那台服务器挂了,所有被hash到那台服务器的ip地址都将无法访问该网站。
2、利用tomcat自带的集群session复制共享,即每次session发生变化时,就广播给所有集群中的服务器,使所有的服务器上的session保持相同。配置不是很难,缺点是会消耗更多内存和带宽,tomcat官方推荐在集群比较小时采用此方案。
3、利用第三方开源组件Tomcat-redis-session-manager来实现session共享。这个方法需要在Tomcat下添加依赖jar包,简单配置context.xml,无代码侵入,但是依赖redis数据库。
三、Shiro实现Session共享
项目中的身份认证、权限管理经常要用到Shiro框架,Shiro也有一套自己的Session管理机制。shiro提供的会话可以用于JavaSE/JavaEE环境,不依赖于任何底层容器,可以独立使用,是完整的会话模块。即可以直接使用shiro的会话管理替换如web容器的会话管理。
Shiro会话管理器 sessionManager
shiro提供了三个默认实现:
- DefaultSessionManager:DefaultSecurityManager使用的默认实现,用于JavaSE环境;
- ServletContainerSessionManager:DefaultWebSecurityManager使用的默认实现,用于Web环境,其直接使用Servlet容器的会话;
- DefaultWebSessionManager:用于Web环境的实现,可以替代ServletContainerSessionManager,自己维护着会话,直接废弃了Servlet容器的会话管理。
单机环境下我们使用DefaultWebSecurityManager的默认会话管理器ServletContainerSessionManager,使用的仍然是Tomcat底层的会话机制。
集群环境下要实现Session共享就要用到DefaultWebSessionManager,自己编写Dao来处理Session在Redis中的增删改查。
结合Spring的配置依赖注入如下:
<bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
<property name="maxIdle" value="1" />
<property name="maxTotal" value="5" />
<property name="blockWhenExhausted" value="true" />
<property name="maxWaitMillis" value="30000" />
<property name="testOnBorrow" value="true" />
</bean>
<bean id="jedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
<property name="hostName" value="127.0.0.1" />
<property name="port" value="6379"/>
<property name="poolConfig" ref="jedisPoolConfig" />
<property name="usePool" value="true"/>
</bean>
<!-- 利用springframework.data.redis操作redis -->
<bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
<property name="connectionFactory" ref="jedisConnectionFactory" />
</bean>
<bean id="redisManager" class="com.java1234.utils.RedisManager">
<property name="redisTemplate" ref="redisTemplate"/>
</bean>
<bean id="shiroRedisSessionDAO" class="com.java1234.dao.RedisSessionDao">
<property name="redisManager" ref="redisManager"/>
</bean>
<!-- sessionIdCookie的实现,用于重写覆盖容器默认的JSESSIONID -->
<bean id="shareSession" class="org.apache.shiro.web.servlet.SimpleCookie">
<!-- cookie的name,对应的默认是 JSESSIONID -->
<constructor-arg name="name" value="SHAREJSESSIONID" />
<!-- jsessionId的path为 / 用于多个系统共享jsessionId -->
<property name="path" value="/" />
<property name="httpOnly" value="true"/>
</bean>
<bean id="defaultWebSessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
<!-- session存储的实现 -->
<property name="sessionDAO" ref="shiroRedisSessionDAO" />
<!-- sessionIdCookie的实现,用于重写覆盖容器默认的JSESSIONID -->
<property name="sessionIdCookie" ref="shareSession" />
<!-- 设置全局会话超时时间,默认30分钟(1800000) -->
<property name="globalSessionTimeout" value="1800000" />
<!-- 是否在会话过期后会调用SessionDAO的delete方法删除会话 默认true -->
<property name="deleteInvalidSessions" value="true" />
<!-- 会话验证器调度时间 -->
<property name="sessionValidationInterval" value="1800000" />
<!-- 定时检查失效的session -->
<property name="sessionValidationSchedulerEnabled" value="true" />
</bean>
<!-- 安全管理器 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="myRealm"/>
<property name="sessionManager" ref="defaultWebSessionManager" />
</bean>
//继承AbstractSessionDAO重写Session的怎能增删改查方法
public class RedisSessionDao extends AbstractSessionDAO {
private Logger logger = LoggerFactory.getLogger(this.getClass());
private RedisManager redisManager;
/**
* The Redis key prefix for the sessions
*/
private static final String KEY_PREFIX = "shiro_redis_session:";
@Override
public void update(Session session) throws UnknownSessionException {
this.saveSession(session);
}
@Override
public void delete(Session session) {
if (session == null || session.getId() == null) {
logger.error("session or session id is null");
return;
}
redisManager.del(KEY_PREFIX + session.getId());
}
@Override
public Collection<Session> getActiveSessions() {
Set<Session> sessions = new HashSet<Session>();
Set<byte[]> keys = redisManager.keys(KEY_PREFIX + "*");
if(keys != null && keys.size()>0){
for(byte[] key : keys){
Session s = (Session) SerializerUtil.deserialize(redisManager.get(SerializerUtil.deserialize(key)));
sessions.add(s);
}
}
return sessions;
}
@Override
protected Serializable doCreate(Session session) {
Serializable sessionId = this.generateSessionId(session);
this.assignSessionId(session, sessionId);
this.saveSession(session);
return sessionId;
}
@Override
protected Session doReadSession(Serializable sessionId) {
if(sessionId == null){
logger.error("session id is null");
return null;
}
Session s = (Session)redisManager.get(KEY_PREFIX + sessionId);
return s;
}
private void saveSession(Session session) throws UnknownSessionException{
if (session == null || session.getId() == null) {
logger.error("session or session id is null");
return;
}
//设置过期时间
long expireTime = 18000000;
session.setTimeout(expireTime);
redisManager.setEx(KEY_PREFIX + session.getId(), session, expireTime);
}
public void setRedisManager(RedisManager redisManager) {
this.redisManager = redisManager;
}
public RedisManager getRedisManager() {
return redisManager;
}
}
//统一操作Redis的Manager工具类
public class RedisManager {
private RedisTemplate<Serializable, Serializable> redisTemplate;
public <T> void set(String key, T obj) throws DataAccessException{
final byte[] bkey = key.getBytes();
final byte[] bvalue = SerializerUtil.serialize(obj);
redisTemplate.execute(new RedisCallback<Void>() {
@Override
public Void doInRedis(RedisConnection connection) throws DataAccessException {
connection.set(bkey, bvalue);
return null;
}
});
}
public <T> boolean setNX(String key, T obj) throws DataAccessException{
final byte[] bkey = key.getBytes();
final byte[] bvalue = SerializerUtil.serialize(obj);
boolean result = redisTemplate.execute(new RedisCallback<Boolean>() {
@Override
public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
return connection.setNX(bkey, bvalue);
}
});
return result;
}
public <T> void setEx(String key, T obj, final long expireSeconds) throws DataAccessException{
final byte[] bkey = key.getBytes();
final byte[] bvalue = SerializerUtil.serialize(obj);
redisTemplate.execute(new RedisCallback<Boolean>() {
@Override
public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
connection.setEx(bkey, expireSeconds, bvalue);
return true;
}
});
}
public <T> T get(final String key) throws DataAccessException{
byte[] result = redisTemplate.execute(new RedisCallback<byte[]>() {
@Override
public byte[] doInRedis(RedisConnection connection) throws DataAccessException {
return connection.get(key.getBytes());
}
});
if (result == null) {
return null;
}
return SerializerUtil.deserialize(result);
}
public Long del(final String key){
if (StringUtils.isEmpty(key)) {
return 0l;
}
Long delNum = redisTemplate.execute(new RedisCallback<Long>() {
@Override
public Long doInRedis(RedisConnection connection) throws DataAccessException {
byte[] keys = key.getBytes();
return connection.del(keys);
}
});
return delNum;
}
public Set<byte[]> keys(final String key){
if (StringUtils.isEmpty(key)) {
return null;
}
Set<byte[]> bytesSet = redisTemplate.execute(new RedisCallback<Set<byte[]>>() {
@Override
public Set<byte[]> doInRedis(RedisConnection connection) throws DataAccessException {
byte[] keys = key.getBytes();
return connection.keys(keys);
}
});
return bytesSet;
}
public RedisTemplate<Serializable, Serializable> getRedisTemplate() {
return redisTemplate;
}
public void setRedisTemplate(RedisTemplate<Serializable, Serializable> redisTemplate) {
this.redisTemplate = redisTemplate;
}
}