1. 程式人生 > >apache shiro叢集實現(一) session共享

apache shiro叢集實現(一) session共享

    Apache Shiro的基本配置和構成這裡就不詳細說明了,其官網有說明文件,這裡僅僅說明叢集的解決方案,詳細配置:shiro web config

    Apache Shiro叢集要解決2個問題,一個是session的共享問題,一個是授權資訊的cache共享問題,官網給的例子是Ehcache的實現,在配置說明上不算很詳細,我這裡用nosql(redis)替代了ehcache做了session和cache的儲存。

shiro spring的預設配置(單機,非叢集)

<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
    <property name="realm" ref="shiroDbRealm" />
    <property name="cacheManager" ref="memoryConstrainedCacheManager" />
</bean>

<!-- 自定義Realm -->
<bean id="shiroDbRealm" class="com.xxx.security.shiro.custom.ShiroDbRealm">
    <property name="credentialsMatcher" ref="customCredentialsMather"></property>
</bean> 
<!-- 使用者授權資訊Cache(本機記憶體實現) -->
<bean id="memoryConstrainedCacheManager" class="org.apache.shiro.cache.MemoryConstrainedCacheManager" />	

<!-- 保證實現了Shiro內部lifecycle函式的bean執行 -->
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>

<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
    <property name="securityManager" ref="securityManager" />
    <property name="loginUrl" value="/login" />
    <property name="successUrl" value="/project" />
    <property name="filterChainDefinitions">
        <value>
	    /login = authc
	    /logout = logout			
	</value>
    </property>
</bean>


上面的配置是shiro非叢集下的配置,DefaultWebSecurityManager類不需要注入sessionManager屬性,它會使用預設的sessionManager類,請看原始碼

public DefaultWebSecurityManager() {
        super();
        ((DefaultSubjectDAO) this.subjectDAO).setSessionStorageEvaluator(new DefaultWebSessionStorageEvaluator());
        this.sessionMode = HTTP_SESSION_MODE;
        setSubjectFactory(new DefaultWebSubjectFactory());
        setRememberMeManager(new CookieRememberMeManager());
        setSessionManager(new ServletContainerSessionManager());
}

在最後一行,set了預設的servlet容器實現的sessionManager,sessionManager會管理session的建立、刪除等等。如果我們需要讓session在叢集中共享,就需要替換這個預設的sessionManager。在其官網上原話是這樣的:
Native Sessions

If you want your session configuration settings and clustering to be portable across servlet containers
(e.g. Jetty in testing, but Tomcat or JBoss in production), or you want to control specific session/clustering 
features, you can enable Shiro's native session management.

The word 'Native' here means that Shiro's own enterprise session management implementation will be used to support 
all Subject and HttpServletRequest sessions and bypass the servlet container completely. But rest assured - Shiro 
implements the relevant parts of the Servlet specification directly so any existing web/http related code works as 
expected and never needs to 'know' that Shiro is transparently managing sessions.

DefaultWebSessionManager

To enable native session management for your web application, you will need to configure a native web-capable 
session manager to override the default servlet container-based one. You can do that by configuring an instance of 
DefaultWebSessionManager on Shiro's SecurityManager. 

我們可以看到如果要用叢集,就需要用本地會話,這裡shiro給我準備了一個預設的native session manager,DefaultWebSessionManager,所以我們要修改spring配置檔案,注入DefaultWebSessionManager

<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
    <property name="sessionManager" ref="defaultWebSessionManager" />
    <property name="realm" ref="shiroDbRealm" />
    <property name="cacheManager" ref="memoryConstrainedCacheManager" />
</bean>
<bean id="defaultWebSessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
    <property name="globalSessionTimeout" value="1200000" />
</bean>
我們繼續看DefaultWebSessionManager的原始碼發現其父類DefaultSessionManager中有sessionDAO屬性,這個屬性是真正實現了session儲存的類,這個就是我們自己實現的redis session的儲存類。
 protected SessionDAO sessionDAO;

 private CacheManager cacheManager;

 private boolean deleteInvalidSessions;

 public DefaultSessionManager() {
     this.deleteInvalidSessions = true;
     this.sessionFactory = new SimpleSessionFactory();
     this.sessionDAO = new MemorySessionDAO();
 }
這裡我們看到了,如果不自己注入sessionDAO,defaultWebSessionManager會使用MemorySessionDAO做為預設實現類,這個肯定不是我們想要的,所以這就自己動手實現sessionDAO吧。
public class CustomShiroSessionDAO extends AbstractSessionDAO {

	private ShiroSessionRepository shiroSessionRepository;

	public ShiroSessionRepository getShiroSessionRepository() {
		return shiroSessionRepository;
	}

	public void setShiroSessionRepository(
			ShiroSessionRepository shiroSessionRepository) {
		this.shiroSessionRepository = shiroSessionRepository;
	}

	@Override
	public void update(Session session) throws UnknownSessionException {
		getShiroSessionRepository().saveSession(session);
	}

	@Override
	public void delete(Session session) {
		if (session == null) {
			LoggerUtil.error(CustomShiroSessionDAO.class,
					"session can not be null,delete failed");
			return;
		}
		Serializable id = session.getId();
		if (id != null)
			getShiroSessionRepository().deleteSession(id);
	}

	@Override
	public Collection<Session> getActiveSessions() {
		return getShiroSessionRepository().getAllSessions();
	}

	@Override
	protected Serializable doCreate(Session session) {
		Serializable sessionId = this.generateSessionId(session);
		this.assignSessionId(session, sessionId);
		getShiroSessionRepository().saveSession(session);
		return sessionId;
	}

	@Override
	protected Session doReadSession(Serializable sessionId) {
		return getShiroSessionRepository().getSession(sessionId);
	}
}
我們自定義CustomShiroSessionDAO繼承AbstractSessionDAO,實現對session操作的方法。這裡為了便於擴充套件,我引入了一個介面ShiroSessionRepository,可以用redis、mongoDB等進行實現。
public interface ShiroSessionRepository {

	void saveSession(Session session);

	void deleteSession(Serializable sessionId);

	Session getSession(Serializable sessionId);

	Collection<Session> getAllSessions();
}
這個是我自己redis的ShiroSessionReposotory儲存實現類:
public class JedisShiroSessionRepository extends JedisManager implements
		ShiroSessionRepository {

	/**
	 * redis session key字首
	 */
	private final String REDIS_SHIRO_SESSION = "shiro-session:";

	@Autowired
	private JedisPool jedisPool;

	@Override
	protected JedisPool getJedisPool() {
		return jedisPool;
	}

	@Override
	protected JedisDataType getJedisDataType() {
		return JedisDataType.SESSION_CACHE;
	}

	@Override
	public void saveSession(Session session) {
		if (session == null || session.getId() == null) {
			LoggerUtil.error(JedisShiroSessionRepository.class,
					"session或者session id為空");
			return;
		}
		byte[] key = SerializeUtil
				.serialize(getRedisSessionKey(session.getId()));
		byte[] value = SerializeUtil.serialize(session);
		Jedis jedis = this.getJedis();
		try {
			Long timeOut = session.getTimeout() / 1000;
			jedis.set(key, value);
			jedis.expire(key, Integer.parseInt(timeOut.toString()));
		} catch (JedisException e) {
			LoggerUtil.error(JedisShiroSessionRepository.class, "儲存session失敗",
					e);
		} finally {
			this.returnResource(jedis);
		}
	}

	@Override
	public void deleteSession(Serializable id) {
		if (id == null) {
			LoggerUtil.error(JedisShiroSessionRepository.class, "id為空");
			return;
		}
		Jedis jedis = this.getJedis();
		try {
			jedis.del(SerializeUtil.serialize(getRedisSessionKey(id)));
		} catch (JedisException e) {
			LoggerUtil.error(JedisShiroSessionRepository.class, "刪除session失敗",
					e);
		} finally {
			this.returnResource(jedis);
		}
	}

	@Override
	public Session getSession(Serializable id) {
		if (id == null) {
			LoggerUtil.error(JedisShiroSessionRepository.class, "id為空");
			return null;
		}
		Session session = null;
		Jedis jedis = this.getJedis();
		try {
			byte[] value = jedis.get(SerializeUtil
					.serialize(getRedisSessionKey(id)));
			session = SerializeUtil.deserialize(value, Session.class);
		} catch (JedisException e) {
			LoggerUtil.error(JedisShiroSessionRepository.class, "獲取id為" + id
					+ "的session失敗", e);
		} finally {
			this.returnResource(jedis);
		}
		return session;
	}

	@Override
	public Collection<Session> getAllSessions() {
		Jedis jedis = this.getJedis();
		Set<Session> sessions = new HashSet<Session>();
		try {
			Set<byte[]> byteKeys = jedis.keys(SerializeUtil
					.serialize(this.REDIS_SHIRO_SESSION + "*"));
			if (byteKeys != null && byteKeys.size() > 0) {
				for (byte[] bs : byteKeys) {
					Session s = SerializeUtil.deserialize(jedis.get(bs),
							Session.class);
					sessions.add(s);
				}
			}
		} catch (JedisException e) {
			LoggerUtil.error(JedisShiroSessionRepository.class,
					"獲取所有session失敗", e);
		} finally {
			this.returnResource(jedis);
		}
		return sessions;
	}

	/**
	 * 獲取redis中的session key
	 * 
	 * @param sessionId
	 * @return
	 */
	private String getRedisSessionKey(Serializable sessionId) {
		return this.REDIS_SHIRO_SESSION + sessionId;
	}

}

這樣sessionDAO我們就完成了,下面繼續修改我們spring配置檔案:

<bean id="defaultWebSessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
    <property name="globalSessionTimeout" value="1200000" />
    <property name="sessionDAO" ref="customShiroSessionDAO" />
</bean>

<bean id="customShiroSessionDAO" class="com.xxx.security.shiro.custom.session.CustomShiroSessionDAO">
    <property name="shiroSessionRepository" ref="jedisShiroSessionRepository" />
</bean>
	
<bean id="jedisShiroSessionRepository" class="com.xxx.security.shiro.custom.session.JedisShiroSessionRepository" />



這樣第一個問題,session的共享問題我們就解決好了,下一篇介紹另一個問題,cache的共享問題。

原始碼地址:https://github.com/michaelliuyang/shiro-redis-cluster