六。shiro集群,将session保存到数据库和redis,使用户保持登录状态

1.目标

1.web端,用户第一次登陆之后,以后不需要再输入用户密码,就可以直接访问。使用cookie

2.shiro集群使用,需要共享session,把session放到数据库或redis就实现了这个目的

2.原理

会话管理器管理着应用中所有 Subject 的会话的创建、维护、删除、失效、验证等工作。是Shiro 的核心组件,顶层组件 SecurityManager 直接继承了 SessionManager,且提供了SessionsSecurityManager 实 现 直 接 把 会话管理 委 托 给 相 应 的 SessionManager ,DefaultSecurityManager 及 DefaultWebSecurityManager 默认 SecurityManager 都继承了SessionsSecurityManager。


Shiro 提供了三个默认实现:

DefaultSessionManager:DefaultSecurityManager 使用的默认实现,用于 JavaSE 环境;

ServletContainerSessionManager:DefaultWebSecurityManager 使用的默认实现,用于 Web环境,其直接使用 Servlet 容器的会话;

DefaultWebSessionManager : 用 于 Web 环境的实 现 , 可 以 替 代ServletContainerSessionManager,自己维护着会话,直接废弃了 Servlet 容器的会话管理。


Shiro 提供 SessionDAO 用于会话的 CRUD,即 DAO(Data Access Object)模式实现



3.将session保存到数据库

web应用通常使用DefaultWebSessionManager,这里我重写了一个StatelessSessionManager ,第二个目标才会用到,现在直接用DefaultWebSessionManager就行

将自己写的sseiondao注入session管理器

	public DefaultWebSessionManager defaultWebSessionManager() {
		//自定义session管理器,重写getSessionId,分析请求头中的指定参数,做用户凭证sessionId
		StatelessSessionManager sessionManager=new StatelessSessionManager();
		sessionManager.setSessionDAO(sessionDao());
		//毫秒,session失效时间为负即永不失效
		sessionManager.setGlobalSessionTimeout(-1);
		return sessionManager;
	}
	
	@Bean
	public DLSessionDao sessionDao() {
		//自定义sessionDao,持久化到数据库
		DLSessionDao sessionDao=new DLSessionDao();
		sessionDao.setUserSessionExtendMapper(userSessionExtendMapper);
		sessionDao.setUserSessionMapper(userSessionMapper);
		return sessionDao;
	}
	
	/**
	 * 身份认证realm; (这个需要自己写,账号密码校验;权限等)
	 * @return
	 */
	@Bean
	public DLingShiroRealm DLingShiroRealm() {
		DLingShiroRealm myShiroRealm = new DLingShiroRealm();
		myShiroRealm.setCredentialsMatcher(credentialsMatcher());
		myShiroRealm.setUserService(userService);
		return myShiroRealm;
	}
/**
 * 
 * @author Melo
 * 
 * 继承EnterpriseCacheSessionDAO 构造方法用ConcurrentHashMap,执行doCreater后执行CachingSessionDAO的create方法将session放缓存、
 * CachingSessionDAO自带map缓存,但在安全管理器中注入了redis,就会将seesion也放到redis
 */
public class DLSessionDao extends EnterpriseCacheSessionDAO {
	
	//@Autowired
	DlUserSessionMapper userSessionMapper;
	
	//@Autowired
	DlUserSessionExtendMapper userSessionExtendMapper;

	/*创建session
	Creater session 是第一次请求,如果浏览器没有jseesion就创建并写给浏览器
	如果安全管理器中的seesion失效了,用户登陆是携带jsessionid,先执行readSession,
	通过Jsessionid区找Session,找到了就给安全管理器,找不到就执行creatSession方法重新创建session。*/
    @Override  
    protected Serializable doCreate(Session session) {  
        Serializable sessionId = super.doCreate(session); //
        //Serializable sessionId = generateSessionId(session);
        //assignSessionId(session, sessionId);

        DlUserSession userSession=new DlUserSession();
        Date now = new Date();
        userSession.setDlUserSessionId((String) sessionId);
        userSession.setDlUserSession(SerializeUtils.serialize(session));
        userSession.setDlUserSessionCreatetime(now);
        userSession.setDlUserSessionUpdatetime(now);
        userSessionMapper.insert(userSession);
        return sessionId;  
    }  
  
    //获取session  
    @Override  
    protected Session doReadSession(Serializable sessionId) {  
       Session session = super.doReadSession(sessionId);  

        if(session==null) {
        	 DlUserSession userSession = userSessionExtendMapper.getSessionBySessionId((String)sessionId);
        	 if(userSession!=null) {
              	session=(Session) SerializeUtils.deserialize(userSession.getDlUserSession());
              	//如果库有,缓存却没有,加入缓存,readsession调用次数太多
              	//super.doCreate(session);虽然可以加入缓存,但错误的 会产生新的seesionid
              	cache(session, sessionId);
        	 }
        }
        return session;  
    }  
  
    //更新session .shiro对每一次请求都会更新最后访问时间.当一个页面包含多个资源的时候就会发生多次update session 
    @Override  
    protected void doUpdate(Session session) {  
        super.doUpdate(session);  
        DlUserSession userSession=new DlUserSession();
    	userSession.setDlUserSession(SerializeUtils.serialize(session));
    	userSession.setDlUserSessionId((String) session.getId());
    	userSession.setDlUserSessionUpdatetime(new Date());
    	//getSecurityManager()是ShiroUtils.getUser()底层的方法,未登录的情况下SecurityManager不存在,就报错 
    	if(ThreadContext.getSecurityManager()!=null&&ShiroUtils.getUser()!=null) {
    		userSession.setDlUserCode(ShiroUtils.getUser().getCode());
    	}
    	userSessionExtendMapper.updateSession(userSession);
    }  
  
    //删除session  
    @Override  
    protected void doDelete(Session session) {  
       super.doDelete(session); 
       userSessionMapper.deleteBySessionId((String)session.getId()); 
    }

我这里选择继承了EnterpriseCacheSessionDAO,它本身就有concurrentmap做缓存,把session放在其中。


session是对象,保存到数据库,需要进行序列化。如果在seesion放入了user等对象,需要将他们也实现序列化接口

工具类

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

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

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;
	}
}

4.debug执行过程:

如果浏览器有JSESSIONID,请求进来先执行doReadSession,如果jesseionid正确与内存或缓存中的seesion对应上,就会再执行updateSession方法(每次请求都会执行update方法,更新seesion最后的访问时间)。

如果没有jesseionid,或jessionid不正确,会执行我们重写的doCreate方法,先调用EnterpriseCacheSessionDAO的doCreate方法创建session,返回一个sessionid,我们把session保存到数据库里。

执行EnterpriseCacheSessionDAO的doCreate后,会执行CachingSessionDAO的create方法将session放缓存,至于放入哪种缓存,就是看你把哪种缓存注入session管理器

    public Serializable create(Session session) {
        Serializable sessionId = super.create(session);
        cache(session, sessionId);
        return sessionId;
    }

主要就是CachingSessionDAO、CachingSessionDAO、AbstractSessionDAO,debug就可以发现其调用过程


5.注意

1.刚开始我只重写了doCreate 和doread方法,发现每次请求会读取22次doread之多,最后一次调用时,会报错。最初以为没有把seesion正确的保存到数据库,最后发现是没有重写update方法的问题。每次请求都会调用update方法对session进行更新

2.SecurityUtils.getSubject()这个方法其实也是依赖于session,shiro就是通过session知道当前访问的用户是哪个用户的;在shiro 自定义realm时,我们通常会把一个用户对象user放入认证对象,所以要把session序列化进数据库,要把user类也实现序列化接口

6.将session保存到redis

网上有一个redis的sessiondao类,也是重写了那四个方法,将session序列化进redis。刚开始我也试过那个,但后来发现,只要你用的安全管理器继承了SessionsSecurityManager,在安全管理器中注入redis管理器,就直接会影响seesion放入哪种缓存,直接将session放入了redis,不需要那个redisseesiondao了。


	@Bean
	public SecurityManager securityManager() {
		DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();

		securityManager.setRealm(DLingShiroRealm());
		
		//shiro自带的一个内存缓存,本质是hashmap,securityManager.setCacheManager(new MemoryConstrainedCacheManager());
		//将缓存注入安全管理器,避免频繁调用授权方法,只要实现了shiro的cache接口、CacheManager接口就可以用来注入安全管理器
		//此配置会将session也放入redis
		securityManager.setCacheManager(redisCacheManager());
		
		securityManager.setSessionManager(defaultWebSessionManager());
		return securityManager;
	}

	/**
	 * cacheManager 缓存 redis实现
	 * 网上的一个 shiro-redis 插件
	 * @return
	 */
	@Bean
	public RedisCacheManager redisCacheManager() {
		RedisCacheManager redisCacheManager = new RedisCacheManager();
		redisCacheManager.setRedisManager(redisManager());
		return redisCacheManager;
	}

在安全管理器中注入redis后,读取session相当于去redis读取,debug会发现 只要redis有数据,doread不会被读取。但update方法必须还得把session修改入库。如果把redis的数据删除,我们可以先在doread方法读取数据库的session,再把此session放入redis,调用CachingSessionDAO的cache方法,这样后面请求就不用反复入库查询

   protected void cache(Session session, Serializable sessionId) {
        if (session == null || sessionId == null) {
            return;
        }
        Cache<Serializable, Session> cache = getActiveSessionsCacheLazy();
        if (cache == null) {
            return;
        }
        cache(session, sessionId, cache);
    }

猜你喜欢

转载自blog.csdn.net/u014203449/article/details/80888637
今日推荐