Shiro permission management framework: in-depth analysis of Session management in Shiro

Keywords: Java Session Shiro Redis

In fact, some of Shiro's learning notes should have been written for a long time, because the late stage of lazy cancer and procrastination has not been implemented. Until today, a project of the company encountered the problem of frequent disconnection of single sign on in the cluster environment. In order to solve this problem, Shiro's relevant documents and tutorials have been reviewed. Finally, the problem was solved, but I think it's time for me to make a wave of Shiro study notes.

This is the fourth part of Shiro series, Filter initialization process and implementation principle in Shiro. Shiro's URL based permission control is implemented through Filter. This article starts with the ShiroFilterFactoryBean we injected, and looks at the source code to trace the implementation principle of the Filter in Shiro.

First address: https://www.guitu18.com/post/2019/08/08/45.html

Session

SessionManager

When configuring Shiro, we configured a defaultwebsecurity manager. Let's look at it first.

DefaultWebSecurityManager

    public DefaultWebSecurityManager() {
        super();
        ((DefaultSubjectDAO) this.subjectDAO).setSessionStorageEvaluator(new DefaultWebSessionStorageEvaluator());
        this.sessionMode = HTTP_SESSION_MODE;
        setSubjectFactory(new DefaultWebSubjectFactory());
        setRememberMeManager(new CookieRememberMeManager());
        setSessionManager(new ServletContainerSessionManager());
    }

A ServletContainerSessionManager is injected into its constructor

public class ServletContainerSessionManager implements WebSessionManager {
    public Session getSession(SessionKey key) throws SessionException {
        if (!WebUtils.isHttp(key)) {
            String msg = "SessionKey must be an HTTP compatible implementation.";
            throw new IllegalArgumentException(msg);
        }
        HttpServletRequest request = WebUtils.getHttpRequest(key);
        Session session = null;
        HttpSession httpSession = request.getSession(false);
        if (httpSession != null) {
            session = createSession(httpSession, request.getRemoteHost());
        }
        return session;
    }
    
    private String getHost(SessionContext context) {
        String host = context.getHost();
        if (host == null) {
            ServletRequest request = WebUtils.getRequest(context);
            if (request != null) {
                host = request.getRemoteHost();
            }
        }
        return host;
    }

    protected Session createSession(SessionContext sessionContext) throws AuthorizationException {
        if (!WebUtils.isHttp(sessionContext)) {
            String msg = "SessionContext must be an HTTP compatible implementation.";
            throw new IllegalArgumentException(msg);
        }
        HttpServletRequest request = WebUtils.getHttpRequest(sessionContext);
        HttpSession httpSession = request.getSession();
        String host = getHost(sessionContext);
        return createSession(httpSession, host);
    }
    
    protected Session createSession(HttpSession httpSession, String host) {
        return new HttpServletSession(httpSession, host);
    }
}

ServletContainerSessionManager does not manage sessions by itself, and its final operation is HttpSession, so it can only work in Servlet containers, and it cannot support any sessions except those using HTTP protocol.

So in general, Shiro will configure a DefaultWebSessionManager, which inherits the DefaultSessionManager. Take a look at the construction method of DefaultSessionManager:

public DefaultSessionManager() {
    this.deleteInvalidSessions = true;
    this.sessionFactory = new SimpleSessionFactory();
    this.sessionDAO = new MemorySessionDAO();
}

The sessionDAO here initializes a MemorySessionDAO, which is actually a Map. In memory, sessions are managed by key value pairs.

public MemorySessionDAO() {
    this.sessions = new ConcurrentHashMap<Serializable, Session>();
}

HttpServletSession

public class HttpServletSession implements Session {
    public HttpServletSession(HttpSession httpSession, String host) {
        if (httpSession == null) {
            String msg = "HttpSession constructor argument cannot be null.";
            throw new IllegalArgumentException(msg);
        }
        if (httpSession instanceof ShiroHttpSession) {
            String msg = "HttpSession constructor argument cannot be an instance of ShiroHttpSession.  This " +
                    "is enforced to prevent circular dependencies and infinite loops.";
            throw new IllegalArgumentException(msg);
        }
        this.httpSession = httpSession;
        if (StringUtils.hasText(host)) {
            setHost(host);
        }
    }
    protected void setHost(String host) {
        setAttribute(HOST_SESSION_KEY, host);
    }
    public void setAttribute(Object key, Object value) throws InvalidSessionException {
        try {
            httpSession.setAttribute(assertString(key), value);
        } catch (Exception e) {
            throw new InvalidSessionException(e);
        }
    }
}

Shiro's HttpServletSession just encapsulates javax.servlet.http.HttpSession, so the related operations of Session in Web applications are ultimately javax.servlet.http.HttpSession. For example, setHost() in the above code saves the content in the form of key value pairs in httpSession.

Let's take a look at this picture:

First, understand the relationship and function of the above classes, and then we want to manage some data in Shiro.

Session Dao is the top-level interface of session management, which defines the methods of adding, deleting, modifying and querying session.

public interface SessionDAO {
    Serializable create(Session session);
    Session readSession(Serializable sessionId) throws UnknownSessionException;
    void update(Session session) throws UnknownSessionException;
    void delete(Session session);
    Collection<Session> getActiveSessions();
}

AbstractSessionDao is an abstract class. In its construction method, JavaUuidSessionIdGenerator is defined as SessionIdGenerator to generate SessionId. Although it implements the two methods of create() and readSession(), the specific process calls its two abstract methods, doCreate() and doReadSession(), which need its subclass to work.

public abstract class AbstractSessionDAO implements SessionDAO {
    private SessionIdGenerator sessionIdGenerator;
    public AbstractSessionDAO() {
        this.sessionIdGenerator = new JavaUuidSessionIdGenerator();
    }
    
    public Serializable create(Session session) {
        Serializable sessionId = doCreate(session);
        verifySessionId(sessionId);
        return sessionId;
    }
    protected abstract Serializable doCreate(Session session);

    public Session readSession(Serializable sessionId) throws UnknownSessionException {
        Session s = doReadSession(sessionId);
        if (s == null) {
            throw new UnknownSessionException("There is no session with id [" + sessionId + "]");
        }
        return s;
    }
    protected abstract Session doReadSession(Serializable sessionId);
}

Looking at the above class diagram AbstractSessionDao, there are three subclasses. Looking at the source code, we find that cacheingsessiondao is an abstract class, which does not implement these two methods. In its subclass EnterpriseCacheSessionDAO, doCreate() and doReadSession() are implemented, but doReadSession() is an empty implementation that returns null directly.

    public EnterpriseCacheSessionDAO() {
        setCacheManager(new AbstractCacheManager() {
            @Override
            protected Cache<Serializable, Session> createCache(String name) throws CacheException {
                return new MapCache<Serializable, Session>(name, new ConcurrentHashMap<Serializable, Session>());
            }
        });
    }

Enterprise cachesession Dao relies on its parent cacheingsessiondao. In its construction method, an anonymous implementation of AbstractCacheManager is injected into the parent class. It is a memory based session Dao, and the MapCache it creates is a Map.

We use it in the Shiro configuration class through sessionManager.setSessionDAO(new EnterpriseCacheSessionDAO());. Then make a breakpoint test in cacheingsessiondao. Getcachedsession(). You can see that cache is a ConcurrentHashMap, which stores the mapping relationship between JSESSIONID and Session in the form of key value in memory.

The third implementation of AbstractSessionDao is MemorySessionDAO, which is a memory based SessionDao. It is simple and direct. The construction method directly creates a ConcurrentHashMap.

    public MemorySessionDAO() {
        this.sessions = new ConcurrentHashMap<Serializable, Session>();
    }

What's the difference between enterprise cache session Dao and enterprise cache session Dao? In fact, enterprise cache session Dao is only a default implementation of caching session Dao. In caching session Dao, cacheManager has no default value. In the construction method of enterprise cache session Dao, it is initialized as a concurrent HashMap.

If we use enterprise Cache Session Dao directly, there is no difference between enterprise Cache Session Dao and memory Session Dao. They are all memory Session Dao based on Map. But the purpose of cacheingsessiondao is to facilitate expansion. Users can inherit cacheingsessiondao and inject their own Cache implementation, such as RedisCache of Redis Cache Session.

In practice, if Redis is needed to manage sessions, it's better to inherit AbstractSessionDao directly. It's unnecessary to have Redis to support the Cache in the middle. In this way, you can also share sessions in a distributed or clustered environment (if there's another layer of Cache in the middle of a distributed or clustered environment, you need to consider synchronization).

Back to the problem mentioned at the beginning of our company's project cluster environment, the frequent offline of single sign on is actually caused by the Cache in the middle layer. In our business code, RedisSessionDao is inherited from the enterprise Cache Session Dao, so there is a memory based Cache layer on top of Redis. At this time, if the user's Session changes, although the Session in Redis is synchronized and the Cache layer is not synchronized, the phenomenon is that the user's Session in one server is valid and the Session in the Cache of the other server is old, and then the user is forced to go offline.

Posted by CBR on Sat, 02 Nov 2019 05:22:57 -0700