<dependency>
    <groupId>net.sf.ehcache</groupId>
    <artifactId>ehcache</artifactId>
    <version>2.9.0</version>
</dependency>存储在 resources/spring-shiro-ehcache.xml
<ehcache updateCheck="false" name="shiroCache">
    <defaultCache
            maxElementsInMemory="10000"
            eternal="false"
            timeToIdleSeconds="120"
            timeToLiveSeconds="120"
            overflowToDisk="false"
            diskPersistent="false"
            diskExpiryThreadIntervalSeconds="120"
            />
</ehcache>在 resources/spring-shiro.xml 里生成 EhCacheManager 的 Bean,把 EhCacheManager 的 Bean 注入到 SecurityManager
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
    <property name="realm" ref="realm"/>
    <!-- 需要使用cache的话加上这句 -->
    <property name="cacheManager" ref="shiroEhcacheManager" />
</bean>
<!-- 需要使用cache的话加上这句 -->
<bean id="shiroEhcacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
    <property name="cacheManagerConfigFile" value="classpath:spring-shiro-ehcache.xml" />
</bean>在 filter.ShiroRealm.doGetAuthenticationInfo() 里打上断点
可以看到添加 EHCache 后 doGetAuthenticationInfo() 在登录成功后不会调用了,因为验证登录信息首先从 Cache 里查找,如果没有找到才去调用 doGetAuthenticationInfo() 进行登录验证。
name:缓存名称。maxElementsInMemory: 缓存最大个eternal:对象是否永久有效,一但设置了,timeout将不起作用timeToIdleSeconds:设置对象在失效前的允许闲置时间(单位:秒)。仅当eternal=false对象不是永久有效时使用,可选属性,默认值是0,也就是可闲置时间无穷大。timeToLiveSeconds:设置对象在失效前允许存活时间(单位:秒)。最大时间介于创建时间和失效时间之间。仅当eternal=false对象不是永久有效时使用,默认是0.,也就是对象存活时间无穷大。overflowToDisk:当内存中对象数量达到maxElementsInMemory时,Ehcache将会对象写到磁盘中。diskSpoolBufferSizeMB:这个参数设置DiskStore(磁盘缓存)的缓存区大小。默认是30MB。每个Cache都应该有自己的一个缓冲区。maxElementsOnDisk:硬盘最大缓存个数。diskPersistent:是否缓存虚拟机重启期数据 Whether the disk store persists between restarts of the Virtual Machine. The default value is false.diskExpiryThreadIntervalSeconds:磁盘失效线程运行时间间隔,默认是120秒。memoryStoreEvictionPolicy:当达到maxElementsInMemory限制时,Ehcache将会根据指定的策略去清理内存。默认策略是LRU(最近最少使用)。你可以设置为FIFO(先进先出)或是LFU(较少使用)。clearOnFlush:内存数量最大时是否清除。| Filter Name | Class | 
|---|---|
| anon | org.apache.shiro.web.filter.authc.AnonymousFilter | 
| authc | org.apache.shiro.web.filter.authc.FormAuthenticationFilter | 
| authcBasic | org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter | 
| logout | org.apache.shiro.web.filter.authc.LogoutFilter | 
| noSessionCreation | org.apache.shiro.web.filter.session.NoSessionCreationFilter | 
| perms | org.apache.shiro.web.filter.authz.PermissionAuthorizationFilter | 
| roles | org.apache.shiro.web.filter.authz.RolesAuthorizationFilter | 
| port | org.apache.shiro.web.filter.authz.PortFilter | 
| rest | org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter | 
| ssl | org.apache.shiro.web.filter.authz.SslFilter | 
| user | org.apache.shiro.web.filter.authz.UserFilter | 
Redis 是一个高速的分布式缓存。
虽然配置 EhCache 提升了效率,但是,Session 仍然存储在 Server 的内存里(Shiro 默认使用 MemorySessionDAO 把 Session 存储在 ConcurrentMap 里),当有大量的用户登录后 Server 的内存就会急剧增加,而且由于 Server 之间内存里的 Session 不能共享,所以没法实现集群。为了解决这两个问题,我们本地仍然使用 EhCache 缓存 Session,但是 Session 存储在 Redis 里。
为了把 Session 存储到 Redis,只需要实现 Shiro 提供的 SessionDAO 就可以了。RedisSessionDAO 继承 CachingSessionDAO 来实现 SessionDAO,主要是实现 Session 的 CRUD,SerializationUtils 用来序列化和反序列化,RedisManager 用来访问 Redis。
RedisSessionDAOpackage shiro;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.UnknownSessionException;
import org.apache.shiro.session.mgt.ValidatingSession;
import org.apache.shiro.session.mgt.eis.CachingSessionDAO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import util.SerializationUtils;
import java.io.Serializable;
import java.util.*;
public class RedisSessionDAO extends CachingSessionDAO {
    private static Logger logger = LoggerFactory.getLogger(RedisSessionDAO.class);
    // 登录成功的信息存储在 session 的这个 attribute 里.
    private static final String AUTHENTICATED_SESSION_KEY =
            "org.apache.shiro.subject.support.DefaultSubjectContext_AUTHENTICATED_SESSION_KEY";
    private String keyPrefix = "shiro_redis_session:";
    private String deleteChannel = "shiro_redis_session:delete";
    private int timeToLiveSeconds = 1800; // Expiration of Jedis's key, unit: second
    private RedisManager redisManager;
    /**
     * DefaultSessionManager 创建完 session 后会调用该方法。
     * 把 session 保持到 Redis。
     * 返回 Session ID;主要此处返回的 ID.equals(session.getId())
     */
    @Override
    protected Serializable doCreate(Session session) {
        logger.debug("=> Create session with ID [{}]", session.getId());
        // 创建一个Id并设置给Session
        Serializable sessionId = this.generateSessionId(session);
        assignSessionId(session, sessionId);
        // session 由 Redis 缓存失效决定
        String key = SerializationUtils.sessionKey(keyPrefix, session);
        String value = SerializationUtils.sessionToString(session);
        redisManager.setex(key, value, timeToLiveSeconds);
        return sessionId;
    }
    /**
     * 决定从本地 Cache 还是从 Redis 读取 Session.
     * @param sessionId
     * @return
     * @throws UnknownSessionException
     */
    @Override
    public Session readSession(Serializable sessionId) throws UnknownSessionException {
        Session s = getCachedSession(sessionId);
        // 1. 如果本地缓存没有,则从 Redis 读取。
        // 2. ServerA 登录了,ServerB 没有登录但缓存里有此 session,所以从 Redis 读取而不是直接用缓存里的
        if (s == null || (
                s.getAttribute(AUTHENTICATED_SESSION_KEY) != null
                && !(Boolean) s.getAttribute(AUTHENTICATED_SESSION_KEY)
        )) {
            s = doReadSession(sessionId);
            if (s == null) {
                throw new UnknownSessionException("There is no session with id [" + sessionId + "]");
            }
            return s;
        }
        return s;
    }
    /**
     * 从 Redis 上读取 session,并缓存到本地 Cache.
     * @param sessionId
     * @return
     */
    @Override
    protected Session doReadSession(Serializable sessionId) {
        logger.debug("=> Read session with ID [{}]", sessionId);
        String value = redisManager.get(SerializationUtils.sessionKey(keyPrefix, sessionId));
        // 例如 Redis 调用 flushdb 情况了所有的数据,读到的 session 就是空的
        if (value != null) {
            Session session = SerializationUtils.sessionFromString(value);
            super.cache(session, session.getId());
            return session;
        }
        return null;
    }
    /**
     * 更新 session 到 Redis.
     * @param session
     */
    @Override
    protected void doUpdate(Session session) {
        // 如果会话过期/停止,没必要再更新了
        if (session instanceof ValidatingSession && !((ValidatingSession) session).isValid()) {
            logger.debug("=> Invalid session.");
            return;
        }
        logger.debug("=> Update session with ID [{}]", session.getId());
        String key = SerializationUtils.sessionKey(keyPrefix, session);
        String value = SerializationUtils.sessionToString(session);
        redisManager.setex(key, value, timeToLiveSeconds);
    }
    /**
     * 从 Redis 删除 session,并且发布消息通知其它 Server 上的 Cache 删除 session.
     * @param session
     */
    @Override
    protected void doDelete(Session session) {
        logger.debug("=> Delete session with ID [{}]", session.getId());
        redisManager.del(SerializationUtils.sessionKey(keyPrefix, session));
        // 发布消息通知其它 Server 上的 cache 删除 session.
        redisManager.publish(deleteChannel, SerializationUtils.sessionIdToString(session));
        // 放在其它类里用一个 daemon 线程执行,删除 cache 中的 session
        // jedis.subscribe(new JedisPubSub() {
        //     @Override
        //     public void onMessage(String channel, String message) {
        //         // 1. deserialize message to sessionId
        //         // 2. Session session = getCachedSession(sessionId);
        //         // 3. uncache(session);
        //     }
        // }, deleteChannel);
    }
    /**
     * 取得所有有效的 session.
     * @return
     */
    @Override
    public Collection<Session> getActiveSessions() {
        logger.debug("=> Get active sessions");
        Set<String> keys = redisManager.keys(keyPrefix + "*");
        Collection<String> values = redisManager.mget(keys.toArray(new String[0]));
        List<Session> sessions = new LinkedList<Session>();
        for (String value : values) {
            sessions.add(SerializationUtils.sessionFromString(value));
        }
        return sessions;
    }
    public String getKeyPrefix() {
        return keyPrefix;
    }
    public void setKeyPrefix(String keyPrefix) {
        this.keyPrefix = keyPrefix;
    }
    public String getDeleteChannel() {
        return deleteChannel;
    }
    public void setDeleteChannel(String deleteChannel) {
        this.deleteChannel = deleteChannel;
    }
    public RedisManager getRedisManager() {
        return redisManager;
    }
    public void setRedisManager(RedisManager redisManager) {
        this.redisManager = redisManager;
    }
    public int getTimeToLiveSeconds() {
        return timeToLiveSeconds;
    }
    public void setTimeToLiveSeconds(int timeToLiveSeconds) {
        this.timeToLiveSeconds = timeToLiveSeconds;
    }
}RedisManagerpackage shiro;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import java.util.Collection;
import java.util.Collections;
import java.util.Set;
public class RedisManager {
    private String host = "127.0.0.1";
    private int port = 6379;
    private int timeout = 0; // Timeout for Jedis try to connect to redis server
    private String password = "";
    private JedisPool jedisPool   = null;
    public RedisManager(){
        init();
    }
    /**
     * Initializing jedis pool to connect to Jedis.
     */
    public void init() {
        if(password != null && !"".equals(password)) {
            jedisPool = new JedisPool(new JedisPoolConfig(), host, port, timeout, password);
        } else if (timeout != 0) {
            jedisPool = new JedisPool(new JedisPoolConfig(), host, port, timeout);
        } else {
            jedisPool = new JedisPool(new JedisPoolConfig(), host, port);
        }
    }
    public Jedis getJedis() {
        return jedisPool.getResource();
    }
    /**
     * Get value from Redis
     * @param key
     * @return
     */
    public String get(String key){
        Jedis jedis = jedisPool.getResource();
        try {
            return jedis.get(key);
        } finally {
            jedis.close();
        }
    }
    /**
     * Set value into Redis with default time to live in seconds.
     * @param key
     * @param value
     */
    public void set(String key, String value){
        Jedis jedis = jedisPool.getResource();
        try {
            jedis.set(key, value);
        } finally {
            jedis.close();
        }
    }
    /**
     * Set value into Redis with specified time to live in seconds.
     * @param key
     * @param value
     * @param timeToLiveSeconds
     */
    public void setex(String key, String value, int timeToLiveSeconds){
        Jedis jedis = jedisPool.getResource();
        try {
            jedis.setex(key, timeToLiveSeconds, value);
        } finally {
            jedis.close();
        }
    }
    /**
     * Delete key and its value from Jedis.
     * @param key
     */
    public void del(String key){
        Jedis jedis = jedisPool.getResource();
        try {
            jedis.del(key);
        } finally {
            jedis.close();
        }
    }
    /**
     * Get keys matches the given pattern.
     * @param pattern
     * @return
     */
    public Set<String> keys(String pattern){
        Jedis jedis = jedisPool.getResource();
        try {
            return jedis.keys(pattern);
        } finally {
            jedis.close();
        }
    }
    /**
     * Get multiple values for the given keys.
     * @param keys
     * @return
     */
    public Collection<String> mget(String... keys) {
        if (keys == null && keys.length == 0) {
            Collections.emptySet();
        }
        Jedis jedis = jedisPool.getResource();
        try {
            return jedis.mget(keys);
        } finally {
            jedis.close();
        }
    }
    /**
     * Publish message to channel using subscribe and publish protocol.
     * @param channel
     * @param value
     */
    public void publish(String channel, String value) {
        Jedis jedis = jedisPool.getResource();
        try {
            jedis.publish(channel, value);
        } finally {
            jedis.close();
        }
    }
    public String getHost() {
        return host;
    }
    public void setHost(String host) {
        this.host = host;
    }
    public int getPort() {
        return port;
    }
    public void setPort(int port) {
        this.port = port;
    }
    public int getTimeout() {
        return timeout;
    }
    public void setTimeout(int timeout) {
        this.timeout = timeout;
    }
    public String getPassword() {
        return password;
    }
    public void setPassword(String password) {
        this.password = password;
    }
}SerializationUtilspackage util;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.SimpleSession;
import java.io.Serializable;
public class SerializationUtils {
    /**
     * 使用 sessionId 创建字符串的 key,用来在 Redis 里作为存储 Session 的 key.
     * @param prefix
     * @param sessionId
     * @return
     */
    public static String sessionKey(String prefix, Serializable sessionId) {
        return prefix + sessionId;
    }
    /**
     * 使用 session 创建字符串的 key,用来在 Redis 里作为存储 Session 的 key.
     * @param prefix
     * @param session
     * @return
     */
    public static String sessionKey(String prefix, Session session) {
        return prefix + session.getId();
    }
    /**
     * 把 sessionId 序列化为 string,因为 Redis 的 key 和 value 必须同时为 string 或者 byte[].
     * @param session
     * @return
     */
    public static String sessionIdToString(Session session) {
        byte[] content = org.apache.commons.lang3.SerializationUtils.serialize(session.getId());
        return org.apache.shiro.codec.Base64.encodeToString(content);
    }
    /**
     * 反序列化得到 sessionId.
     * @param value
     * @return
     */
    public static Serializable sessionIdFromString(String value) {
        byte[] content = org.apache.shiro.codec.Base64.decode(value);
        return org.apache.commons.lang3.SerializationUtils.deserialize(content);
    }
    /**
     * 把 session 序列化为 string,因为 Redis 的 key 和 value 必须同时为 string 或者 byte[].
     * @param value
     * @return
     */
    public static Session sessionToString(String value) {
        byte[] content = org.apache.shiro.codec.Base64.decode(value);
        return org.apache.commons.lang3.SerializationUtils.deserialize(content);
    }
    /**
     * 反序列化得到 session.
     * @param session
     * @return
     */
    public static String sessionFromString(Session session) {
        byte[] content = org.apache.commons.lang3.SerializationUtils.serialize((SimpleSession) session);
        return org.apache.shiro.codec.Base64.encodeToString(content);
    }
}配置 shiro-spring.xml,使用我们实现的 ShiroSessionDAO 存储和读取 Session,其需要一个 RedisManager 来访问 Redis。
    <!-- 安全管理器 -->
    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <property name="realm" ref="userRealm"/>
        <!-- 需要使用cache的话加上这句 -->
        <property name="cacheManager" ref="ehCacheManager" />
        <property name="sessionManager" ref="sessionManager" />
    </bean>
    <!-- 需要使用cache的话加上这句 -->
    <bean id="ehCacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
        <property name="cacheManagerConfigFile" value="classpath:ehcache-shiro.xml" />
    </bean>
    <!--保持 Session 到 Redis-->
    <bean id="redisManager" class="shiro.RedisManager"/>
    <bean id="redisSessionDAO" class="shiro.RedisSessionDAO">
        <property name="redisManager" ref="redisManager"/>
        <property name="timeToLiveSeconds" value="180"/>
    </bean>
    <bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
        <property name="sessionDAO" ref="redisSessionDAO" />
    </bean>