应用如果做负载均衡,集群间session需要共享,如果session没有共享,用户登录系统以后session保存在登录的应用里面,其他应用里面没有session,没有登陆状态,访问会失败。下面介绍一个SpringBoot下面基于Shiro的session共享方案。
方案的全部代码在GitHub上面。https://github.com/qwzhang01/bkcell_security
思路
- 使用Shiro托管应用session
- 使用Redis管理Shiro缓存
实现步骤
- 设置项目缓存为Redis,这样Spring项目的缓存就都会存在Redis
- 设置应用session由Shiro托管
- 实现Shiro的缓存管理器CacheManger接口,将Spring应用缓存管理器注入shiro缓存管理器,这样shiro的缓存都由Spring处理
- 实现Shiro的Cache接口,将Spring的缓存工具类注入,使Shiro对缓存信息的存取由Spring的缓存实现
- 实现Shiro的EnterpriseCacheSessionDAO类,重写Shiro对于session的CRUD,使用重写的Shiro的Cache接口,对session的CRUD在Redis中处理
具体实现
1. 配置Redis
在application.properties文件中添加如下内容,配置Redis的host 密码 端口号等
spring.redis.host=192.168.10.135 spring.redis.port=6379 spring.redis.password=000000
添加Redis缓存配置类
import com.canyou.bkcell.common.kit.PropKit; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.CachingConfigurerSupport; import org.springframework.cache.annotation.EnableCaching; import org.springframework.cache.interceptor.KeyGenerator; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.cache.RedisCacheManager; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; @Configuration @EnableCaching public class RedisConfig extends CachingConfigurerSupport { @Autowired private RedisConnectionFactory factory; @Override @Bean public KeyGenerator keyGenerator() { return (target, method, params) -> { StringBuilder sb = new StringBuilder(); sb.append(target.getClass().getName()); sb.append(method.getName()); for (Object obj : params) { sb.append(obj.toString()); } return sb.toString(); }; } @Bean public CacheManager cacheManager(RedisTemplate redisTemplate) { RedisCacheManager rcm = new RedisCacheManager(redisTemplate); rcm.setDefaultExpiration(PropKit.getInt("spring.redis.timeout") * 60); return rcm; } @Bean public RedisTemplate<String, Object> redisTemplate() { RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>(); redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setHashKeySerializer(new StringRedisSerializer()); redisTemplate.setHashValueSerializer(new StringRedisSerializer()); redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer(this.getClass().getClassLoader())); redisTemplate.setConnectionFactory(factory); return redisTemplate; } }
通过以上两步,应用的缓存实现使用Redis。
2. 配置Shiro,由Shiro托管应用session
在Shiro的SecurityManager中注入SessionManager
@Bean public DefaultWebSessionManager sessionManager() { ShiroSessionManager sessionManager = new ShiroSessionManager(); sessionManager.setSessionDAO(sessionDao()); sessionManager.setSessionIdUrlRewritingEnabled(false); //设置session过期时间为1小时(单位:毫秒),默认为30分钟 sessionManager.setGlobalSessionTimeout(PropKit.getInt("spring.redis.session.timeout") * 60 * 1000); sessionManager.setDeleteInvalidSessions(true); sessionManager.setCacheManager(shiroRedisCacheManager()); sessionManager.setSessionValidationSchedulerEnabled(false); Cookie sessionIdCookie = sessionManager.getSessionIdCookie(); sessionIdCookie.setPath("/"); sessionIdCookie.setName("csid"); sessionManager.setSessionIdCookieEnabled(true); sessionManager.setSessionIdUrlRewritingEnabled(false); return sessionManager; } @Bean public SecurityManager securityManager() { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(authRealm()); securityManager.setCacheManager(shiroRedisCacheManager()); // 设置通过shiro管理应用session securityManager.setSessionManager(sessionManager()); return securityManager; }
3. 实现Shiro的缓存管理器CacheManger接口,将Spring应用缓存管理器注入shiro缓存管理器,这样shiro的缓存都由Spring处理
import org.apache.shiro.cache.Cache; import org.apache.shiro.cache.CacheException; import org.apache.shiro.cache.CacheManager; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Component; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; @Component @Qualifier("shiroRedisCacheManager") public class ShiroRedisCacheManager implements CacheManager { private final ConcurrentMap<String, Cache> caches = new ConcurrentHashMap<String, Cache>(); // 注入Spring的缓存管理器 @Autowired private org.springframework.cache.CacheManager cacheManager; @Override public <K, V> Cache<K, V> getCache(String name) throws CacheException { Cache cache = caches.get(name); if (cache == null) { org.springframework.cache.Cache springCache = cacheManager.getCache(name); // 通过spring的缓存管理器,获取缓存,将缓存注入Redis的缓存中 cache = new ShiroRedisCache(springCache); caches.put(name, cache); } return cache; } }
4. Shiro的缓存类
import com.canyou.bkcell.common.kit.ByteKit; import org.apache.shiro.cache.Cache; import org.apache.shiro.cache.CacheException; import java.util.Collection; import java.util.Set; public class ShiroRedisCache<K, V> implements Cache<K, V> { private String keyPrefix = "shiro_redis_session:"; private org.springframework.cache.Cache cache; public ShiroRedisCache(org.springframework.cache.Cache springCache) { this.cache = springCache; } public void setKeyPrefix(String keyPrefix) { this.keyPrefix = keyPrefix; } private String genKey(K key) { return (keyPrefix + new String(ByteKit.toByte(key))); } @Override public V get(K key) throws CacheException { if (key == null) { return null; } org.springframework.cache.Cache.ValueWrapper valueWrapper = cache.get(genKey(key)); if (valueWrapper == null) { return null; } V v = (V) valueWrapper.get(); return v; } @Override public V put(K key, V value) throws CacheException { cache.put(genKey(key), value); return value; } @Override public V remove(K key) throws CacheException { V v = (V) cache.get(genKey(key)).get(); cache.evict(genKey(key)); return v; } @Override public void clear() throws CacheException { cache.clear(); } @Override public int size() { throw new RuntimeException(""); } @Override public Set<K> keys() { throw new RuntimeException(""); } @Override public Collection<V> values() { throw new RuntimeException(""); } }
5. 重写SessionDAO,实现session的CRUD功能
import com.canyou.bkcell.common.kit.ServletKit; import org.apache.shiro.cache.Cache; import org.apache.shiro.session.Session; import org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO; import javax.servlet.http.HttpServletRequest; import java.io.Serializable; public class RedisSessionDao extends EnterpriseCacheSessionDAO { private Cache cache() { Cache<Object, Object> cache = getCacheManager().getCache(this.getClass().getName()); return cache; } @Override protected Serializable doCreate(Session session) { Serializable sessionId = super.doCreate(session); cache().put(sessionId.toString(), session); return sessionId; } @Override protected Session doReadSession(Serializable sessionId) { Session session = null; HttpServletRequest request = ServletKit.getRequest(); if (request != null){ String uri = request.getServletPath(); if (ServletKit.isStaticFile(uri)){ return null; } session = (Session)request.getAttribute("session_"+sessionId); } if (session == null) { session = super.doReadSession(sessionId); } if (session == null) { session = (Session) cache().get(sessionId.toString()); } return session; } @Override protected void doUpdate(Session session) { HttpServletRequest request = ServletKit.getRequest(); if (request != null) { String uri = request.getServletPath(); if (ServletKit.isStaticFile(uri)) { return; } } super.doUpdate(session); cache().put(session.getId().toString(), session); } @Override protected void doDelete(Session session) { super.doDelete(session); cache().remove(session.getId().toString()); } }
致此,完成使用Redis缓存Shiro授权认证信息,搭建集群权限系统。
6. 简单优化,减少session的redis读取次数
shiro的session存在redis里面后,一次Request对session有很多次读取操作,同时静态资源的访问等都会读取session,虽然redis的性能与内存一样,但是redis毕竟存在网络传输的过程。因此在sessionDAO里面优化的session读操作,减少不必要的在redis读取次数。
1) 优化思路
- 过滤静态资源,请求静态资源的时候不读取session
- 读取session先通过Request域获取,如果Request域中不存时,再通过Redis读取获取session。
2)实现步骤及具体实现
1.添加Servlet工具类,实现在任意位置获取Request,添加判断请求uri是否是静态资源方法。
import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import org.springframework.web.servlet.resource.ResourceUrlProvider; import javax.servlet.http.HttpServletRequest; public class ServletKit { public static HttpServletRequest getRequest() { return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); } public static boolean isStaticFile(String uri) { ResourceUrlProvider resourceUrlProvider = SpringContextKit.getBean(ResourceUrlProvider.class); String staticUri = resourceUrlProvider.getForLookupPath(uri); return staticUri != null; } }
2.在sessionDao的doReadSession操作中过滤静态资源代码,请求uri如果是静态资源,session返回null;读取session操作先获取Request域中的session,如果获取不到,再读取Redis缓存。
@Override protected Session doReadSession(Serializable sessionId) { Session session = null; // 获取本次Request HttpServletRequest request = ServletKit.getRequest(); if (request != null){ String uri = request.getServletPath(); // 过滤静态资源请求 if (ServletKit.isStaticFile(uri)){ return null; } // 在Request域中获取session session = (Session)request.getAttribute("session_"+sessionId); } if (session == null) { session = super.doReadSession(sessionId); } if (session == null) { session = (Session) cache().get(sessionId.toString()); } return session; }