1. 程式人生 > >shiro原始碼分析篇5:結合redis實現session跨域

shiro原始碼分析篇5:結合redis實現session跨域

相信大家對session跨域也比較瞭解了。以前單臺伺服器session本地快取就可以了,現在分散式後,session集中管理,那麼用redis來管理是一個非常不錯的選擇。

在結合redis做session快取的時候,也遇到了很多坑,不過還算是解決了。

和上篇講述一樣,實現自定義快取,需要實現兩個介面Cache,CachaManager。
RedisCache.java

package com.share1024.cache;

import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.apache.shiro.util.CollectionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.*;

/**
 * @author : yesheng
 * @Description :
 * @Date : 2017/10/22
 */
public class RedisCache<K,V> implements Cache<K,V> { private Logger logger = LoggerFactory.getLogger(RedisCache.class); private final String SHIRO_SESSION="shiro_session:"; public String getKey(K key){ return SHIRO_SESSION + key; } @Override public V get
(K key) throws CacheException { logger.info("get--從redis中獲取:{}",key); Object o = SerializeUtils.deserialize(RedisUtil.getInstance().get(getKey(key).getBytes())); if(o == null){ return null; } return (V)o; } @Override public V put(K key, V value
) throws CacheException { logger.info("put--儲存到redis,key:{},value:{}",key,value); get(key); byte[] b = SerializeUtils.serialize(value); Object o = SerializeUtils.deserialize(b); RedisUtil.getInstance().set(getKey(key).getBytes(),SerializeUtils.serialize(value)); return get(key); } @Override public V remove(K key) throws CacheException { logger.info("remove--刪除key:{}",key); V value = get(key); RedisUtil.getInstance().del(getKey(key).getBytes()); return value; } @Override public void clear() throws CacheException { logger.info("clear--清空快取"); RedisUtil.getInstance().del((SHIRO_SESSION + "*").getBytes()); } @Override public int size() { logger.info("size--獲取快取大小"); return keys().size(); } @Override public Set<K> keys() { logger.info("keys--獲取快取大小keys"); return (Set<K>) RedisUtil.getInstance().keys(SHIRO_SESSION + "*"); } @Override public Collection<V> values() { logger.info("values--獲取快取值values"); Set<K> keys = keys(); if (!CollectionUtils.isEmpty(keys)) { List<V> values = new ArrayList<V>(keys.size()); for (K key : keys) { @SuppressWarnings("unchecked") V value = get(key); if (value != null) { values.add(value); } } return Collections.unmodifiableList(values); } else { return Collections.emptyList(); } } }

RedisCacheManager.java

package com.share1024.cache;

import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.apache.shiro.cache.CacheManager;

import java.util.concurrent.ConcurrentHashMap;

/**
 * @author : yesheng
 * @Description :
 * @Date : 2017/10/22
 */
public class RedisCacheManager implements CacheManager{

    private final ConcurrentHashMap<String,Cache> caches = new ConcurrentHashMap<String, Cache>();

    public <K, V> Cache<K, V> getCache(String name) throws CacheException {

        Cache cache = caches.get(name);

        if(cache == null){
            cache = new RedisCache<K,V>();
            caches.put(name,cache);
        }
        return cache;
    }
}

RedisUtil.java太長 大家參考https://github.com/smallleaf/cacheWeb
SerializeUtils.java

package com.share1024.cache;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.*;

/**
 * @author : yesheng
 * @Description :
 * @Date : 2017/10/22
 */
public class SerializeUtils {
    private static Logger logger = LoggerFactory.getLogger(SerializeUtils.class);

    /**
     * 反序列化
     * @param bytes
     * @return
     */
    public static Object deserialize(byte[] bytes) {

        Object result = null;

        if (isEmpty(bytes)) {
            return null;
        }

        try {
            ByteArrayInputStream byteStream = new ByteArrayInputStream(bytes);
            try {
                ObjectInputStream objectInputStream = new ObjectInputStream(byteStream);
                try {
                    result = objectInputStream.readObject();
                }
                catch (ClassNotFoundException ex) {
                    throw new Exception("Failed to deserialize object type", ex);
                }
            }
            catch (Throwable ex) {
                throw new Exception("Failed to deserialize", ex);
            }
        } catch (Exception e) {
            logger.error("Failed to deserialize",e);
        }
        return result;
    }

    public static boolean isEmpty(byte[] data) {
        return (data == null || data.length == 0);
    }

    /**
     * 序列化
     * @param object
     * @return
     */
    public static byte[] serialize(Object object) {

        byte[] result = null;

        if (object == null) {
            return new byte[0];
        }
        try {
            ByteArrayOutputStream byteStream = new ByteArrayOutputStream(128);
            try  {
                if (!(object instanceof Serializable)) {
                    throw new IllegalArgumentException(SerializeUtils.class.getSimpleName() + " requires a Serializable payload " +
                            "but received an object of type [" + object.getClass().getName() + "]");
                }
                ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteStream);
                objectOutputStream.writeObject(object);
                objectOutputStream.flush();
                result =  byteStream.toByteArray();
            }
            catch (Throwable ex) {
                throw new Exception("Failed to serialize", ex);
            }
        } catch (Exception ex) {
            logger.error("Failed to serialize",ex);
        }
        return result;
    }

}

修改spring-shiro.xml

 <bean id="redisCacheManager" class="com.share1024.cache.RedisCacheManager"></bean>
   <!-- 安全管理器 -->
    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <property name="realm" ref="userRealm"/>
        <property name="sessionManager" ref="sessionManager"/>
        <!--<property name="cacheManager" ref="cacheManagerShiro"/>-->
        <property name="cacheManager" ref="redisCacheManager"/>
    </bean>

即可。
如何測試跨域,大家用nginx負載均衡一下就可以了。一臺伺服器登入,其他伺服器就不用再登入,session從redis中獲取。

在網上看到很多基本上是還要重寫sessionDao。從前面幾篇分析來看,如果只是要換個快取地方是完全沒有必要的。實現Cache,CacheManager介面即可。

當然如果有其它的業務要求就看情況實現sessionDao。

巨坑來了。
1、Redis儲存物件
序列化方式,來儲存
2、SimpleSession:shiro存入快取的都是SimpleSession。來看看這個類:

public class SimpleSession implements ValidatingSession, Serializable {

    // Serialization reminder:
    // You _MUST_ change this number if you introduce a change to this class
    // that is NOT serialization backwards compatible.  Serialization-compatible
    // changes do not require a change to this number.  If you need to generate
    // a new number in this case, use the JDK's 'serialver' program to generate it.
    private static final long serialVersionUID = -7125642695178165650L;

    //TODO - complete JavaDoc
    private transient static final Logger log = LoggerFactory.getLogger(SimpleSession.class);

    protected static final long MILLIS_PER_SECOND = 1000;
    protected static final long MILLIS_PER_MINUTE = 60 * MILLIS_PER_SECOND;
    protected static final long MILLIS_PER_HOUR = 60 * MILLIS_PER_MINUTE;

    //serialization bitmask fields. DO NOT CHANGE THE ORDER THEY ARE DECLARED!
    static int bitIndexCounter = 0;
    private static final int ID_BIT_MASK = 1 << bitIndexCounter++;
    private static final int START_TIMESTAMP_BIT_MASK = 1 << bitIndexCounter++;
    private static final int STOP_TIMESTAMP_BIT_MASK = 1 << bitIndexCounter++;
    private static final int LAST_ACCESS_TIME_BIT_MASK = 1 << bitIndexCounter++;
    private static final int TIMEOUT_BIT_MASK = 1 << bitIndexCounter++;
    private static final int EXPIRED_BIT_MASK = 1 << bitIndexCounter++;
    private static final int HOST_BIT_MASK = 1 << bitIndexCounter++;
    private static final int ATTRIBUTES_BIT_MASK = 1 << bitIndexCounter++;

    // ==============================================================
    // NOTICE:
    //
    // The following fields are marked as transient to avoid double-serialization.
    // They are in fact serialized (even though 'transient' usually indicates otherwise),
    // but they are serialized explicitly via the writeObject and readObject implementations
    // in this class.
    //
    // If we didn't declare them as transient, the out.defaultWriteObject(); call in writeObject would
    // serialize all non-transient fields as well, effectively doubly serializing the fields (also
    // doubling the serialization size).
    //
    // This finding, with discussion, was covered here:
    //
    // http://mail-archives.apache.org/mod_mbox/shiro-user/201109.mbox/%[email protected]%3E
    //
    // ==============================================================
    private transient Serializable id;
    private transient Date startTimestamp;
    private transient Date stopTimestamp;
    private transient Date lastAccessTime;
    private transient long timeout;
    private transient boolean expired;
    private transient String host;
    private transient Map<Object, Object> attributes;

大家發現沒有SimpleSession的屬性已經被transient修飾,序列化的時候應該不會被序列化進去啊。
我當時糾結了很久。最後發現SimpleSession,寫了這兩個方法writeObject(ObjectOutputStream)和readObject(ObjectInputStream)

    @SuppressWarnings({"unchecked"})
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        in.defaultReadObject();
        short bitMask = in.readShort();

        if (isFieldPresent(bitMask, ID_BIT_MASK)) {
            this.id = (Serializable) in.readObject();
        }
        if (isFieldPresent(bitMask, START_TIMESTAMP_BIT_MASK)) {
            this.startTimestamp = (Date) in.readObject();
        }
        if (isFieldPresent(bitMask, STOP_TIMESTAMP_BIT_MASK)) {
            this.stopTimestamp = (Date) in.readObject();
        }
        if (isFieldPresent(bitMask, LAST_ACCESS_TIME_BIT_MASK)) {
            this.lastAccessTime = (Date) in.readObject();
        }
        if (isFieldPresent(bitMask, TIMEOUT_BIT_MASK)) {
            this.timeout = in.readLong();
        }
        if (isFieldPresent(bitMask, EXPIRED_BIT_MASK)) {
            this.expired = in.readBoolean();
        }
        if (isFieldPresent(bitMask, HOST_BIT_MASK)) {
            this.host = in.readUTF();
        }
        if (isFieldPresent(bitMask, ATTRIBUTES_BIT_MASK)) {
            this.attributes = (Map<Object, Object>) in.readObject();
        }
    }
    private void writeObject(ObjectOutputStream out) throws IOException {
        out.defaultWriteObject();
        short alteredFieldsBitMask = getAlteredFieldsBitMask();
        out.writeShort(alteredFieldsBitMask);
        if (id != null) {
            out.writeObject(id);
        }
        if (startTimestamp != null) {
            out.writeObject(startTimestamp);
        }
        if (stopTimestamp != null) {
            out.writeObject(stopTimestamp);
        }
        if (lastAccessTime != null) {
            out.writeObject(lastAccessTime);
        }
        if (timeout != 0l) {
            out.writeLong(timeout);
        }
        if (expired) {
            out.writeBoolean(expired);
        }
        if (host != null) {
            out.writeUTF(host);
        }
        if (!CollectionUtils.isEmpty(attributes)) {
            out.writeObject(attributes);
        }
    }

雖然屬性已經被transient,但自定義序列化方法,又讓他們重新序列化了。在這裡序列化的好處,就是我們可以做一些自定義操作,比如校驗資訊,序列化到本地,等等。看看上面為null就不序列化,這樣可以節省記憶體空間等等。

3、序列化報錯
我在進行測試時,發現登入成功後,登入的資訊一直儲存不到redis中。導致每次都要登入,找了很久都沒有發現原因。最終一步步debug終於發現。在做登入驗證的User類沒有實現Serializable。序列化一直報錯只是異常沒有丟擲。所以在做序列化有關工作時,要看看相關類是否能夠序列化。

這5篇從簡單登入開始,一直到現在能夠自定義快取,用redis來做session快取等等。
shiro篇已經過去。

談到session跨域。
等有時間我想再寫三篇,一篇是tomcat實現session跨域處理。一篇是分析spring-session實現原理。一篇是spring-session如何與shiro結合。


菜鳥不易,望有問題指出,共同進步。