一、pom
<!-- Shiro权限验证 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
<version>1.6.0</version>
</dependency>
<!-- 使用Shiro<Tag>-->
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.0.0</version>
</dependency>
<!--redisson-->
<!-- https://mvnrepository.com/artifact/org.redisson/redisson-spring-boot-starter -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.16.3</version>
</dependency>
<!--session-redis-->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
二、使用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<String, CacheConfig> configMap = new ConcurrentHashMap<>();
private ConcurrentMap<String, Cache> instanceMap = new ConcurrentHashMap<>();
public RedissonShiroCacheManager(){}
public RedissonShiroCacheManager(RedissonClient redisson){
this(redisson, (String)null, null);
}
public RedissonShiroCacheManager(RedissonClient redisson, Map<String, ? extends CacheConfig> config) {
this(redisson, config, null);
}
public RedissonShiroCacheManager(RedissonClient redisson, Map<String, ? extends CacheConfig> config, Codec codec) {
this.redisson = redisson;
this.configMap = (Map<String, CacheConfig>) 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 <K, V> Cache<K, V> getCache(String name) throws CacheException {
Cache<K, V> 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 <K, V> Cache<K, V> createMap(String name, CacheConfig config) {
RMap<K, Object> map = getMap(name, config);
Cache<K, V> cache = new RedissonShiroCache<>(map, this.allowNullValues);
Cache<K, V> oldCache = this.instanceMap.putIfAbsent(name, cache);
if (oldCache != null) {
cache = oldCache;
}
return cache;
}
protected <K> RMap<K, Object> getMap(String name, CacheConfig config) {
if (this.codec != null) {
return this.redisson.getMap(name, this.codec);
}
return this.redisson.getMap(name);
}
private <K, V> Cache<K, V> createMapCache(String name, CacheConfig config) {
RMapCache<K, Object> map = getMapCache(name, config);
Cache<K, V> cache = new RedissonShiroCache<>(map, config, this.allowNullValues);
Cache<K, V> oldCache = this.instanceMap.putIfAbsent(name, cache);
if (oldCache != null) {
cache = oldCache;
} else {
map.setMaxSize(config.getMaxSize());
}
return cache;
}
protected <K> RMapCache<K, Object> 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<String, CacheConfig>) CacheConfig.fromJSON(ResourceUtils.getInputStreamForPath(this.configLocation));
} catch (IOException e) {
// try to read yaml
try {
this.configMap = (Map<String, CacheConfig>) CacheConfig.fromYAML(ResourceUtils.getInputStreamForPath(this.configLocation));
} catch (IOException e1) {
throw new IllegalArgumentException(
"Could not parse cache configuration at [" + configLocation + "]", e1);
}
}
}
public void setConfig(Map<String, ? extends CacheConfig> config) {
this.configMap = (Map<String, CacheConfig>) 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<K, V> implements Cache<K, V> {
private RMapCache<K, Object> mapCache;
private final RMap<K, Object> map;
private CacheConfig config;
private final boolean allowNullValues;
private final AtomicLong hits = new AtomicLong();
private final AtomicLong misses = new AtomicLong();
public RedissonShiroCache(RMapCache<K, Object> mapCache, CacheConfig config, boolean allowNullValues) {
this.mapCache = mapCache;
this.map = mapCache;
this.config = config;
this.allowNullValues = allowNullValues;
}
public RedissonShiroCache(RMap<K, Object> 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<K> keys() {
return this.map.readAllKeySet();
}
@Override
public Collection<V> values() {
Collection<Object> innerValues = this.map.readAllValues();
Collection<V> res = new ArrayList<>(innerValues.size());
for (Object val : innerValues) {
res.add(fromStoreValue(val));
}
return res;
}
protected V fromStoreValue(Object storeValue) {
if (storeValue instanceof NullValue) {
return null;
}
return (V) storeValue;
}
protected Object toStoreValue(V userValue) {
if (userValue == null) {
return NullValue.INSTANCE;
}
return userValue;
}
/** The number of get requests that were satisfied by the cache.
* @return the number of hits
*/
long getCacheHits(){
return this.hits.get();
}
/** A miss is a get request that is not satisfied.
* @return the number of misses
*/
long getCacheMisses(){
return this.misses.get();
}
private void addCacheHit(){
this.hits.incrementAndGet();
}
private void addCacheMiss(){
this.misses.incrementAndGet();
}
}
三、开启springsession,接管shiro-session
@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;
/**
* ServletContainerSessionManager 类中有一个方法是isServletContainerSessions(),返回的是true.
* DefaultWebSessionManager类中有一个方法是isServletContainerSessions(),返回是false。
* 因为实现了Spring Session,代理了所有Servlet里的session,所以这里的session一定是Servlet能控制的,否则无法实现Spring session共享。
*/
@Bean
public SessionManager sessionManager(){
return new ServletContainerSessionManager();
}
/**
* 注入这个是是为了在thymeleaf中使用shiro的自定义tag。
*/
@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<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
// 注销登录
LinkedHashMap<String, Filter> filtersMap = new LinkedHashMap<>();
filtersMap.put("logout", shiroLogoutFilter());
shiroFilterFactoryBean.setFilters(filtersMap);
filterChainDefinitionMap.put("/loginOut", "logout");
// 开放登录接口
filterChainDefinitionMap.put("/doLogin", "anon");
........
// 其余url全部拦截,必须放在最后
filterChainDefinitionMap.put("/**", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
public ShiroLogoutFilter shiroLogoutFilter(){
ShiroLogoutFilter shiroLogoutFilter = new ShiroLogoutFilter(redissonClient);
//配置登出后重定向的地址,等出后配置跳转到登录接口
shiroLogoutFilter.setRedirectUrl("/login");
return shiroLogoutFilter;
}
@Bean("authenticator")
public SessionsSecurityManager securityManager() {
SessionsSecurityManager securityManager = new DefaultWebSecurityManager();
//设置认证realm
securityManager.setRealm(loginRealm());
// 设置记住我功能
// securityManager.setRememberMeManager(rememberMeManager());
// 设置会话管理器
securityManager.setSessionManager(sessionManager());
return securityManager;
}
@Bean
public RememberMeManager rememberMeManager() {
CookieRememberMeManager rememberMeManager = new CookieRememberMeManager();
//注入自定义cookie(主要是设置寿命, 默认的一年太长)
SimpleCookie simpleCookie = new SimpleCookie("rememberMe");
simpleCookie.setHttpOnly(true);
//设置RememberMe的cookie有效期为7天
simpleCookie.setMaxAge(604800);
rememberMeManager.setCookie(simpleCookie);
//手动设置对称加密秘钥,防止重启系统后系统生成新的随机秘钥,防止导致客户端cookie无效
rememberMeManager.setCipherKey(Base64.decode("6ZmI6I2j3Y+R1aSn5BOlAA=="));
return rememberMeManager;
}
@Bean
public LoginRealm loginRealm() {
LoginRealm loginRealm = new LoginRealm();
//开启缓存处理
loginRealm.setCacheManager(new RedissonShiroCacheManager(redissonClient));
//开启全局缓存
//loginRealm.setCachingEnabled(true);
// //开启认证缓存
// loginRelam.setAuthenticationCachingEnabled(true);
//loginRealm.setAuthenticationCacheName("authenticafdfdfdtionCache");
//认证授权缓存
// loginRealm.setAuthorizationCachingEnabled(true);
// loginRealm.setAuthorizationCacheName("AuthorizationCache");
return loginRealm;
}
/**
* 以下是为了能够使用@RequiresPermission()等标签
* 这里命名为advisorAutoProxyCreatorShiro是因为advisorAutoProxyCreator会与druid冲突
*/
@Bean
@DependsOn({"lifecycleBeanPostProcessor"})
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreatorShiro() {
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
/**
* LifecycleBeanPostProcessor将Initializable和Destroyable的实现类统一在其内部
* 自动分别调用了Initializable.init()和Destroyable.destroy()方法,从而达到管理shiro bean生命周期的目的。
*/
@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;
/**
* @param request
* @param response
* @throws Exception
*/
@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里的读取和存储
!<org.redisson.codec.JsonJacksonCodec> {}
"transportMode": #传输模式 默认:TransportMode.NIO linux系统下可以使用RPOLL,性能高
"NIO"
六、总结
通过spring-session集成shiro,我们可以实现用户权限控制,认证缓存,将session的存储位置由tomcat等web容器剥离至redis或者mysql中进行持久化,这样即使微服务中某台机器宕机,重启,也不会丢失用户信息,进一步提高系统的健壮性。同时,本例子只在在单机redis的情况下使用,各位可以进一步将redis扩展到redis集群,在redisson的帮助下,使用起来也非常方便