org.apache.shiro
shiro-spring-boot-web-starter
1.6.0
com.github.theborakompanioni
thymeleaf-extras-shiro
2.0.0
org.redisson
redisson-spring-boot-starter
3.16.3
org.springframework.session
spring-session-data-redis
二、使用redisson的方式实现shiro的cache和cachemanager
- RedissonShiroCacheManager
@Component
public class RedissonShiroCacheManager implements CacheManager, Initializable {
private boolean allowNullValues = true;
private Codec codec = new JsonJacksonCodec();
private RedissonClient redisson;
private String configLocation;
private Map configMap = new ConcurrentHashMap<>();
private ConcurrentMap instanceMap = new ConcurrentHashMap<>();
public RedissonShiroCacheManager(){}
public RedissonShiroCacheManager(RedissonClient redisson){
this(redisson, (String)null, null);
}
public RedissonShiroCacheManager(RedissonClient redisson, Map config) {
this(redisson, config, null);
}
public RedissonShiroCacheManager(RedissonClient redisson, Map config, Codec codec) {
this.redisson = redisson;
this.configMap = (Map) config;
if (codec != null) {
this.codec = codec;
}
}
public RedissonShiroCacheManager(RedissonClient redisson, String configLocation) {
this(redisson, configLocation, null);
}
public RedissonShiroCacheManager(RedissonClient redisson, String configLocation, Codec codec) {
this.redisson = redisson;
this.configLocation = configLocation;
if (codec != null) {
this.codec = codec;
}
}
protected CacheConfig createDefaultConfig() {
return new CacheConfig();
}
@Override
public Cache getCache(String name) throws CacheException {
Cache cache = this.instanceMap.get(name);
if (cache != null) {
return cache;
}
CacheConfig config = this.configMap.get(name);
if (config == null) {
config = createDefaultConfig();
configMap.put(name, config);
}
if (config.getMaxIdleTime() == 0 && config.getTTL() == 0 && config.getMaxSize() == 0) {
return createMap(name, config);
}
return createMapCache(name, config);
}
private Cache createMap(String name, CacheConfig config) {
RMap map = getMap(name, config);
Cache cache = new RedissonShiroCache<>(map, this.allowNullValues);
Cache oldCache = this.instanceMap.putIfAbsent(name, cache);
if (oldCache != null) {
cache = oldCache;
}
return cache;
}
protected RMap getMap(String name, CacheConfig config) {
if (this.codec != null) {
return this.redisson.getMap(name, this.codec);
}
return this.redisson.getMap(name);
}
private Cache createMapCache(String name, CacheConfig config) {
RMapCache map = getMapCache(name, config);
Cache cache = new RedissonShiroCache<>(map, config, this.allowNullValues);
Cache oldCache = this.instanceMap.putIfAbsent(name, cache);
if (oldCache != null) {
cache = oldCache;
} else {
map.setMaxSize(config.getMaxSize());
}
return cache;
}
protected RMapCache getMapCache(String name, CacheConfig config) {
if (this.codec != null) {
return this.redisson.getMapCache(name, this.codec);
}
return redisson.getMapCache(name);
}
@Override
public void init() {
if (this.configLocation == null) {
return;
}
try {
this.configMap = (Map) CacheConfig.fromJSON(ResourceUtils.getInputStreamForPath(this.configLocation));
} catch (IOException e) {
// try to read yaml
try {
this.configMap = (Map) CacheConfig.fromYAML(ResourceUtils.getInputStreamForPath(this.configLocation));
} catch (IOException e1) {
throw new IllegalArgumentException(
"Could not parse cache configuration at [" + configLocation + "]", e1);
}
}
}
public void setConfig(Map config) {
this.configMap = (Map) config;
}
public RedissonClient getRedisson() {
return redisson;
}
public void setRedisson(RedissonClient redisson) {
this.redisson = redisson;
}
public Codec getCodec() {
return codec;
}
public void setCodec(Codec codec) {
this.codec = codec;
}
public String getConfigLocation() {
return configLocation;
}
public void setConfigLocation(String configLocation) {
this.configLocation = configLocation;
}
public boolean isAllowNullValues() {
return allowNullValues;
}
public void setAllowNullValues(boolean allowNullValues) {
this.allowNullValues = allowNullValues;
}
}
- RedissonShiroCache
public class RedissonShiroCache三、开启springsession,接管shiro-sessionimplements Cache { private RMapCache mapCache; private final RMap map; private CacheConfig config; private final boolean allowNullValues; private final AtomicLong hits = new AtomicLong(); private final AtomicLong misses = new AtomicLong(); public RedissonShiroCache(RMapCache mapCache, CacheConfig config, boolean allowNullValues) { this.mapCache = mapCache; this.map = mapCache; this.config = config; this.allowNullValues = allowNullValues; } public RedissonShiroCache(RMap map, boolean allowNullValues) { this.map = map; this.allowNullValues = allowNullValues; } @Override public V get(K key) throws CacheException { Object value = this.map.get(key); if (value == null) { addCacheMiss(); } else { addCacheHit(); } return fromStorevalue(value); } @Override public V put(K key, V value) throws CacheException { Object previous; if (!allowNullValues && value == null) { if (mapCache != null) { previous = mapCache.remove(key); } else { previous = map.remove(key); } return fromStorevalue(previous); } Object val = toStorevalue(value); if (mapCache != null) { previous = mapCache.put(key, val, config.getTTL(), TimeUnit.MILLISECONDS, config.getMaxIdleTime(), TimeUnit.MILLISECONDS); } else { previous = map.put(key, val); } return fromStorevalue(previous); } public void fastPut(K key, V value) throws CacheException { if (!allowNullValues && value == null) { if (mapCache != null) { mapCache.fastRemove(key); } else { map.fastRemove(key); } return; } Object val = toStorevalue(value); if (mapCache != null) { mapCache.fastPut(key, val, config.getTTL(), TimeUnit.MILLISECONDS, config.getMaxIdleTime(), TimeUnit.MILLISECONDS); } else { map.fastPut(key, val); } } public V putIfAbsent(K key, V value) throws CacheException { Object previous; if (!allowNullValues && value == null) { if (mapCache != null) { previous = mapCache.get(key); } else { previous = map.get(key); } return fromStorevalue(previous); } Object val = toStorevalue(value); if (mapCache != null) { previous = mapCache.putIfAbsent(key, val, config.getTTL(), TimeUnit.MILLISECONDS, config.getMaxIdleTime(), TimeUnit.MILLISECONDS); } else { previous = map.putIfAbsent(key, val); } return fromStorevalue(previous); } public boolean fastPutIfAbsent(K key, V value) throws CacheException { if (!allowNullValues && value == null) { return false; } Object val = toStorevalue(value); if (mapCache != null) { return mapCache.fastPutIfAbsent(key, val, config.getTTL(), TimeUnit.MILLISECONDS, config.getMaxIdleTime(), TimeUnit.MILLISECONDS); } else { return map.fastPutIfAbsent(key, val); } } @Override public V remove(K key) throws CacheException { Object previous = this.map.remove(key); return fromStorevalue(previous); } public long fastRemove(K ... keys) { return this.map.fastRemove(keys); } @Override public void clear() throws CacheException { this.map.clear(); } @Override public int size() { return this.map.size(); } @Override public Set keys() { return this.map.readAllKeySet(); } @Override public Collection values() { Collection
@SpringBootApplication
//使用EnableRedisHttpSession注解开启spring分布式session,该类的作用是配置org.springframework.session.web.http.SessionRepositoryFilter进行请求拦截
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 3600)
@EnableCaching
public class GeApplication {
public static void main(String[] args) {
SpringApplication.run(GeApplication.class, args);
}
}
- shiroconfig类的配置
public class ShiroConfig {
@Autowired
RedissonClient redissonClient;
@Bean
public SessionManager sessionManager(){
return new ServletContainerSessionManager();
}
@Bean(name = "shiroDialect")
public ShiroDialect shiroDialect() {
return new ShiroDialect();
}
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 设置securityManager
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 设置登录url
shiroFilterFactoryBean.setLoginUrl("/login");
// 设置主页url
shiroFilterFactoryBean.setSuccessUrl("/shiro");
// 设置未授权的url
shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized");
Map filterChainDefinitionMap = new linkedHashMap<>();
// 注销登录
linkedHashMap filtersMap = new linkedHashMap<>();
filtersMap.put("logout", shiroLogoutFilter());
shiroFilterFactoryBean.setFilters(filtersMap);
filterChainDefinitionMap.put("/loginOut", "logout");
// 开放登录接口
filterChainDefinitionMap.put("/doLogin", "anon");
........
// 其余url全部拦截,必须放在最后
filterChainDefinitionMap.put("
@Bean
@DependsOn({"lifecycleBeanPostProcessor"})
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreatorShiro() {
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
@Bean
public static LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager());
return authorizationAttributeSourceAdvisor;
}
}
四、自定义shiro的登出行为,做一些springsession中与用户有关的缓存操作
- 自定义shiro只需要实现LogoutFilter,重写prehandle方法即可
public class ShiroLogoutFilter extends LogoutFilter {
private final RedissonClient redissonClient;
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
Subject subject = getSubject(request,response);
try {
HttpServletRequest httpReq = (HttpServletRequest) request;
HttpSession session = httpReq.getSession();
//根据spring session的信息,删除用户的缓存
String sessionKey="spring:session:sessions:" + session.getId();
String expires="spring:session:sessions:expires:" + session.getId();
redissonClient.getBucket(sessionKey).delete();
redissonClient.getBucket(expires).delete();
} catch (Throwable t) {
t.printStackTrace();
}
//登出
subject.logout();
//获取登出后重定向到的地址
String redirectUrl = getRedirectUrl(request,response,subject);
//重定向
issueRedirect(request,response,redirectUrl);
//配置登出后重定向的地址,等出后配置跳转到登录接口
return false;
}
public ShiroLogoutFilter(RedissonClient redissonClient){
this.redissonClient=redissonClient;
}
}
五、最后,redissonnclient用于操作redis
@Configuration
public class RedissonConfig {
@Bean(destroyMethod="shutdown")
public RedissonClient getRedissonClient() throws IOException {
ResourceLoader loader = new DefaultResourceLoader();
Resource resource = loader.getResource("redisson.yml");
Config config = Config.fromYAML(resource.getInputStream());
return Redisson.create(config);
}
}
- redisson.yml
singleServerConfig: idleConnectionTimeout: 10000 #连接空闲超时,单位:毫秒 默认:10000 connectTimeout: 10000 #连接超时,单位:毫秒。默认:10000 timeout: 3000 #命令等待超时,单位:毫秒 默认:3000 retryAttempts: 3 #命令失败重试次数 retryInterval: 1500 #命令重试发送时间间隔,单位:毫秒 # lockWatchdogTimeout: 30000 #监控锁的看门狗超时,单位:毫秒 该参数只适用于分布式锁的加锁请求中未明确使用leaseTimeout参数的情况。如果该看门口未使用lockWatchdogTimeout去重新调整一个分布式锁的lockWatchdogTimeout超时,那么这个锁将变为失效状态。这个参数可以用来避免由Redisson客户端节点宕机或其他原因造成死锁的情况。 password: xxxxxxxxx clientName: null #客户端名称 subscriptionsPerConnection: 5 #单个连接最大订阅数量 address: "redis://ip:port" subscriptionConnectionMinimumIdleSize: 1 #从节点发布和订阅连接的最小空闲连接数 subscriptionConnectionPoolSize: 50 #从节点发布和订阅连接池大小 # 集群模式下不支持该选项 database: 10 dnsMonitoringInterval: 5000 #DNS监控间隔,单位:毫秒 在启用该功能以后,Redisson将会监测DNS的变化情况 sslEnableEndpointIdentification: true #启用SSL终端识别,默认为true threads: 0 #线程池数量 默认值: 当前处理核数量 * 2 nettyThreads: 0 #Netty线程池数量 默认值: 当前处理核数量 * 2 ,这个线程池数量是在一个Redisson实例内,被其创建的所有分布式数据类型和服务,以及底层客户端所一同共享的线程池里保存的线程数量。 codec: #编码 默认值:org.redisson.codec.JsonJacksonCodec,Redisson的对象编码类是用于将对象进行序列化和反序列化,以实现对该对象在Redis里的读取和存储 !六、总结{} "transportMode": #传输模式 默认:TransportMode.NIO linux系统下可以使用RPOLL,性能高 "NIO"
通过spring-session集成shiro,我们可以实现用户权限控制,认证缓存,将session的存储位置由tomcat等web容器剥离至redis或者mysql中进行持久化,这样即使微服务中某台机器宕机,重启,也不会丢失用户信息,进一步提高系统的健壮性。同时,本例子只在在单机redis的情况下使用,各位可以进一步将redis扩展到redis集群,在redisson的帮助下,使用起来也非常方便



