參考資料:http://blog.csdn.net/lishehe/article/details/45223823
-
說在前面:
共享的方式有很多,傳統的做法是通過配置 web 容器,通過容器間 session 的復制達到共享的目的(不推薦),現在常用的做法是通過單獨存儲session達到共享目的,將session存儲到 Mysql 、Memcache、Redis中,等到使用的時候再從中取出來即可。由於各種存儲載體本身的限制,大家可以根據具體情況采用不同實現方案,這里介紹 Redis 的實現方案。 -
非集成下的配置
<!-- 保證實現了 Shiro 內部 lifecycle 函數的 bean 執行 --> <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor" /> <!-- 用戶授權信息Cache(本機內存實現)--> <bean id="cacheManager" class="org.apache.shiro.cache.MemoryConstrainedCacheManager"/> <!-- shiro 的自帶 ehcahe 緩存管理器 --> <bean id="cacheManager" class="org.springframework.cache.ehcache.EhCacheCacheManager"> <property name="cacheManagerConfigFile" value="classpath:config/shiro/ehcache-shiro.xml"/> </bean> <!--自定義Realm --> <bean id="myRealm" class="com.system.shiro.MyRealm"/> <!-- 憑證匹配器 --> <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <property name="realm" ref="myRealm" /> <!-- redis 緩存 --> <property name="cacheManager" ref="cacheManager" /> </bean> <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <property name="securityManager" ref="securityManager" /> <property name="loginUrl" value="/index.jsp" /> <property name="successUrl" value="/loginSuccess.shtml" /> <property name="filterChainDefinitions"> <value> <!-- 靜態資源放行 --> /statics/** = anon /common/** = anon /error/** = anon <!-- 登錄資源放行 --> /toLogin/** = anon /login/** = anon <!-- shiro 自帶登出 --> /logout = logout </value> </property> </bean>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
上面是 shiro 非集群下的配置,DefaultWebSecurityManager 類不需要注入sessionManager 屬性,它會使用默認的 ServletContainerSessionManager 作為sessionManager 。如下圖
setSessionManager 默認設置 servlet容器實現的sessionManager,sessionManager 會管理 session 的創建、刪除等等。如果我們需要讓 session 在集群中共享,就需要替換這個默認的 sessionManager。官網原話如下:
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.
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
我們可以看到如果要用集群,就需要用本地會話,這里 shiro 給我准備了一個默認的native session manager,DefaultWebSessionManager,所以我們要修改 spring 配置文件,注入 DefaultWebSessionManager。我們繼續看DefaultWebSessionManager的源碼,發現其父類 DefaultSessionManager 中有sessionDAO 屬性,這個屬性是真正實現了session儲存的類,這個就是我們自己實現的 redis session的儲存類。
package com.system.shiro; import java.io.Serializable; import java.util.Collection; import java.util.HashSet; import java.util.Set; 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 com.system.utils.RedisManager; import com.system.utils.SerializerUtil; 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<Session> getActiveSessions() { Set<Session> sessions = new HashSet<Session>(); Set<byte[]> 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 = 1800000l; session.setTimeout(expireTime); redisManager.setEx(KEY_PREFIX + session.getId(), session, expireTime); } public void setRedisManager(RedisManager redisManager) { this.redisManager = redisManager; } public RedisManager getRedisManager() { return redisManager; } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
使用到的工具類如下:
RedisManager.java
package com.system.utils; import java.io.Serializable; import java.util.Set; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DataAccessException; import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.core.RedisCallback; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; @Component public class RedisManager { @Autowired private RedisTemplate<Serializable, Serializable> redisTemplate; /** * 過期時間 */ // private Long expire; /** * 添加緩存數據(給定key已存在,進行覆蓋) * @param key * @param obj * @throws DataAccessException */ public <T> void set(String key, T obj) throws DataAccessException{ final byte[] bkey = key.getBytes(); final byte[] bvalue = SerializerUtil.serialize(obj); redisTemplate.execute(new RedisCallback<Void>() { @Override public Void doInRedis(RedisConnection connection) throws DataAccessException { connection.set(bkey, bvalue); return null; } }); } /** * 添加緩存數據(給定key已存在,不進行覆蓋,直接返回false) * @param key * @param obj * @return 操作成功返回true,否則返回false * @throws DataAccessException */ public <T> 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<Boolean>() { @Override public Boolean doInRedis(RedisConnection connection) throws DataAccessException { return connection.setNX(bkey, bvalue); } }); return result; } /** * 添加緩存數據,設定緩存失效時間 * @param key * @param obj * @param expireSeconds 過期時間,單位 秒 * @throws DataAccessException */ public <T> 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<Boolean>() { @Override public Boolean doInRedis(RedisConnection connection) throws DataAccessException { connection.setEx(bkey, expireSeconds, bvalue); return true; } }); } /** * 獲取key對應value * @param key * @return * @throws DataAccessException */ public <T> T get(final String key) throws DataAccessException{ byte[] result = redisTemplate.execute(new RedisCallback<byte[]>() { @Override public byte[] doInRedis(RedisConnection connection) throws DataAccessException { return connection.get(key.getBytes()); } }); if (result == null) { return null; } return SerializerUtil.deserialize(result); } /** * 刪除指定key數據 * @param key * @return 返回操作影響記錄數 */ public Long del(final String key){ if (StringUtils.isEmpty(key)) { return 0l; } Long delNum = redisTemplate.execute(new RedisCallback<Long>() { @Override public Long doInRedis(RedisConnection connection) throws DataAccessException { byte[] keys = key.getBytes(); return connection.del(keys); } }); return delNum; } public Set<byte[]> keys(final String key){ if (StringUtils.isEmpty(key)) { return null; } Set<byte[]> bytesSet = redisTemplate.execute(new RedisCallback<Set<byte[]>>() { @Override public Set<byte[]> doInRedis(RedisConnection connection) throws DataAccessException { byte[] keys = key.getBytes(); return connection.keys(keys); } }); return bytesSet; } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
- 130
- 131
- 132
- 133
- 134
- 135
SerializerUtil.java
package com.system.utils; import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer; /** * 序列化工具類 * @author HandyZcy * */ public class SerializerUtil { private static final JdkSerializationRedisSerializer jdkSerializationRedisSerializer = new JdkSerializationRedisSerializer(); /** * 序列化對象 * @param obj * @return */ public static <T> byte[] serialize(T obj){ try { return jdkSerializationRedisSerializer.serialize(obj); } catch (Exception e) { throw new RuntimeException("序列化失敗!", e); } } /** * 反序列化對象 * @param bytes 字節數組 * @param cls cls * @return */ @SuppressWarnings("unchecked") public static <T> T deserialize(byte[] bytes){ try { return (T) jdkSerializationRedisSerializer.deserialize(bytes); } catch (Exception e) { throw new RuntimeException("反序列化失敗!", e); } } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
整體配置文件如下:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:util="http://www.springframework.org/schema/util" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd " > <description>Shiro安全配置</description> <!-- 分布式 配置參考:http://blog.csdn.net/lishehe/article/details/45223823 --> <!-- 保證實現了 Shiro 內部 lifecycle 函數的 bean 執行 --> <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor" /> <!-- 用戶授權信息Cache(本機內存實現) <bean id="cacheManager" class="org.apache.shiro.cache.MemoryConstrainedCacheManager"/> --> <!-- shiro 的自帶 ehcahe 緩存管理器 <bean id="cacheManager" class="org.springframework.cache.ehcache.EhCacheCacheManager"> <property name="cacheManagerConfigFile" value="classpath:config/shiro/ehcache-shiro.xml"/> </bean> --> <!-- 自定義cacheManager --> <bean id="redisCache" class="com.system.shiro.RedisCache"> <constructor-arg ref="redisManager"></constructor-arg> </bean> <!-- 自定義redisManager-redis --> <bean id="redisCacheManager" class="com.system.shiro.RedisCacheManager"> <property name="redisManager" ref="redisManager" /> </bean> <!--自定義Realm --> <bean id="myRealm" class="com.system.shiro.MyRealm"/> <!-- 憑證匹配器 --> <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <property name="realm" ref="myRealm" /> <property name="sessionMode" value="http" /> <property name="sessionManager" ref="defaultWebSessionManager" /> <!-- redis 緩存 --> <property name="cacheManager" ref="redisCacheManager" /> </bean> <bean id="defaultWebSessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager"> <!-- session存儲的實現 --> <property name="sessionDAO" ref="shiroRedisSessionDAO" /> <!-- sessionIdCookie的實現,用於重寫覆蓋容器默認的JSESSIONID --> <property name="sessionIdCookie" ref="shareSession" /> <!-- 設置全局會話超時時間,默認30分鍾(1800000) --> <property name="globalSessionTimeout" value="1800000" /> <!-- 是否在會話過期后會調用SessionDAO的delete方法刪除會話 默認true --> <property name="deleteInvalidSessions" value="true" /> <!-- 會話驗證器調度時間 --> <property name="sessionValidationInterval" value="1800000" /> <!-- 定時檢查失效的session --> <property name="sessionValidationSchedulerEnabled" value="true" /> </bean> <!-- 通過@Component 注解交由 Spring IOC 管理 <bean id="redisManager" class="com.system.utils.RedisManager"></bean> --> <!-- session會話存儲的實現類 --> <bean id="shiroRedisSessionDAO" class="com.system.shiro.RedisSessionDao"> <property name="redisManager" ref="redisManager"/> </bean> <!-- sessionIdCookie的實現,用於重寫覆蓋容器默認的JSESSIONID --> <bean id="shareSession" class="org.apache.shiro.web.servlet.SimpleCookie"> <!-- cookie的name,對應的默認是 JSESSIONID --> <constructor-arg name="name" value="SHAREJSESSIONID" /> <!-- jsessionId的path為 / 用於多個系統共享jsessionId --> <property name="path" value="/" /> <property name="httpOnly" value="true"/> </bean> <!-- 配置shiro的過濾器工廠類,id- shiroFilter要和我們在web.xml中配置的過濾器一致 --> <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <!-- Shiro的核心安全接口,這個屬性是必須的 --> <property name="securityManager" ref="securityManager" /> <!-- 要求登錄時的鏈接,非必須的屬性,默認會自動尋找Web工程根目錄下的"/login.jsp"頁面 --> <property name="loginUrl" value="/index.jsp" /> <!-- 登錄成功后要跳轉的連接 --> <property name="successUrl" value="/loginSuccess.shtml" /> <!-- 用戶訪問未對其授權的資源時,所顯示的連接 --> <property name="unauthorizedUrl" value="/error/forbidden.jsp" /> <!-- 自定義權限配置:url 過濾在這里做 --> <property name="filterChainDefinitions"> <!-- 參考:http://blog.csdn.net/jadyer/article/details/12172839 --> <!-- Shiro驗證URL時,URL匹配成功便不再繼續匹配查找(所以要注意配置文件中的URL順序,尤其在使用通配符時)故filterChainDefinitions的配置順序為自上而下,以最上面的為准 --> <!-- Pattern里用到的是兩顆星,這樣才能實現任意層次的全匹配 --> <value> <!-- 靜態資源放行 --> /statics/** = anon /common/** = anon /error/** = anon <!-- 登錄資源放行 --> /toLogin/** = anon /login/** = anon <!-- shiro 自帶登出 --> /logout = logout <!-- 表示用戶必需已通過認證,並擁有 superman 角色 && superman:role:list 權限才可以正常發起'/role'請求--> /role/** = authc,roles[superman],perms[superman:role:list] /right/** = authc,roles[superman],perms[superman:right:list] /manager/preEditPwd = authc /manager/editUserBase = authc <!-- 表示用戶必需已通過認證,並擁有 superman 角色 && superman:manager:list 才可以正常發起'/manager'請求 --> /manager/** = authc,roles[superman],perms[superman:manager:list] /** = authc </value> </property>