一、场景
在集群条件下,比如利用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的配置依赖注入如下:
//继承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 getActiveSessions() {
Set sessions = new HashSet();
Set 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 redisTemplate;
public void set(String key, T obj) throws DataAccessException{
final byte[] bkey = key.getBytes();
final byte[] bvalue = SerializerUtil.serialize(obj);
redisTemplate.execute(new RedisCallback() {
@Override
public Void doInRedis(RedisConnection connection) throws DataAccessException {
connection.set(bkey, bvalue);
return null;
}
});
}
public 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() {
@Override
public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
return connection.setNX(bkey, bvalue);
}
});
return result;
}
public 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() {
@Override
public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
connection.setEx(bkey, expireSeconds, bvalue);
return true;
}
});
}
public T get(final String key) throws DataAccessException{
byte[] result = redisTemplate.execute(new RedisCallback() {
@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() {
@Override
public Long doInRedis(RedisConnection connection) throws DataAccessException {
byte[] keys = key.getBytes();
return connection.del(keys);
}
});
return delNum;
}
public Set keys(final String key){
if (StringUtils.isEmpty(key)) {
return null;
}
Set bytesSet = redisTemplate.execute(new RedisCallback>() {
@Override
public Set doInRedis(RedisConnection connection) throws DataAccessException {
byte[] keys = key.getBytes();
return connection.keys(keys);
}
});
return bytesSet;
}
public RedisTemplate getRedisTemplate() {
return redisTemplate;
}
public void setRedisTemplate(RedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
}