shiro 通过redis实现session共享
共享Session目的
在分布式服务的工程中,一个服务器断了,负载均衡服务器会把请求分配给其他的服务器,如果设置了session共享,就不需要用户再次登录了.
shiro实现session共享的原理
默认的情况下,shiro的session是在服务器上的,当该服务器宕掉了,session就不存在了,用户必须重新登录,如果我们把shiro的session存到redis服务器上,就可以实现session共享了.
如何实现?
如果我们想通过redis实现shiro的session共享,只需要把shiro的安全管理器(SecurityManager)中的2个属性替换了就可以实现共享
但是序列化还有个大坑,如果你也遇到了,请看到最后
- sessionDAO
- cacheManager
下面是自定义的这2个属性的代码
**RedisSessionDao **
import org.apache.shiro.session.Session;
import org.apache.shiro.session.UnknownSessionException;
import org.apache.shiro.session.mgt.eis.AbstractSessionDAO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.RedisTemplate;
import java.io.Serializable;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.TimeUnit;
public class RedisSessionDao extends AbstractSessionDAO {
private static Logger logger = LoggerFactory.getLogger(RedisSessionDao.class);
private RedisTemplate redisTemplate;
private String keyPrefix = "shiro.redis.session:";
public RedisSessionDao(RedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
private String getKey(String key) {
return keyPrefix + key;
}
private void saveSession(Session session) throws UnknownSessionException {
if (session != null && session.getId() != null) {
String key = this.getKey(session.getId().toString());
session.setTimeout(30*60*1000);
redisTemplate.opsForValue().set(key, session, 30*60*1000, TimeUnit.MILLISECONDS);
} else {
logger.error("session or session id is null");
}
}
@Override
public void update(Session session) throws UnknownSessionException {
this.saveSession(session);
}
@Override
public void delete(Session session) {
try {
String key = getKey(session.getId().toString());
redisTemplate.delete(key);
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
}
@Override
public Collection<Session> getActiveSessions() {
Set<Session> sessions = new HashSet<>();
Set<String> keys = redisTemplate.keys(getKey("*"));
if (keys != null && keys.size() > 0) {
for (String key : keys) {
Session s = (Session) redisTemplate.opsForValue().get(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) {
Session readSession = null;
try {
readSession = (Session) redisTemplate.opsForValue().get(getKey(sessionId.toString()));
} catch (Exception e) {
logger.error(e.getMessage(),e);
}
return readSession;
}
}
RedisCacheManager
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.apache.shiro.cache.CacheManager;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.data.redis.core.RedisTemplate;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
public class RedisCacheManager implements CacheManager {
private final ConcurrentMap<String, Cache> caches = new ConcurrentHashMap<String, Cache>();
private RedisTemplate redisTemplate;
public RedisCacheManager(RedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
@Override
public <K, V> Cache<K, V> getCache(String name) throws CacheException {
Cache cache = this.caches.get(name);
if (cache == null) {
//自定义shiroCache
cache = new ShiroRedisCache<K, V>();
this.caches.put(name, cache);
}
return cache;
}
private class ShiroRedisCache<K, V> implements Cache<K, V> {
private long cacheLive = 30*60*1000;
private String cacheKeyPrefix = "shiro.redis.cacheKey:";
@Override
public V get(K k) throws CacheException {
return (V) redisTemplate.opsForValue().get(this.getRedisCacheKey(k));
}
@Override
public V put(K k, V v) throws CacheException {
redisTemplate.opsForValue().set(this.getRedisCacheKey(k), v, cacheLive, TimeUnit.MINUTES);
return v;
}
@Override
public V remove(K k) throws CacheException {
V obj = (V) redisTemplate.opsForValue().get(this.getRedisCacheKey(k));
redisTemplate.delete(this.getRedisCacheKey(k));
return obj;
}
@Override
public void clear() throws CacheException {
Set keys = redisTemplate.keys(this.cacheKeyPrefix + "*");
if (null != keys && keys.size() > 0) {
Iterator itera = keys.iterator();
redisTemplate.delete(itera.next());
}
}
@Override
public int size() {
Set<K> keys = redisTemplate.keys(this.cacheKeyPrefix + "*");
return keys.size();
}
@Override
public Set<K> keys() {
return redisTemplate.keys(this.cacheKeyPrefix + "*");
}
@Override
public Collection<V> values() {
Set<K> keys = redisTemplate.keys(this.cacheKeyPrefix + "*");
Set<V> values = new HashSet<V>(keys.size());
for (K key : keys) {
values.add((V) redisTemplate.opsForValue().get(this.getRedisCacheKey(key)));
}
return values;
}
private String getRedisCacheKey(K key) {
Object redisKey = this.getStringRedisKey(key);
if (redisKey instanceof String) {
return this.cacheKeyPrefix + redisKey;
} else {
return String.valueOf(redisKey);
}
}
private Object getStringRedisKey(K key) {
Object redisKey;
if (key instanceof PrincipalCollection) {
redisKey = this.getRedisKeyFromPrincipalCollection((PrincipalCollection) key);
} else {
redisKey = key.toString();
}
return redisKey;
}
private Object getRedisKeyFromPrincipalCollection(PrincipalCollection key) {
List realmNames = this.getRealmNames(key);
Collections.sort(realmNames);
Object redisKey = this.joinRealmNames(realmNames);
return redisKey;
}
private List<String> getRealmNames(PrincipalCollection key) {
ArrayList realmArr = new ArrayList();
Set realmNames = key.getRealmNames();
Iterator i$ = realmNames.iterator();
while (i$.hasNext()) {
String realmName = (String) i$.next();
realmArr.add(realmName);
}
return realmArr;
}
private Object joinRealmNames(List<String> realmArr) {
StringBuilder redisKeyBuilder = new StringBuilder();
for (int i = 0; i < realmArr.size(); ++i) {
String s = realmArr.get(i);
redisKeyBuilder.append(s);
}
String redisKey = redisKeyBuilder.toString();
return redisKey;
}
}
}
然后在shiro配置的时候替换这2个属性
@Autowired
private RedisTemplate redisTemplate;
@Bean
public DefaultWebSessionManager defaultWebSessionManager() {
DefaultWebSessionManager defaultWebSessionManager = new DefaultWebSessionManager();
defaultWebSessionManager.setSessionDAO(new RedisSessionDao(redisTemplate));
return defaultWebSessionManager;
}
@Bean
public SecurityManager securityManager(@Qualifier("manageShiroRealm") ManageShiroRealm manageShiroRealm, @Qualifier("defaultWebSessionManager") DefaultWebSessionManager defaultWebSessionManager) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
//设置自己的ShiroRealm
securityManager.setSessionManager(defaultWebSessionManager);
securityManager.setCacheManager(new RedisCacheManager(redisTemplate));
return securityManager;
}
session序列化大坑
使用上面的代码就可以把shiro的session存到redis上了,但是还有一个序列化大坑在等着我们
shiro的session的实现类是SimpleSession,这个类是个大坑,重写又很麻烦,很少有人重写,下面我说一下我研究的半天的结果
经常使用的序列化方式有以下几种
- java原生序列化,序列化后是二进制,难以查看
- Jackson JSON 序列化
- FastJSON序列化
- ProtoBuff序列化,优点:体积小 速度快 缺点:需要自己写.proto文件,二进制存储,难以查看
下面我来说一下JSON格式的2中序列化
b) Jackson2JsonRedisSerializer
// 指定要序列化的域,field,get和set,以及修饰符范围,ANY是都有包括private和public
// om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
// // 指定序列化输入的类型,类必须是非final修饰的,final修饰的类
// om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
会出以下问题 - isValid()方法会序列化出一个valid,反序列化的时候报错,这个可以设置unknow策略搞定
- 但是getAttributesLazy也会被序列化,它没有对应的set方法,走到这里我就不再找解决办法了,应该有解决办法,但是也有可能会出新的问题,时间紧,放弃了
c) 然后我使用fastJSON来序列化,出现了transient变量不能序列化的问题,这个可以通过修改全局配置来解决
static {
JSON.DEFAULT_GENERATE_FEATURE = SerializerFeature.config(
JSON.DEFAULT_GENERATE_FEATURE, SerializerFeature.SkipTransientField, false);
}
本文介绍如何利用Shiro和Redis实现分布式环境下的Session共享,确保用户在服务器切换时无需重复登录。文章提供了自定义RedisSessionDao和RedisCacheManager的具体实现,并讨论了序列化过程中可能遇到的问题及解决方案。

1万+

被折叠的 条评论
为什么被折叠?



