session管理以及并发用户登陆限制(网页应用版)
首先讲一下博主在shiro中对session和cookie使用的理解吧。
在每一次用户成功登陆后,shiro都会自动的创建一个session并储存在服务端,该session会包含Subject的基本信息,并会在请求结束后返回session的sessionId给客户端,客户端在浏览器没有关闭的情况下可以使用sessionId进行身份验证,通过在之后的请求带上sessionId的cookie来检索服务器端的session,如果session存在则验证通过,并使用session的信息。
这里顺便说一下rememberMe功能的实现机制。
如果开启了shiro的rememberMe功能,那么请求结束后将会再加上一个rememberMe的cookie,该cookie会储存用户的基本信息,一般都会对它进行加密,当浏览器关闭后,seesionId的cookie将会被清除,但remremberMe的cookie将会根据自己定义的时间而保留在用户的浏览器中,当用户下一次再次打来浏览器访问网址时,会自动带上这个cookie,而如果该网址不是登陆网址的话,shiro将会对该cookie进行解码,获得里面的用户信息,从而实现免密登陆,更新subject的信息,生成新的session并返回sessionId。
简而言之,shiro是通过在服务端生成sesion来维持于客户端的连接,cookie则是判断该
连接是否有效的token
sessionDao实现
使用了Ehcache作为缓存,也可以使用redis或者数据库来进行持久化操作
继承AbstractSessionDAO
//导入PRINCIPALS_SESSION_KEY
import static org.apache.shiro.subject.support.DefaultSubjectContext.PRINCIPALS_SESSION_KEY;
/**
* Shiro的是Session操作的实现类
* 但update操作由于是更新用户的最新一次操作,所以调用频率高
* 如果shiro的过滤器过滤所有的链接,那么就算是静态资源也算会调用update
* 因此如果要减少update的调用,目前的解决方案是将对外服务的接口的url加上.do之类的标志结尾
* shiro的过滤器只过滤这些url
* @author MDY
*/
public class MyShiroDao extends AbstractSessionDAO {
//用于缓存session
private Cache<Serializable, Session> cache;
//用于缓存sessionId对应的用户,实现用户登陆人数限制
private Cache<String, Serializable> userCache;
@Override
protected Serializable doCreate(Session session) {
Serializable sessionId = generateSessionId(session);
assignSessionId(session, sessionId);
cache.put(sessionId, session);
return sessionId;
}
@Override
protected Session doReadSession(Serializable sessionId) {
if (sessionId == null) {
return null;
}
return cache.get(sessionId);
}
@Override
public void update(Session session) throws UnknownSessionException {
if (session instanceof ValidatingSession && !((ValidatingSession) session).isValid()) {
//如果会话过期/停止 没必要再更新了
return;
}
cache.put(session.getId(), session);
}
@Override
public void delete(Session session) {
cache.remove(session.getId());
userCache.put(String.valueOf(session.getAttribute(PRINCIPALS_SESSION_KEY)), null);
}
@Override
public Collection<Session> getActiveSessions() {
return cache.values();
}
public void setCache(MyShiroCache myShiroCache) {
userCache = myShiroCache.getUserSessionCache();
cache = myShiroCache.getSessionCache();
}
}
SessionManager实现
这里是直接复制网上的代码的,通过在request里面设置seesionId和session,减少了sessionDao的read次数
public class MyShiroSessionManager extends DefaultWebSessionManager {
/**
* 获取session
* 优化单次请求需要多次访问缓存的问题
* @throws UnknownSessionException
*/
@Override
protected Session retrieveSession(SessionKey sessionKey){
Serializable sessionId = getSessionId(sessionKey);
ServletRequest request = null;
if (sessionKey instanceof WebSessionKey) {
request = ((WebSessionKey) sessionKey).getServletRequest();
}
if (request != null && null != sessionId) {
Object sessionObj = request.getAttribute(sessionId.toString());
if (sessionObj != null) {
return (Session) sessionObj;
}
}
Session session = super.retrieveSession(sessionKey);
if (request != null && null != sessionId) {
request.setAttribute(sessionId.toString(), session);
}
return session;
}
}
单用户登陆限制
这里说一下自己的实现思路:网上大部分教程都是直接在过滤器实现对用户的踢出,用过重写shiro的filter的isAccessAllowed方法,而博主则是在登陆的时候进行会话的判断,通过重写HashedCredentialsMatcher的doCredentialsMatch方法,该方法会在Subject.login(token)的时候调用,这里只实现单用户登陆限制
@Override
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
String username = (String) token.getPrincipal();
boolean match = super.doCredentialsMatch(token, info);
if (match) {
myShiroCache.getPasswordRetryCache().remove(username);
//实现对之前登陆的用户踢出
Session session = SecurityUtils.getSubject().getSession();
Serializable sessionId = session.getId();
//从缓存中取出之前该用户对应的sessionId,有的话就删除
Serializable perSessionId = myShiroCache.getUserSessionCache().get(username);
if (perSessionId != null) {
myShiroCache.getSessionCache().remove(perSessionId);
}
myShiroCache.getUserSessionCache().put(username, sessionId);
System.err.println(sessionId.toString());
}
return match;
}
- MyShiroCache实现,这里采用Ehcache当缓存,Ehcache的整合可以参考这
发现其实如果自己实现了SessionDao的话,就没有必要专门使用Shiro的CacheManager,因此建议这里的MyShiroCache可以直接使用Ehcache或者redis等缓存工具的实例。。。
(温馨提醒:用redis缓存shiro的session的时候会有坑,当你想将session序列化成字符串你可能遇到)
/**
* 提供shiro用来操作ehcache的cache
* @author MDY
*/
public class MyShiroCache {
private Cache<String, Serializable> userSessionCache;
private Cache<Serializable, Session> sessionCache;
//shiro的缓存管理器
public MyShiroCache(CacheManager cacheManager) {
this.userSessionCache = cacheManager.getCache("userSessionId");
this.sessionCache = cacheManager.getCache("sessionId");
}
public Cache<Serializable, Session> getSessionCache() {
return sessionCache;
}
public Cache<String, Serializable> getUserSessionCache() {
return userSessionCache;
}
}