基于redis缓存的session共享
结合上面的 MSM 思想,由 redis负责 session 数据的存储,而我们自己实现的 session manager 将负责 session 生命周期的管理。
此架构存在着当redis master故障时, 虽然可以有一到多个备用slave,但是redis不会主动的进行master切换,这时session服务中断。
为了做到redis的高可用,引入了zookper或者haproxy或者keepalived来解决redis master slave的切换问题。即:
此体系结构中, redis master出现故障时, 通过haproxy设置redis slave为临时master, redis master重新恢复后,
再切换回去. 此方案中, redis-master 与redis-slave 是双向同步的, 解决目前redis单点问题. 这样保证了session信息
在redis中的高可用。
实现此方案:
nginx 1 192.168.1.102
tomcat1 1
tomcat2 1
redis-master 1
redis-slave 1
slave1 1
slave2 1
haproxy vip 1
Apache Shiro集群要解决2个问题,一个是session的共享问题,一个是授权信息的cache共享问题。从DefaultWebSecurityManager入手看源码
发现其父类DefaultSessionManager中有sessionDAO属性,这个属性是真正实现了session储存的类,这个就是我们自己实现的redis session的储存类。
RedisSessionDAO.java
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.util.SerializationUtils;
import java.io.Serializable;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
public class RedisSessionDAO extends AbstractSessionDAO {
private static Logger logger = LoggerFactory.getLogger(RedisSessionDAO.class);
/**
* shiro-redis的session对象前缀
*/
private RedisManager redisManager;
/**
* The Redis key prefix for the sessions
*/
private String keyPrefix = "shiro_session:";
@Override
public void update(Session session) throws UnknownSessionException {
this.saveSession(session);
}
/**
* save session
* @param session
* @throws UnknownSessionException
*/
private void saveSession(Session session) throws UnknownSessionException{
System.out.println("----saveSession---"+session.getId());
if(session == null || session.getId() == null){
logger.error("session or session id is null");
return;
}
byte[] key = getByteKey(session.getId());
byte[] value = SerializationUtils.serialize(session);
// session.setTimeout(redisManager.getExpire()*1000);
this.redisManager.set(key, value, Integer.parseInt(session.getTimeout()+""));
}
@Override
public void delete(Session session) {
if(session == null || session.getId() == null){
logger.error("session or session id is null");
return;
}
redisManager.del(this.getByteKey(session.getId()));
}
@Override
public Collection<Session> getActiveSessions() {
Set<Session> sessions = new HashSet<Session>();
Set<byte[]> keys = redisManager.keys(this.keyPrefix + "*");
if(keys != null && keys.size()>0){
for(byte[] key:keys){
Session s = (Session)SerializationUtils.deserialize(redisManager.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) {
if(sessionId == null){
logger.error("session id is null");
return null;
}
Session s = (Session)SerializationUtils.deserialize(redisManager.get(this.getByteKey(sessionId)));
return s;
}
/**
* 获得byte[]型的key
* @param sessionId
* @return
*/
private byte[] getByteKey(Serializable sessionId){
String preKey = this.keyPrefix + sessionId;
return preKey.getBytes();
}
public RedisManager getRedisManager() {
return redisManager;
}
public void setRedisManager(RedisManager redisManager) {
this.redisManager = redisManager;
/**
* 初始化redisManager
*/
this.redisManager.init();
}
/**
* Returns the Redis session keys
* prefix.
* @return The prefix
*/
public String getKeyPrefix() {
return keyPrefix;
}
/**
* Sets the Redis sessions key
* prefix.
* @param keyPrefix The prefix
*/
public void setKeyPrefix(String keyPrefix) {
this.keyPrefix = keyPrefix;
}
}
RedisManager.java
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import java.util.Set;
public class RedisManager {
private static Logger logger = LoggerFactory.getLogger(RedisSessionDAO.class);
private String host;
private int port;
// 0 - never expire
private int expire = 0;
private static JedisPool jedisPool = null;
public RedisManager() {
}
/**
* 初始化方法
*/
public void init(){
if(null == host || 0 == port){
logger.error("请初始化redis配置文件!");
throw new NullPointerException("找不到redis配置");
}
if(jedisPool == null){
//jedisPool = JedisUtil.getJedisPool();
jedisPool = new JedisPool(new JedisPoolConfig(), host, port);
}
}
/**
* get value from redis
* @param key
* @return
*/
public byte[] get(byte[] key) {
byte[] value = null;
Jedis jedis = jedisPool.getResource();
try {
value = jedis.get(key);
} finally {
jedisPool.returnResource(jedis);
}
return value;
}
/**
* get value from redis
* @param key
* @return
*/
public String get(String key) {
String value=null;
Jedis jedis = jedisPool.getResource();
try {
value = jedis.get(key);
} finally {
jedisPool.returnResource(jedis);
}
return value;
}
/**
* set
* @param key
* @param value
* @return
*/
public byte[] set(byte[] key, byte[] value) {
Jedis jedis = jedisPool.getResource();
try {
jedis.set(key, value);
if (this.expire != 0) {
jedis.expire(key, this.expire);
}
} finally {
jedisPool.returnResource(jedis);
}
return value;
}
/**
* set
* @param key
* @param value
* @return
*/
public String set(String key,String value) {
Jedis jedis = jedisPool.getResource();
try {
jedis.set(key, value);
if (this.expire != 0) {
jedis.expire(key, this.expire);
}
} finally {
jedisPool.returnResource(jedis);
}
return value;
}
/**
* set
* @param key
* @param value
* @param expire
* @return
*/
public byte[] set(byte[] key, byte[] value, int expire) {
Jedis jedis = jedisPool.getResource();
try {
jedis.set(key, value);
if (expire != 0) {
jedis.expire(key, expire);
}
} finally {
jedisPool.returnResource(jedis);
}
return value;
}
/**
* set
* @param key
* @param value
* @param expire
* @return
*/
public String set(String key,String value, int expire) {
Jedis jedis = jedisPool.getResource();
try {
jedis.set(key, value);
if (expire != 0) {
jedis.expire(key, expire);
}
} finally {
jedisPool.returnResource(jedis);
}
return value;
}
/**
* del
* @param key
*/
public void del(byte[] key) {
Jedis jedis = jedisPool.getResource();
try {
jedis.del(key);
} finally {
jedisPool.returnResource(jedis);
}
}
/**
* del
* @param key
*/
public void del(String key) {
Jedis jedis = jedisPool.getResource();
try {
jedis.del(key);
} finally {
jedisPool.returnResource(jedis);
}
}
/**
* flush
*/
public void flushDB() {
Jedis jedis = jedisPool.getResource();
try {
jedis.flushDB();
} finally {
jedisPool.returnResource(jedis);
}
}
/**
* size
*/
public Long dbSize() {
Long dbSize = 0L;
Jedis jedis = jedisPool.getResource();
try {
dbSize = jedis.dbSize();
} finally {
jedisPool.returnResource(jedis);
}
return dbSize;
}
/**
* keys
* @param pattern
* @return
*/
public Set<byte[]> keys(String pattern) {
Set<byte[]> keys = null;
Jedis jedis = jedisPool.getResource();
try {
keys = jedis.keys(pattern.getBytes());
} finally {
jedisPool.returnResource(jedis);
}
return keys;
}
public String getHost() {
return host;
}
public void setHost(String host) {
this.host = host;
}
public int getPort() {
return port;
}
public void setPort(int port) {
this.port = port;
}
public int getExpire() {
return expire;
}
public void setExpire(int expire) {
this.expire = expire;
}
public void destroy(){
jedisPool.destroy();
}
}
RedisCacheManager.java
import org.apache.shiro.ShiroException;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.apache.shiro.cache.CacheManager;
import org.apache.shiro.cache.MapCache;
import org.apache.shiro.util.Destroyable;
import org.apache.shiro.util.Initializable;
import org.apache.shiro.util.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import redis.clients.jedis.JedisPoolConfig;
import redis.clients.jedis.JedisShardInfo;
import redis.clients.jedis.ShardedJedisPool;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
public class RedisCacheManager implements CacheManager, Destroyable {
private static final Logger logger = LoggerFactory.getLogger(RedisCacheManager.class);
// fast lookup by name map
private final ConcurrentMap<String, Cache> caches = new ConcurrentHashMap<String, Cache>();
private RedisManager redisManager;
/**
* The Redis key prefix for caches
*/
private String keyPrefix = "shiro_redis_cache:";
/**
* Returns the Redis session keys
* prefix.
*
* @return The prefix
*/
public String getKeyPrefix() {
return keyPrefix;
}
/**
* Sets the Redis sessions key
* prefix.
*
* @param keyPrefix The prefix
*/
public void setKeyPrefix(String keyPrefix) {
this.keyPrefix = keyPrefix;
}
@Override
public <K, V> Cache<K, V> getCache(String name) throws CacheException {
logger.debug("获取名称为: " + name + " 的RedisCache实例");
Cache c = caches.get(name);
if (c == null) {
// initialize the Redis manager instance
redisManager.init();
// create a new cache instance
c = new RedisCache<K, V>(redisManager, keyPrefix);
// add it to the cache collection
caches.put(name, c);
}
return c;
}
public RedisManager getRedisManager() {
return redisManager;
}
public void setRedisManager(RedisManager redisManager) {
this.redisManager = redisManager;
}
@Override
public void destroy() throws Exception {
this.redisManager.destroy();
}
}
RedisCache.javapackage com.zte.alm.cs.ui.shiro.redis;
package com.zte.alm.cs.ui.shiro.redis;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.apache.shiro.subject.PrincipalCollection;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.CollectionUtils;
import org.springframework.util.SerializationUtils;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.ShardedJedis;
import redis.clients.jedis.ShardedJedisPool;
import java.util.*;
public class RedisCache<K, V> implements Cache<K, V> {
private Logger logger = LoggerFactory.getLogger(this.getClass());
/**
* The wrapped Jedis instance.
*/
private RedisManager cache;
/**
* The Redis key prefix for the sessions
*/
private String keyPrefix = "shiro_session:";
/**
* Returns the Redis session keys
* prefix.
* @return The prefix
*/
public String getKeyPrefix() {
return keyPrefix;
}
/**
* Sets the Redis sessions key
* prefix.
* @param keyPrefix The prefix
*/
public void setKeyPrefix(String keyPrefix) {
this.keyPrefix = keyPrefix;
}
/**
* 通过一个JedisManager实例构造RedisCache
*/
public RedisCache(RedisManager cache) {
if (cache == null) {
throw new IllegalArgumentException("Cache argument cannot be null.");
}
this.cache = cache;
}
/**
* Constructs a cache instance with the specified
* Redis manager and using a custom key prefix.
* @param cache The cache manager instance
* @param prefix The Redis key prefix
*/
public RedisCache(RedisManager cache, String prefix) {
this(cache);
// set the prefix
this.keyPrefix = prefix;
}
/**
* 获得byte[]型的key
* @param key
* @return
*/
private byte[] getByteKey(K key) {
if (key instanceof String) {
String preKey = this.keyPrefix + key;
return preKey.getBytes();
} else if(key instanceof PrincipalCollection){
String preKey = this.keyPrefix + key.toString();
return preKey.getBytes();
}else{
return SerializationUtils.serialize(key);
}
}
@Override
public V get(K key) throws CacheException {
logger.debug("根据key从Redis中获取对象 key [" + key + "]");
try {
if (key == null) {
return null;
} else {
byte[] rawValue = cache.get(getByteKey(key));
@SuppressWarnings("unchecked")
V value = (V) SerializationUtils.deserialize(rawValue);
return value;
}
} catch (Throwable t) {
throw new CacheException(t);
}
}
public String getStr(String key) throws CacheException {
logger.debug("根据key从Redis中获取对象 key [" + key + "]");
try {
if (key == null) {
return null;
} else {
return cache.get(key);
}
} catch (Throwable t) {
throw new CacheException(t);
}
}
@Override
public V put(K key, V value) throws CacheException {
logger.debug("根据key从存储 key [" + key + "]");
try {
cache.set(getByteKey(key), SerializationUtils.serialize(value));
return value;
} catch (Throwable t) {
throw new CacheException(t);
}
}
public String putStr(String key, String value) throws CacheException {
logger.debug("根据key从存储 key [" + key + "]");
try {
cache.set(key, value);
return value;
} catch (Throwable t) {
throw new CacheException(t);
}
}
public String put(String key,String value, int expire) throws CacheException {
logger.debug("根据key从存储 key [" + key + "]");
try {
cache.set(key, value, expire);
return value;
} catch (Throwable t) {
throw new CacheException(t);
}
}
public String removeString(String key) throws CacheException {
logger.debug("从redis中删除 key [" + key + "]");
try {
String previous = cache.get(key);
cache.del(key);
return previous;
} catch (Throwable t) {
throw new CacheException(t);
}
}
@Override
public V remove(K key) throws CacheException {
logger.debug("从redis中删除 key [" + key + "]");
try {
V previous = get(key);
cache.del(getByteKey(key));
return previous;
} catch (Throwable t) {
throw new CacheException(t);
}
}
@Override
public void clear() throws CacheException {
logger.debug("从redis中删除所有元素");
try {
cache.flushDB();
} catch (Throwable t) {
throw new CacheException(t);
}
}
@Override
public int size() {
try {
Long longSize = new Long(cache.dbSize());
return longSize.intValue();
} catch (Throwable t) {
throw new CacheException(t);
}
}
@SuppressWarnings("unchecked")
@Override
public Set<K> keys() {
try {
Set<byte[]> keys = cache.keys(this.keyPrefix + "*");
if (CollectionUtils.isEmpty(keys)) {
return Collections.emptySet();
} else {
Set<K> newKeys = new HashSet<K>();
for (byte[] key : keys) {
newKeys.add((K) key);
}
return newKeys;
}
} catch (Throwable t) {
throw new CacheException(t);
}
}
@Override
public Collection<V> values() {
try {
Set<byte[]> keys = cache.keys(this.keyPrefix + "*");
if (!CollectionUtils.isEmpty(keys)) {
List<V> values = new ArrayList<V>(keys.size());
for (byte[] key : keys) {
@SuppressWarnings("unchecked") V value = get((K) key);
if (value != null) {
values.add(value);
}
}
return Collections.unmodifiableList(values);
} else {
return Collections.emptyList();
}
} catch (Throwable t) {
throw new CacheException(t);
}
}
}
ShiroRealm.java
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import javax.annotation.Resource;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import com.zte.alm.cs.persist.entity.sys.SysUser;
import com.zte.alm.cs.service.sys.ISysGroupService;
import com.zte.alm.cs.service.sys.ISysOrgRoleService;
import com.zte.alm.cs.service.sys.ISysOrgService;
import com.zte.alm.cs.service.sys.ISysPermissionResourceService;
import com.zte.alm.cs.service.sys.ISysUserService;
public class ShiroRealm extends AuthorizingRealm {
private static final Logger log = LoggerFactory.getLogger(ShiroRealm.class);
//清空
protected void doClearCache(PrincipalCollection principals) {
super.doClearCache(principals);
this.clearCachedAuthorizationInfo(principals);
}
//修改key的值
public Object getAuthorizationCacheKey(PrincipalCollection principals){
Collection<?> collection = principals.fromRealm(getName());
if (collection == null || collection.isEmpty()) {
return null;
}
ShiroUser shiroUser = (ShiroUser) collection.iterator().next();
return shiroUser.getLoginName();
}
applicationContext-shiro.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd"
default-lazy-init="true">
<description>Shiro安全配置</description>
<!-- 凭证匹配器 -->
<bean id="credentialsMatcher" class="com.test.shiro.CredentialsMatcher">
</bean>
<!-- Shiro's main business-tier object for web-enabled applications -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="sessionManager" ref="defaultWebSessionManager" />
<property name="realm" ref="almShiroRealm" />
<!-- <property name="cacheManager" ref="shiroEhcacheManager" /> -->
<property name="cacheManager" ref="redisCacheManager" />
</bean>
<bean id="defaultWebSessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
<!-- 相隔多久检查一次session的有效性 -->
<property name="sessionValidationInterval" value="1800000"/>
<!-- session 有效时间为半小时 (毫秒单位)-->
<property name="globalSessionTimeout" value="1800000"/>
<!-- 是否在会话过期后会调用SessionDAO的delete方法删除会话 默认true -->
<property name="deleteInvalidSessions" value="true" />
<property name="sessionDAO" ref="redisShiroSessionDAO"/>
</bean>
<!-- 項目自定义的Realm -->
<bean id="almShiroRealm" class="com.test.shiro.ShiroRealm">
<property name="credentialsMatcher" ref="credentialsMatcher" />
</bean>
<!-- Shiro Filter -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager" />
<property name="loginUrl" value="/login" />
<property name="successUrl" value="/login/index" />
<property name="filters">
<map>
<!-- 是否启用验证码检验 -->
<entry key="authc" value-ref="CaptchaFormAuthenticationFilter" />
<entry key="user" value-ref="AlmUserFilter" />
</map>
</property>
<property name="filterChainDefinitions">
<value>
/js/** = anon
/css/**=anon
/img/**=anon
/font/**=anon
/mail/**=anon
/login =authc
/logout = logout
/** = user
</value>
</property>
</bean>
<!-- 用户授权信息Cache, 采用EhCache -->
<bean id="shiroEhcacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
<property name="cacheManagerConfigFile" value="classpath:conf/ehcache/ehcache-shiro.xml" />
</bean>
<bean id="redisShiroSessionDAO" class="com.test.shiro.redis.RedisSessionDAO">
<property name="redisManager" ref="redisManager" />
</bean>
<bean id="redisCacheManager" class="com.test.shiro.redis.RedisCacheManager"><!-- 自定义cacheManager -->
<property name="redisManager" ref="redisManager" />
</bean>
<bean id="redisManager" class="com.test.shiro.redis.RedisManager">
<property name="host" value="127.0.0.1" />
<property name="port" value="6379" />
</bean>
<bean id="CaptchaFormAuthenticationFilter" class="com.test.shiro.CaptchaFormAuthenticationFilter" />
<bean id="AlmUserFilter" class="com.test.shiro.AlmUserFilter" />
<!-- 保证实现了Shiro内部lifecycle函数的bean执行 -->
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor" />
</beans>
/**
* 权限
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(
PrincipalCollection principals) {
log.info("--doGetAuthorizationInfo--");
// TODO Auto-generated method stub
Collection<?> collection = principals.fromRealm(getName());
if (collection == null || collection.isEmpty()) {
return null;
}
ShiroUser shiroUser = (ShiroUser) collection.iterator().next();
return newAuthorizationInfo(shiroUser);
}
private SimpleAuthorizationInfo newAuthorizationInfo(ShiroUser shiroUser) {
}
/**
* 用户认证
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(
AuthenticationToken paramAuthenticationToken)
throws AuthenticationException {
}
}
补充:发现在realm的权限中设置的shiroUser信息没有生效,处理方式,重新设置session的属性值
Subject subject = SecurityUtils.getSubject();
Session session = subject.getSession();
String RUN_AS_PRINCIPALS_SESSION_KEY = "org.apache.shiro.subject.support.DefaultSubjectContext_PRINCIPALS_SESSION_KEY";
PrincipalCollection principals = new SimplePrincipalCollection(shiroUser,this.getName());
session.setAttribute(RUN_AS_PRINCIPALS_SESSION_KEY, principals);