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