使用shiro的會話管理和redis快取管理來構建登入模組spring+struts+hibernate(SSH)
shiro是一個很好用的安全框架,主要表現在使用者認證,許可權認證,會話管理,如果想優化還可以做Cache管理,我們不需要做太多工作在使用者身份token安全方面(記錄shiro及用redis開發的步驟及一些問題,因為網上很多資料都不給全程式碼讓小白沒法理解,這裡我整合了一下,在最後給上專案資源連結,這篇文章是我兩個星期實踐後的體會,大牛不喜勿噴)。
這篇是關於用shiro提供的會話介面和快取介面去實現會話管理和快取管理,優化登入模組,就不講shiro基礎怎麼搭建了,如果想了解shiro的基礎和使用請看我的上一篇’shiro基本配置‘(URL)。
有些人可能會發現如果我在登入後將身份資訊和角色以及許可權資訊存入session,每次我要訪問其他頁面時,都要從session裡拿出來,這樣的效率並不高,redis儲存存結構化資料,存取都很快,雖然還有其他更適用於快取的技術,shiro也可以用他自己家的EHCache啦,不過在這裡我就說redis(因為我對redis有點迷戀,網站計數,訊息啊都在用它,回到主題哈),使用者模組實現redis快取後快了很多。
首先我們先搞定Cache管理(快取)
spring-shiro.xml
shiro的安全管理器的配置,其他的包括過濾器zanshi不用理
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <property name="realm" ref="myRealm" /><!--Realm配置(基礎)--> <property name="sessionManager" ref="sessionManager"/><!--這是session配置--> <property name="cacheManager" ref="customShiroCacheManager"/><!--這是我們自定義的Cache配置--> </bean>
下面這部分程式碼最主要的就是定義了CustomShiroCacheManager這個bean了,因為他實現了shiro 提供的cacheManager介面,而其他都是被呼叫或注入的bean,
spring-shiro.xml
<!-- 這部分引用github的sojson的方法(這部分配置太麻煩了,有時間改為註解掃描注入 --> <!-- redis快取管理器(使用者快取) *test--> <bean id="customShiroCacheManager" class="com.usersAc.shiro.cache.impl.CustomShiroCacheManager"> <property name="shiroCacheManager" ref="jedisShiroCacheManager"/> </bean> <!-- shiro用redis實現快取管理器 *test --> <bean id="jedisShiroCacheManager" class="com.usersAc.shiro.cache.impl.JedisShiroCacheManager"> <property name="jedisManager" ref="jedisManager"/> </bean> <!-- Redis快取 *test--> <bean id="jedisManager" class="com.usersAc.shiro.cache.JedisManager"> <property name="jedisPool" ref="jedisPool"/> </bean>
jedisPool > jedisManager > jedisShiroCacheManager > customShiroCacheManager >securityManager.cacheManager
jedisPool-----就是我們jedis的連線池(配置在下面)
jedisManager -----是我們jedis管理器(自定義),用來定義對redis的操作
jedisShiroCacheManager-----呼叫getCache()返回JedisShiroCache(許可權操作類)
JedisShiroCache-----實現了ache介面,將許可權資訊存入redis快取或從redis快取取出
customShiroCacheManager-----實現了shiro 提供的cacheManager介面,作為Cache管理器
(這些類我也會貼在下面供理解)
下面部分是redis的配置,這裡沒有用redisTemplate,用了一般的配置方法,沒有太多封裝好的方法,有需求就可以自己定義
spring-shiro.xml
<!-- redis池的配置 -->
<bean id="jedisPoolConfig"
class="redis.clients.jedis.JedisPoolConfig">
<property name="maxIdle" value="${redis.maxIdle}" />
<property name="testOnBorrow" value="${redis.testOnBorrow}" />
</bean>
<!-- 我們上面說的jedisPool的配置(配置host,埠,超時,其他預設)-->
<bean id="jedisPool" class="redis.clients.jedis.JedisPool">
<constructor-arg name="poolConfig" ref="jedisPoolConfig" />
<constructor-arg name="host" value="${redis.host}" />
<constructor-arg name="port" value="${redis.port}" type="int" />
<constructor-arg name="timeout" value="${redis.timeout}" type="int" />
</bean>
<!-- redis的context:placeholder ,掃描redis.properties的引數-->
<bean id="propertyConfigurerRedis"
class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="order" value="1" />
<property name="ignoreUnresolvablePlaceholders" value="true" />
<property name="systemPropertiesMode" value="1" />
<property name="searchSystemEnvironment" value="true" />
<property name="locations">
<list>
<value>classpath:redis.properties</value>
</list>
</property>
</bean>
redis.properties
redis.host=127.0.0.1
redis.port=6379
redis.default.db=1
redis.timeout=100000
redis.maxActive=300
redis.maxIdle=100
redis.maxWait=1000
redis.testOnBorrow=true
貼部分注入或呼叫的bean
customShiroCacheManager
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.apache.shiro.cache.CacheManager;
import org.apache.shiro.util.Destroyable;
import com.usersAc.shiro.cache.ShiroCacheManager;
/**
* 這裡的shiroCacheManager會被(jedisShiroCacheManager)注入,
* jedisPool > jedisManager > jedisShiroCacheManager > customShiroCacheManager >securityManager.cacheManager
*
*/
public class CustomShiroCacheManager implements CacheManager, Destroyable {
private ShiroCacheManager shiroCacheManager;//實際注入了JedisShiroCacheManager,而ShiroCacheManager是解耦介面
@Override
public <K, V> Cache<K, V> getCache(String name) throws CacheException {
return getShiroCacheManager().getCache(name);
}
@Override
public void destroy() throws Exception {
shiroCacheManager.destroy();
}
public ShiroCacheManager getShiroCacheManager() {
return shiroCacheManager;
}
public void setShiroCacheManager(ShiroCacheManager shiroCacheManager) {
this.shiroCacheManager = shiroCacheManager;
}
}
JedisShiroCacheManager(實現了ShiroCacheManager(作為解耦的介面)
import org.apache.shiro.cache.Cache;
import com.usersAc.shiro.cache.JedisManager;
import com.usersAc.shiro.cache.JedisShiroCache;
import com.usersAc.shiro.cache.ShiroCacheManager;
/**
* 注入JedisManager(redis底層操作類)
* 身份資訊由sessionManager處理
* 返回JedisShiroCache(許可權操作類)
*/
public class JedisShiroCacheManager implements ShiroCacheManager {
private JedisManager jedisManager; //注入了JedisManager
@Override
public <K, V> Cache<K, V> getCache(String name) {
return new JedisShiroCache<K, V>(name, getJedisManager());
}
@Override
public void destroy() {
}
public JedisManager getJedisManager() {
return jedisManager;
}
public void setJedisManager(JedisManager jedisManager) {
this.jedisManager = jedisManager;
}
}
JedisManager
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import org.apache.shiro.session.Session;
import com.usersAc.common.utils.LoggerUtils;
import com.usersAc.common.utils.SerializeUtil;
import com.usersAc.common.utils.StringUtils;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.exceptions.JedisConnectionException;
/**
* Redis Manager Utils
*
* 這部分用來定義對redis的操作(偽底層,即還有上層呼叫)
*/
public class JedisManager {
/*注入連線池的bean*/
private JedisPool jedisPool;
public Jedis getJedis() {
Jedis jedis = null;
try {
/*獲取連線池資源*/
jedis = getJedisPool().getResource();
} catch (JedisConnectionException e) {
String message = StringUtils.trim(e.getMessage());
if("Could not get a resource from the pool".equalsIgnoreCase(message)){
System.out.println("檢查redis是否啟動");
System.exit(0);//停止專案
}
throw new JedisConnectionException(e);
} catch (Exception e) {
throw new RuntimeException(e);
}
return jedis;
}
/*
*
* 返回資源--資源釋放
*
*/
public void returnResource(Jedis jedis, boolean isBroken) {
if (jedis == null)
return;
/**
* @deprecated starting from Jedis 3.0 this method will not be exposed.
* Resource cleanup should be done using @see {@link redis.clients.jedis.Jedis#close()}
if (isBroken){
getJedisPool().returnBrokenResource(jedis);
}else{
getJedisPool().returnResource(jedis);
}
*/
/* 這裡本來是
* jedis.close();
* 但現在我的jedis版本太低,要至少2.9
* close是將連線返回,使多次使用的redis的連線都是同一個,不會產生在連線數限制數那麼多連線
* 下面這段是quit掉連線,並且如果isConnected(),則socket.close()<!--disconnect()-->關閉socket
* socket的close和shutdown
* close-----關閉本程序的socket id,但連結還是開著的,用這個socket id的其它程序還能用這個連結,能讀或寫這個socket id
* shutdown--則破壞了socket 連結,讀的時候可能偵探到EOF結束符,寫的時候可能會收到一個SIGPIPE訊號,這個訊號可能直到
*/
if (isBroken)
getJedisPool().returnBrokenResource(jedis);
else
getJedisPool().returnResource(jedis);
/* jedis.quit();
jedis.disconnect();*/
}
public byte[] getValueByKey(int dbIndex, byte[] key) throws Exception {
Jedis jedis = null;
byte[] result = null;
boolean isBroken = false;
try {
jedis = getJedis();
jedis.select(dbIndex);
result = jedis.get(key);
} catch (Exception e) {
isBroken = true;
throw e;
} finally {
returnResource(jedis, isBroken);
}
return result;
}
public void deleteByKey(int dbIndex, byte[] key) throws Exception {
Jedis jedis = null;
boolean isBroken = false;
try {
jedis = getJedis();
jedis.select(dbIndex);
Long result = jedis.del(key);
LoggerUtils.fmtDebug(getClass(), "刪除Session結果:%s" , result);
} catch (Exception e) {
isBroken = true;
throw e;
} finally {
returnResource(jedis, isBroken);
}
}
public void saveValueByKey(int dbIndex, byte[] key, byte[] value, int expireTime)
throws Exception {
Jedis jedis = null;
boolean isBroken = false;
try {
jedis = getJedis();
jedis.select(dbIndex);
jedis.set(key, value);
if (expireTime > 0)
jedis.expire(key, expireTime);
} catch (Exception e) {
isBroken = true;
throw e;
} finally {
returnResource(jedis, isBroken);
}
}
public JedisPool getJedisPool() {
return jedisPool;
}
public void setJedisPool(JedisPool jedisPool) {
this.jedisPool = jedisPool;
}
/**
* 獲取所有Session
* @param dbIndex
* @param redisShiroSession
* @return
* @throws Exception
*/
@SuppressWarnings("unchecked")
public Collection<Session> AllSession(int dbIndex, String redisShiroSession) throws Exception {
Jedis jedis = null;
boolean isBroken = false;
Set<Session> sessions = new HashSet<Session>();
try {
jedis = getJedis();
jedis.select(dbIndex);
Set<byte[]> byteKeys = jedis.keys((JedisShiroSessionRepository.REDIS_SHIRO_ALL).getBytes());
if (byteKeys != null && byteKeys.size() > 0) {
for (byte[] bs : byteKeys) {
Session obj = SerializeUtil.deserialize(jedis.get(bs),
Session.class);
if(obj instanceof Session){
sessions.add(obj);
}
}
}
} catch (Exception e) {
isBroken = true;
throw e;
} finally {
returnResource(jedis, isBroken);
}
return sessions;
}
}
程式碼量比較多,我都放在專案的我都放在我專案裡的com.userAc.shiro.cache資料夾下了(有需要文章下面取)
Cache的配置工作基本做完,可能會有人覺得配置麻煩,後面還有會話管理的配置和cookie的配置,不過為了展示好整個shiro準備工作,方便理解,我下次再用註解或Template去簡化。
接下來講Session的配置
jedisShiroSessionRepository------使用jedis管理器,這部分主要是使用者身份的token的快取存取,這裡的JedisManager在上面Cache那有,可以自己看下。
customShiroSessionDAO------繼承了shiro 提供的AbstractSessionDAO介面作為監聽用的DAO
customSessionManager------手動操作session,暫時不需要用到,可以獲取有效session使用者或使用者的所有許可權再用,現在我們僅僅是做session存取登入的token
customSesssionListener------shiro的監聽類,監聽AuthorizingRealmd類的繼承實現Realm(其實是監聽CachingRealm類,而AuthorizingRealmd類是CachingRealm類的子類)
sessionManager------實現會話管理的主配置,要配置在shiro的securityManager
<property name="sessionManager" ref="sessionManager"/><!--這是session配置-->
spring-shiro.xml
<!-- (自定義)session 操作。。。建立、刪除、查詢 -->
<bean id="jedisShiroSessionRepository" class="com.usersAc.shiro.cache.JedisShiroSessionRepository" >
<property name="jedisManager" ref="jedisManager"/>
</bean>
<!-- Shiro對應(自定義)session的監聽 -->
<bean id="customShiroSessionDAO" class="com.usersAc.shiro.CustomShiroSessionDAO">
<property name="shiroSessionRepository" ref="jedisShiroSessionRepository"/>
<property name="sessionIdGenerator" ref="sessionIdGenerator"/>
</bean>
<!-- 手動操作Session,管理Session(暫時不需要用到)-->
<bean id="customSessionManager" class="com.usersAc.shiro.session.CustomSessionManager">
<property name="shiroSessionRepository" ref="jedisShiroSessionRepository"/>
<property name="customShiroSessionDAO" ref="customShiroSessionDAO"/>
</bean>
<bean id="customSessionListener" class="com.usersAc.shiro.listenter.CustomSessionListener">
<property name="shiroSessionRepository" ref="jedisShiroSessionRepository"/>
</bean>
<bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
<!-- 相隔多久檢查一次session的有效性 -->
<property name="sessionValidationInterval" value="1800000"/>
<!-- session 有效時間為半小時 (毫秒單位)-->
<property name="globalSessionTimeout" value="1800000"/>
<property name="sessionDAO" ref="customShiroSessionDAO"/>
<!-- session 監聽,可以多個。 -->
<property name="sessionListeners"> <!--這裡是監聽類-->
<list>
<ref bean="customSessionListener"/>
</list>
</property>
</bean>
會話管理的配置基本可以了
這裡不貼程式碼了,在我專案裡看會好點,我主要是把配置的問題解釋清楚
如果要使用redis快取記得開啟redisfuwu
最後是Cookie配置了
這部分主要是為了用shiro 的rememberMe來管理Cookie,比如讓客戶端在幾天或半個月記住登入狀態,原本客戶端登入時只需要存sessionId在Cookie裡就行了,如果rememberMe則會存使用者使用者名稱密碼許可權等資訊在客戶端,如果沒有加密的話會不安全,所以下面這部分除了加密都是版式的
spring-shiro.xml
<!-- 使用者資訊記住我功能的相關配置 -->
<bean id="rememberMeCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
<constructor-arg value="v_v-re-baidu"/>
<property name="httpOnly" value="true"/>
<!-- 配置儲存rememberMe Cookie的domain為 一級域名
<property name="domain" value=".itboy.net"/>
-->
<property name="maxAge" value="2592000"/><!-- 30天時間,記住我30天 -->
</bean>
<!-- rememberMe管理器 -->
<bean id="rememberMeManager" class="org.apache.shiro.web.mgt.CookieRememberMeManager">
<!-- rememberMe cookie加密的金鑰 建議每個專案都不一樣,自定義加密-->
<property name="cipherKey"
value="#{T(org.apache.shiro.codec.Base64).decode('3AvVhmFLUs0KTA3Kprsdag==')}"/>
<property name="cookie" ref="rememberMeCookie"/>
</bean>
使用者開啟記住我選項後
UsernamePasswordToken token=new UsernamePasswordToken(usern,passd);
token.setRememberMe(true);
這裡我是寫在Action裡的,如果使用者選擇記住我為true,則setRememberMe(true)
實現後可以在瀏覽器的Cookie裡看到rememberme的cookie,像我上面設的有效時間為30天,則會在30天后才失效