session管理以及并发用户登陆限制(网页应用版)


首先讲一下博主在shiro中对sessioncookie使用的理解吧。
在每一次用户成功登陆后,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方法,而博主则是在登陆的时候进行会话的判断,通过重写HashedCredentialsMatcherdoCredentialsMatch方法,该方法会在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;
    }

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

}