什么是Apache Shiro?
Apache Shiro是一个功能强大且易于使用的Java安全框架,为开发人员提供了一个直观而全面的解决方案,用于身份验证、授权、加密和会话管理。
由于本篇主要解决shiro管理下session共享问题,shiro基本功能和用法就不赘述了.
Shiro的架构
session共享主要自定义 session Manager ,session DAO , cacheManager 和 cache来实现
- sessionManager : shiro 会话的管理器,网络会话使用WebSessionManager
- sessionDao: 存取会话的DAO层实现
- cacheManager: 缓存管理器
- cache: 负责缓存存取的DAO层实现
自定义类的意义
sessionManager和sessionDao
由于sessionManager负责管理sessionDao ,想要自定义 sessionDao 必须先自定义 sessionManager
sessionDao是实现session共享的核心,通过自定义该类,来实现用redis管理session
cacheManager和cahe
cacheManager和cahe和上面两者的关系基本相同,前者是管理器,后者是dao层实现;
shiro框架对身份认证和权限认证有缓存功能,在配置realm时可以设置是否开启此功能(realm主要就是负责身份认证和权限认证的工作),由于realm的功能原因,基本都要去数据库或网络IO,所有使用缓存是十分有用的,在这里我们也用redis进行缓存的管理;
结合配置分析
话不多说上配置文件
我们使用spirng注解方式来配置shiro
-
SecurityManager
往大的将融合redis就是改造SecurityManager,通过setCacheManager()和setSessionManager()方法分别设置缓存管理器和会话管理器,配置如下:
@Bean("securityManager")
public DefaultWebSecurityManager getManager() {
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
manager.setRealm(getRealm());
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
manager.setSubjectDAO(subjectDAO);
//以上部分未做改动,重点在下面两方法
//redis缓存和session共享
if("y".equals(redis)){
// 设置缓存,该缓存使用不使用,由realm的配置决定
System.out.println("使用redis进行session共享");
manager.setCacheManager(getRedisCacheManager());
manager.setSessionManager(getDefaultWebSessionManager());
}
return manager;
}
将该类设置进ShiroFilterFactoryBean中
-
会话管理器
- SessionManager:
@Bean(name = "sessionManager")
public DefaultWebSessionManager getDefaultWebSessionManager() {
//自定义sessionManager为了解决浏览器访问时网址后面添加;SESSIONID=xxxx的问题
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager(){
//参考https://blog.csdn.net/yyf314922957/article/details/51038322
@Override
protected void onStart(Session session, SessionContext context) {
super.onStart(session, context);
ServletRequest request = WebUtils.getRequest(context);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE,ShiroHttpServletRequest.COOKIE_SESSION_ID_SOURCE);
}
};
sessionManager.setDeleteInvalidSessions(true);
sessionManager.setSessionDAO(getRedisSessionDao());
sessionManager.setSessionIdCookie(getSimpleCookie());
//相隔多久检查一次session的有效性
sessionManager.setSessionValidationInterval(validate);
//session 有效期
sessionManager.setGlobalSessionTimeout(timeout);
return sessionManager;
}
- SessionDao
//配置基于redis的SessionDao
@Bean("redisSessionDao")
public MyRedisSessionDao getRedisSessionDao() {
MyRedisSessionDao sessionDAO = new MyRedisSessionDao();
return sessionDAO;
}
- SimpleCookie
可以注意到SessionManager通过setSessionIdCookie()方法设置了个SimpleCookie,该类解决nginx分发访问时不同tomcat的sessionID不一致导致获取不到session的问题
//解决nginx分发时出现There is no session with id ....的session不一致,找不到的问题
@Bean("simpleCookie")
public SimpleCookie getSimpleCookie (){
SimpleCookie simpleCookie = new SimpleCookie(SHIRO_SESSION);
simpleCookie.setPath("/");
return simpleCookie;
}
-
缓存管理器
- CacheManager
而cache多例不需要交由spring管理,只需配置CacheManager
//配置缓存
@Bean("redisCacheManager")
public MyRedisCacheManager getRedisCacheManager() {
MyRedisCacheManager redisCacheManager = new MyRedisCacheManager();
return redisCacheManager;
}
各类实现代码
由以上配置可以看出需要实现的类其实只有三个,分别为SessionDao,CacheManager,Cache.代码如下:
-
SessionDao
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.UnknownSessionException;
import org.apache.shiro.session.mgt.eis.AbstractSessionDAO;
import org.springframework.beans.factory.annotation.Autowired;
import java.io.Serializable;
import java.util.*;
public class MyRedisSessionDao extends AbstractSessionDAO {
private Object lock = new Object();
public static final String SESSION_KEY = "session_key";
//基于RedisTemplate集成工具类
@Autowired
private RedisUtil redisUtil;
//删除session,退出时会调用到该方法
@Override
public void delete(Session session) {
if(session == null || session.getId() == null){
System.out.println("Session is null");
return;
}
redisUtil.hdel(SESSION_KEY,session.getId().toString());
}
//获得sessin集合
@Override
public Collection<Session> getActiveSessions() {
List<Session> list = new ArrayList<>();
List<Object> objects = redisUtil.hgetList(SESSION_KEY);
for (Object object : objects) {
Session session = null;
Base64 base64 = new Base64();
try {
byte[] decode = base64.decode((String) object);
session = (Session) JSONUtil.unSerialize(decode);
} catch (Exception e) {
e.printStackTrace();
}
list.add(session);
}
return list;
}
//更新session
@Override
public void update(Session session) throws UnknownSessionException {
this.saveSession(session);
}
private void saveSession(Session session){
if(session == null || session.getId() == null){
System.out.println("Session is null");
return;
}
//添加进redis
Base64 base64 = new Base64();
String sessionStr = base64.encodeToString(JSONUtil.serialize(session));
redisUtil.hset(SESSION_KEY,session.getId().toString(),sessionStr);
}
//shiro框架创建session的方法,shiro通过doReadSession得到空的session得知当前会话还没有session就会去创建session
@Override
protected Serializable doCreate(Session session) {
Serializable sessionId = generateSessionId(session);
assignSessionId(session, sessionId);
//添加进redis
this.saveSession(session);
return sessionId;
}
//获取shiro框架session的方法
@Override
protected Session doReadSession(Serializable sessionId) {
Session session = null;
String sessionStr = (String) redisUtil.hget(SESSION_KEY, sessionId.toString());
if(StringUtils.isNotEmpty(sessionStr)){
Base64 base64 = new Base64();
try {
synchronized (lock){
byte[] decode = base64.decode(sessionStr);
session = (Session) JSONUtil.unSerialize(decode);
}
} catch (Exception e) {
e.printStackTrace();
}
}
System.out.println("get sessionId: "+sessionId + "session: "+ session);
return session;
}
}
说明
- RedisUtil
这个类基于RedisTemplate集成的工具类,对RedisTemplate的简单封装,主要功能就是基于redis对数据进行增删改查,由于本篇关注点不在此,就不展开了.如何配置RedisTemplate请自行百度. -
Base64
为什么要使用Base64转码呢?说来话长,首先是这个Session实现类的问题,shiro对session的默认实现类是org.apache.shiro.session.mgt.SimpleSession这个类.我使用RedisTemplate<String,Object>来进行存取时(内部使用jackson来实现object与string相互转转换,redis只能存储字符串),会报错如下图:
究其原因,可能是SimpleSession中有个isValid()的方法,却没有valid字段,导致jackson创建类时报错.
解决这个问题的方法有很多种,我决定绕过jackson,自己实现session与string互相转换,于是使用java的对象序列化,转换成字节数组,再用base64转换成字符串,如上面代码的saveSession()方法,其中JSONUtil的相关代码如下:
/**
序列化对象
@param object
@return
*/
public static byte[] serialize(Object object) {
ObjectOutputStream oos = null;
ByteArrayOutputStream baos = null;
try {
if (object != null){
baos = new ByteArrayOutputStream();
oos = new ObjectOutputStream(baos);
oos.writeObject(object);
return baos.toByteArray();
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
反序列化对象
@param bytes
@return
*/
public static Object unSerialize(byte[] bytes) throws Exception{
ByteArrayInputStream bais = null;
if (bytes != null && bytes.length > 0){
bais = new ByteArrayInputStream(bytes);
ObjectInputStream ois = new ObjectInputStream(bais);
return ois.readObject();
}
return null;
}
本以为万事大吉却还是报错,错误的大体问题是在doReadSession()方法中unSerialize反序列化时出现错误,内部原因暂时不知.但经过排查,主要是由于doReadSession存在着并发调用,在并发情况下java对象流处理时会出错!?本人并没有做更多的测试,同时百度无果.于是就加了个锁解决并发报错的问题,希望有大佬能解答这个疑惑!
ps
为什么不选则其他JSON工具类而使用java序列化呢?
答:实在是束手无策,诸如fastjson,gson都会报错,SimpleSession这个类真滴有毒,恨不得自定义,但是根据时下流行奥卡姆剃刀原则,还是惰性一点好,搞不好又是一堆bug.
而这个类的一些属性被transient关键字所修饰,如下图:
最大的问题就是这个类,剩下的两个类就简单了;
-
CacheManager
import com.zoan.jdcbj.utils.RedisUtil;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.apache.shiro.cache.CacheManager;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
public class MyRedisCacheManager implements CacheManager {
@Autowired
private RedisUtil redisUtil;
private final ConcurrentMap<String, Cache> caches = new ConcurrentHashMap<>();
@Override
public <K, V> Cache<K, V> getCache(String name) throws CacheException {
Cache cache = caches.get(name);
if (cache == null) {
cache = new RedisCache(redisUtil);
caches.put(name, cache);
}
return cache;
}
}
内部存在一个map来存储Cache
-
Cache
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.springframework.data.redis.core.RedisTemplate;
import java.util.*;
public class RedisCache<K, V> implements Cache<K, V> {
private RedisUtil redisUtil;
private RedisTemplate<String, Object> redisTemplate;
public RedisCache(RedisUtil redisUtil) {
this.redisUtil = redisUtil;
}
private static final String CACHE_KEY = "cache_key";
@Override
public V get(K key) throws CacheException {
return (V) redisUtil.hget(CACHE_KEY,key.toString());
}
@Override
public V put(K key, V value) throws CacheException {
redisUtil.hset(CACHE_KEY,key.toString(),value);
return value;
}
@Override
public V remove(K key) throws CacheException {
Object value = redisUtil.hget(CACHE_KEY, key.toString());
redisUtil.hdel(CACHE_KEY,key.toString());
return (V) value;
}
@Override
public void clear() throws CacheException {
Set<K> keys = keys();
for (K key : keys) {
redisUtil.hdel(CACHE_KEY,key.toString());
}
}
@Override
public int size() {
return keys().size();
}
@Override
public Set<K> keys() {
Set<Object> objects = redisUtil.hgetKeys(CACHE_KEY);
Set<K> keys = new HashSet<>();
for (Object object : objects) {
keys.add((K) object);
}
return keys;
}
@Override
public Collection<V> values() {
List<Object> objects = redisUtil.hgetList(CACHE_KEY);
List<V> list = new ArrayList<>();
for (Object object : objects) {
list.add((V)object);
}
return list;
}
}
顾名思义,不做赘述;
其他
- nginx的配置没什么特别之处,就不展示了;
总结
其实整合的过程并不难,需要理解的概念也很简单,但是问题在于坑实在有点多,主要体现在:
- simpleSession使用redis存取时对象和字符串转换问题
- nginx分发时session不一致问题
这两个问题结合shiro框架会引发许多bug值得注意;