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結合。
菜鳥不易,望有問題指出,共同進步。