Spring boot + shiro + redis 實現session共享(偽單點登入)
為實現Web應用的分散式叢集部署,要解決登入session的統一。本文利用shiro做許可權控制,redis做session儲存,結合spring boot快速配置實現session共享。注意本文未解決跨域的問題。不過對於一般的情況能夠很好的起到作用,具體已經在不同埠上進行了測試。
我們先從配置說起:
1.引入相關依賴
引入shiro相關的依賴還有Redis的依賴
<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>${shiro-version}</version> </dependency> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>2.7.2</version> </dependency>
2.要使用Redis,我們要對Redis進行配置
jedis :
pool :
host : 127.0.0.1
port : 6379
password:
maxTotal: 100
maxIdle: 10
maxWaitMillis : 100000
對連線池進行配置:
JedisPoolConfig config = new JedisPoolConfig(); config.setMaxTotal(200); config.setMaxIdle(50); config.setMinIdle(8);//設定最小空閒數 config.setMaxWaitMillis(10000); config.setTestOnBorrow(true); config.setTestOnReturn(true); //Idle時進行連線掃描 config.setTestWhileIdle(true); //表示idle object evitor兩次掃描之間要sleep的毫秒數 config.setTimeBetweenEvictionRunsMillis(30000); //表示idle object evitor每次掃描的最多的物件數 config.setNumTestsPerEvictionRun(10); //表示一個物件至少停留在idle狀態的最短時間,然後才能被idle object evitor掃描並驅逐;這一項只有在timeBetweenEvictionRunsMillis大於0時才有意義 config.setMinEvictableIdleTimeMillis(60000); jedisPool = new JedisPool(config, "127.0.0.1", 6379, 10000);
3. 在專案中通過ShiroConfiguration對shiro進行配置,我們知道 shiro本身是自帶預設的session管理的,但是如果我們要實現session的共享和快取的話,首先就需要對SessionDao進行重寫,如果說要優化session的存取更新頻率的話還可以對DefaultWebSessionManager進行重寫。(這個具體在後面講)
同時我們還需要設定一個cookie的名稱,用來與shiro預設的JSessionID 進行區分。
由於我們還要設定快取管理,所以需要 定義JedisCache 使快取的方式通過Redis來實現。
import com.xxx.util.shiro_redis.CustomSessionManager; import com.xxx.util.shiro_redis.JedisCacheManager; import com.xxx.util.shiro_redis.RedisSessionDao; import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.session.mgt.SessionManager; import org.apache.shiro.session.mgt.eis.SessionDAO; import org.apache.shiro.spring.LifecycleBeanPostProcessor; import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor; import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.apache.shiro.web.servlet.SimpleCookie; import org.apache.shiro.web.session.mgt.DefaultWebSessionManager; import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.LinkedHashMap; import java.util.Map; /** * @Author yunfd */ @Configuration public class ShiroConfiguration { //這裡就是會話管理的操作類 @Bean public SessionDAO sessionDAO() { return new RedisSessionDao(); } //這裡需要設定一個cookie的名稱 原因就是會跟原來的session的id值重複的 @Bean public SimpleCookie simpleCookie() { SimpleCookie simpleCookie = new SimpleCookie("REDISSESSION"); return simpleCookie; } @Bean public JedisCacheManager jedisCacheManager() { return new JedisCacheManager(); } @Bean(name = "sessionManager") public DefaultWebSessionManager configWebSessionManager(){ CustomSessionManager manager = new CustomSessionManager(); manager.setSessionIdCookie(simpleCookie()); manager.setSessionDAO(sessionDAO());// 設定SessionDao manager.setDeleteInvalidSessions(true);// 刪除過期的session manager.setSessionValidationSchedulerEnabled(true);// 是否定時檢查session return manager; } @Bean(name = "securityManager") public SecurityManager securityManager(ShiroRealm shiroRealm, SessionManager sessionManager) { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(shiroRealm); // 注入快取管理器; securityManager.setCacheManager(jedisCacheManager()); // 會話管理 securityManager.setSessionManager(sessionManager); return securityManager; } @Bean public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) { ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean(); shiroFilter.setSecurityManager(securityManager); shiroFilter.setLoginUrl("/index.html"); shiroFilter.setUnauthorizedUrl("/404.html"); Map<String, String> filterMap = new LinkedHashMap<>(); filterMap.put("/", "anon"); filterMap.put("/**", "authc"); shiroFilter.setFilterChainDefinitionMap(filterMap); return shiroFilter; } @Bean(name = "lifecycleBeanPostProcessor") public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() { return new LifecycleBeanPostProcessor(); } @Bean public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() { DefaultAdvisorAutoProxyCreator proxyCreator = new DefaultAdvisorAutoProxyCreator(); proxyCreator.setProxyTargetClass(true); return proxyCreator; } @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) { AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor(); advisor.setSecurityManager(securityManager); return advisor; } }
上面提到的要重寫的部分:
1.JedisCache
import org.apache.commons.lang3.SerializationUtils;
import org.apache.shiro.cache.Cache;
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.io.Serializable;
import java.util.*;
public class JedisCache<K,V> implements Cache<K, V> ,Serializable{
private static final Logger LOGGER = LoggerFactory.getLogger(JedisCache.class);
private static final String PREFIX = "SHIRO_SESSION_ID";
private static JedisPool jedisPool;
static {
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(200);
config.setMaxIdle(50);
config.setMinIdle(8);//設定最小空閒數
config.setMaxWaitMillis(10000);
config.setTestOnBorrow(true);
config.setTestOnReturn(true);
//Idle時進行連線掃描
config.setTestWhileIdle(true);
//表示idle object evitor兩次掃描之間要sleep的毫秒數
config.setTimeBetweenEvictionRunsMillis(30000);
//表示idle object evitor每次掃描的最多的物件數
config.setNumTestsPerEvictionRun(10);
//表示一個物件至少停留在idle狀態的最短時間,然後才能被idle object evitor掃描並驅逐;這一項只有在timeBetweenEvictionRunsMillis大於0時才有意義
config.setMinEvictableIdleTimeMillis(60000);
jedisPool = new JedisPool(config, "127.0.0.1", 6379, 10000);
}
private byte[] getByteKey(K k){
if(k instanceof String){
String key = PREFIX+k;
return key.getBytes();
}else {
return SerializationUtils.serialize((Serializable) k);
}
}
@Override
public int size() {
Jedis jedis = jedisPool.getResource();
Long size = jedis.dbSize();
return size.intValue();
}
@Override
public Set<K> keys() {
Jedis jedis = jedisPool.getResource();
Set<byte[]> bytes = jedis.keys( (PREFIX + new String("*")).getBytes());
Set<K> keys = new HashSet<>();
if(bytes!=null){
for (byte[] b: bytes) {
keys.add(SerializationUtils.deserialize(b));
}
}
JedisUtil.closeJedis(jedis);
return keys;
}
@Override
public Collection<V> values() {
Set<K> keys = this.keys();
Jedis jedis = jedisPool.getResource();
List<V> lists = new ArrayList<>();
for (K k:keys) {
byte[] bytes = jedis.get(getByteKey(k));
lists.add(SerializationUtils.deserialize(bytes));
}
JedisUtil.closeJedis(jedis);
return lists;
}
@Override
public void clear() {
jedisPool.getResource().flushDB();
}
@Override
public V put(K k, V v) {
LOGGER.info("key---->"+k+"value---->"+v);
Jedis jedis = jedisPool.getResource();
jedis.set(getByteKey(k), SerializationUtils.serialize((Serializable) v));
jedis.expire(getByteKey(k),10000);
byte[] bytes = jedis.get(SerializationUtils.serialize(getByteKey(k)));
JedisUtil.closeJedis(jedis);
if(bytes==null){
return null;
}
return SerializationUtils.deserialize(bytes);
}
@Override
public V get(K k) {
LOGGER.info("get------>key="+k);
if(k==null){
return null;
}
//System.out.println(k);
Jedis jedis = jedisPool.getResource();
byte[] bytes = jedis.get(getByteKey(k));
JedisUtil.closeJedis(jedis);
if(bytes==null){
return null;
}
return SerializationUtils.deserialize(bytes);
}
@Override
public V remove(K k) {
Jedis jedis = jedisPool.getResource();
byte[] bytes = jedis.get(getByteKey(k));
jedis.del(getByteKey(k));
JedisUtil.closeJedis(jedis);
if(bytes==null){
return null;
}
return SerializationUtils.deserialize(bytes);
}
}
2.JedisCacheManager 用來生成JedisCache
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.apache.shiro.cache.CacheManager;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
public class JedisCacheManager implements CacheManager {
private final ConcurrentMap<String, Cache> caches = new ConcurrentHashMap<String, Cache>();
//cache
@Override
public <K, V> Cache<K, V> getCache(String s) throws CacheException {
Cache cache = caches.get(s);
if(cache == null){
cache = new JedisCache();
caches.put(s,cache);
}
return cache;
}
}
3.重頭戲,RedisSessionDao 對SessionDao進行重寫
import org.apache.commons.lang3.SerializationUtils;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.eis.CachingSessionDAO;
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.io.Serializable;
public class RedisSessionDao extends CachingSessionDAO {
private static final String PREFIX = "SHIRO_SESSION_ID";
private static final int EXPRIE = 10000;
private static final Logger LOGGER = LoggerFactory.getLogger(RedisSessionDao.class);
private static JedisPool jedisPool;
static {
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(200);
config.setMaxIdle(50);
config.setMinIdle(8);//設定最小空閒數
config.setMaxWaitMillis(10000);
config.setTestOnBorrow(true);
config.setTestOnReturn(true);
//Idle時進行連線掃描
config.setTestWhileIdle(true);
//表示idle object evitor兩次掃描之間要sleep的毫秒數
config.setTimeBetweenEvictionRunsMillis(30000);
//表示idle object evitor每次掃描的最多的物件數
config.setNumTestsPerEvictionRun(10);
//表示一個物件至少停留在idle狀態的最短時間,然後才能被idle object evitor掃描並驅逐;這一項只有在timeBetweenEvictionRunsMillis大於0時才有意義
config.setMinEvictableIdleTimeMillis(60000);
jedisPool = new JedisPool(config, "127.0.0.1", 6379, 10000);
}
@Override
protected Serializable doCreate(Session session) {
LOGGER.info("--------doCreate-----");
Serializable serializable = this.generateSessionId(session);
assignSessionId(session, serializable);
Jedis jedis = jedisPool.getResource();
session.setTimeout(EXPRIE*1000);
/*jedis.set(getByteKey(serializable),SerializationUtils.serialize((Serializable)session));
jedis.expire(SerializationUtils.serialize(getByteKey(serializable)),EXPRIE);*/
jedis.setex(getByteKey(serializable),EXPRIE,SerializationUtils.serialize((Serializable)session) );
JedisUtil.closeJedis(jedis);
return serializable;
}
@Override
protected Session doReadSession(Serializable serializable) {
if(serializable ==null){
return null;
}
LOGGER.info("--------doReadSession-----");
Jedis jedis = jedisPool.getResource();
Session session = null;
byte[] s = jedis.get(getByteKey(serializable));
if (s != null) {
session = SerializationUtils.deserialize(s);
jedis.expire((PREFIX+serializable).getBytes(),EXPRIE);
}
//判斷是否有會話 沒有返回NULL
if(session==null){
return null;
}
JedisUtil.closeJedis(jedis);
return session;
}
private byte[] getByteKey(Object k){
if(k instanceof String){
String key = PREFIX+k;
return key.getBytes();
}else {
return SerializationUtils.serialize((Serializable) k);
}
}
@Override
protected void doUpdate(Session session) {
LOGGER.info("--------doUpdate-----");
if(session==null){
return ;
}
//((WebSessionKey)sessionKey)
Jedis jedis = jedisPool.getResource();
session.setTimeout(EXPRIE*1000);
/*jedis.set(getByteKey(session.getId()),SerializationUtils.serialize((Serializable)session));
jedis.expire(SerializationUtils.serialize((PREFIX+session.getId())),EXPRIE);*/
jedis.setex(getByteKey(session.getId()),EXPRIE,SerializationUtils.serialize((Serializable)session) );
}
@Override
protected void doDelete(Session session) {
LOGGER.info("--------doDelete-----");
Jedis jedis = jedisPool.getResource();
jedis.del(getByteKey(session.getId()));
JedisUtil.closeJedis(jedis);
}
}
4.JedisUtil 用來開關連線池
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
public class JedisUtil {
private static JedisPool jedisPool;
static {
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(200);
config.setMaxIdle(50);
config.setMinIdle(8);//設定最小空閒數
config.setMaxWaitMillis(10000);
config.setTestOnBorrow(true);
config.setTestOnReturn(true);
//Idle時進行連線掃描
config.setTestWhileIdle(true);
//表示idle object evitor兩次掃描之間要sleep的毫秒數
config.setTimeBetweenEvictionRunsMillis(30000);
//表示idle object evitor每次掃描的最多的物件數
config.setNumTestsPerEvictionRun(10);
//表示一個物件至少停留在idle狀態的最短時間,然後才能被idle object evitor掃描並驅逐;這一項只有在timeBetweenEvictionRunsMillis大於0時才有意義
config.setMinEvictableIdleTimeMillis(60000);
jedisPool = new JedisPool(config, "127.0.0.1", 6379, 10000);
}
public static Jedis getJedis(){
return jedisPool.getResource();
}
public static void closeJedis(Jedis jedis){
jedis.close();
}
}
5.如果想要對 doReadSession 和 doUpdate 次數過於頻繁進行優化的話,我們通過shiroConfiguration中的SessionManager進行追蹤, 發現是DefaultWebSessionManager類,而它又是繼承自DefaultSessionManager,其中retrieveSession方法中進行doReadSession,onChange方法進行doUpdate。對這兩個方法進行重寫,即可進行優化。在這裡retrieveSession方法中我先將SessionKey強轉成WebSessionKey,從中取出request。第一次readSession我們把Session寫入request中,那麼之後如果有重複操作的時候 就可以從request中將最先寫入的Session給讀出來。從而對 doReadSession的次數進行優化。 doUpdate 次數過多的原因基本來源於 timeout的問題,所以可以在onChange 方法中對timeout進行一個限制 ,從而介紹 doUpdate的次數。
import org.apache.shiro.session.Session;
import org.apache.shiro.session.UnknownSessionException;
import org.apache.shiro.session.mgt.SessionKey;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.web.session.mgt.WebSessionKey;
import javax.servlet.ServletRequest;
import java.io.Serializable;
public class CustomSessionManager extends DefaultWebSessionManager {
@Override
protected Session retrieveSession(SessionKey sessionKey) throws UnknownSessionException {
Serializable sessionId = getSessionId(sessionKey);
ServletRequest request = null;
if(sessionKey instanceof WebSessionKey)
{
request = ((WebSessionKey)sessionKey).getServletRequest();
}
if(request!=null && sessionId!=null){
Session session = (Session)request.getAttribute(sessionId.toString());
if(session !=null){
return session;
}
}
Session session = super.retrieveSession(sessionKey);
if(request!=null && sessionId != null) {
request.setAttribute(sessionId.toString(),session);
}
return session;
}
@Override
protected void onChange(Session session) {
// if(session.getTimeout()>10000*500)
// return ;
super.onChange(session);
}
}
以上就是與Session共享相關的內容 。