SpringBoot+Shiro学习(五):Session会话管
这节我们讲一下,Shiro的Session会话管理。
会话
所谓会话,即用户访问应用时保持的连接关系,在多次交互中应用能够识别出当前访问的用户是谁,且可以在多次交互中保存一些数据。如访问一些网站时登录成功后,网站可以记住用户,且在退出之前都可以识别当前用户是谁。
SessionManager
会话管理器管理着应用中所有Subject的会话的创建、维护、删除、失效、验证等工作。是Shiro的核心组件,顶层组件SecurityManager直接继承了SessionManager,且提供了SessionsSecurityManager实现直接把会话管理委托给相应的SessionManager。Shiro提供了三个实现:DefaultSecurityManager及DefaultWebSecurityManager。
image.png
Shiro提供了三个默认实现:
DefaultSessionManager:DefaultSecurityManager使用的默认实现,用于JavaSE环境;
ServletContainerSessionManager:DefaultWebSecurityManager使用的默认实现,用于Web环境,其直接使用Servlet容器的会话;
DefaultWebSessionManager:用于Web环境的实现,可以替代ServletContainerSessionManager,自己维护着会话,直接废弃了Servlet容器的会话管理。
我们主要使用DefaultWebSessionManager,因为是web应用。列举一下常用的方法:
void validateSessions();//验证所有会话是否过期
Session getSession(SessionKey key) throws SessionException; //根据会话Key获取会话
sessionManager.setSessionValidationInterval(10000);//定时查询所有session是否过期的时间
sessionManager.setSessionListeners(List<SessionListener> list);//设置session监听器
sessionManager.setGlobalSessionTimeout(10000);//设置session过期时间
sessionManager.setSessionDAO(redisSessionDao());设置SessionDao
我们可以通过继承DefaultWebSessionManager来自定义我们的SessionManager,如下,我们将session存入到会话中的request。这样就不用每次都去缓存中取,提高性能:
public class CustomSessionManager extends DefaultWebSessionManager {
@Override
protected Session retrieveSession(SessionKey sessionKey) throws UnknownSessionException {
Serializable sessionId = getSessionId(sessionKey);
ServletRequest request = null;
if(sessionId instanceof WebSessionKey){
request = ((WebSessionKey)sessionKey).getServletRequest();
}
if(request != null && sessionId != null){
return (Session) request.getAttribute(sessionId.toString());
}
Session session = super.retrieveSession(sessionKey);
if(request !=null && sessionId != null){
request.setAttribute(sessionId.toString(),session);
}
return session;
}
}
会话监听器
会话监听器用于监听会话创建、过期及停止事件:
@Component
public class MySessionListener implements SessionListener {
private final AtomicInteger sessionCount = new AtomicInteger(0);
@Override
public void onStart(Session session) {
sessionCount.incrementAndGet();
System.out.println("登陆+1=="+sessionCount.get());
}
@Override
public void onStop(Session session) {
sessionCount.decrementAndGet();
System.out.println("登陆-1=="+sessionCount.get());
}
@Override
public void onExpiration(Session session) {
sessionCount.decrementAndGet();
System.out.println("登陆过期-1=="+sessionCount.get());
}
}
如果只想监听某一个事件,可以继承SessionListenerAdapter实现:
public class MySessionListener2 extends SessionListenerAdapter {
@Override
public void onStart(Session session) {
System.out.println("会话创建:" + session.getId());
}
}
会话存储/持久化
Shiro提供SessionDAO用于会话的CRUD,即DAO(Data Access Object)模式实现:
//如DefaultSessionManager在创建完session后会调用该方法;如保存到关系数据库/文件系统/NoSQL数据库;即可以实现会话的持久化;返回会话ID;主要此处返回的ID.equals(session.getId());
Serializable create(Session session);
//根据会话ID获取会话
Session readSession(Serializable sessionId) throws UnknownSessionException;
//更新会话;如更新会话最后访问时间/停止会话/设置超时时间/设置移除属性等会调用
void update(Session session) throws UnknownSessionException;
//删除会话;当会话过期/会话停止(如用户退出时)会调用
void delete(Session session);
//获取当前所有活跃用户,如果用户量多此方法影响性能
Collection<Session> getActiveSessions();
image
AbstractSessionDAO提供了SessionDAO的基础实现,如生成会话ID等;
CachingSessionDAO提供了对开发者透明的会话缓存的功能,只需要设置相应的CacheManager即可;MemorySessionDAO直接在内存中进行会话维护;
EnterpriseCacheSessionDAO提供了缓存功能的会话维护,默认情况下使用MapCache实现,内部使用ConcurrentHashMap保存缓存的会话。
我们可以通过自定义SessionDao继承AbstractSessionDAO重写方法将session持久化到redis等缓存中。
public class RedisSessionDao extends AbstractSessionDAO {
@Resource
RedisUtil redisUtil;
private final String SHIRO_SESSIOM_PREFIX = "shiro-session";
private byte[] getKey(String key){
return (SHIRO_SESSIOM_PREFIX+key).getBytes();
}
@Override
protected Serializable doCreate(Session session) {
Serializable sessionId = generateSessionId(session);
assignSessionId(session,sessionId);
saveSession(session);
return sessionId;
}
@Override
protected Session doReadSession(Serializable sessionId) {
if (sessionId == null) {
return null;
}
byte[] key = getKey(sessionId.toString());
byte[] value = redisUtil.get(key);
return (Session) SerializationUtils.deserialize(value);
}
@Override
public void update(Session session) throws UnknownSessionException {
saveSession(session);
}
private void saveSession(Session session){
if(session !=null&& session.getId()!=null) {
byte[] key = getKey(session.getId().toString());
byte[] value = SerializationUtils.serialize(session);
redisUtil.set(key, value);
redisUtil.expire(key, 600);
}
}
@Override
public void delete(Session session) {
if(session == null || session.getId() ==null){
return;
}
byte[] key = getKey(session.getId().toString());
redisUtil.del(key);
}
@Override
public Collection<Session> getActiveSessions() {
Set<byte[]> keys = redisUtil.getKeys(SHIRO_SESSIOM_PREFIX);
Set<Session> sessions = new HashSet<>();
if(CollectionUtils.isEmpty(keys)){
return sessions;
}
for(byte[] key:keys){
Session session = (Session)SerializationUtils.deserialize(redisUtil.get(key));
sessions.add(session);
}
return sessions;
}
}