应用如果做负载均衡,集群间session需要共享,如果session没有共享,用户登录系统以后session保存在登录的应用里面,其他应用里面没有session,没有登陆状态,访问会失败。下面介绍一个SpringBoot下面基于Shiro的session共享方案。
方案的全部代码在码云上面。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 redisTemplate() {
RedisTemplate 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 caches = new ConcurrentHashMap();
// 注入Spring的缓存管理器
@Autowired
private org.springframework.cache.CacheManager cacheManager;
@Override
public Cache 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 implements Cache {
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 keys() {
throw new RuntimeException("");
}
@Override
public Collection 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
致此,完成使用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;
}
7. Shiro缓存使用Spring缓存方案优缺点简单总结
- 优点,缓存使用Spring自身的缓存,Redis读写以及序列化等全部由Spring实现,简化开发,方便Spring项目之间的整合。
- 缺点,不同版本Spring或者不同技术栈之间的应用做session共享,可能会由于Redis读写方案不一致或序列化方案不一致等问题,无法兼容。
- 二备方案,session共享如果要兼容老板本项目,可以重写shiro的缓存管理器(上面代码中的ShiroRedisCacheManager类)和缓存(ShiroRedisCache)两个类,使用兼容老版本Redis读写及序列化的方法代替Spring缓存。
转载于:https://my.oschina.net/qwzhang01/blog/1620339