今天在搭建springboot+shiro+nginx的多服务器应用时,遇到了在一个服务器shiro认证通过之后另一个服务器没有认证shiro,所以在访问另一个服务器的时候会抛出shiro未认证的错误,后来发现是shiro中session没有共享的问题。
网上找了一些博客文档也描述的不是很清楚,因为我本身也是用的redis集群,所以在看到网上序列化session存储的时候就想到了将单机redis改redis集群来使用,结果反而还被自己坑了,当然也是博客上面描述的不是很清楚。这边整理了一下之后再分享一下这个坑的解决方法。
首先必须有springboot的一些基本配置以及springboot整合shiro的一些配置。这里还需要引入shiro-redis这个jar包
org.crazycake
shiro-redis
2.8.24
下面是shiroCofig的一些配置 由于shiroConfig类中LifecycleBeanPostProcessor这个bean会影响@Value注入属性,所以这边把该bean配置移到了@SpringbootApplication中
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.crazycake.shiro.RedisCacheManager;
import org.crazycake.shiro.RedisManager;
import org.crazycake.shiro.RedisSessionDAO;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.config.MethodInvokingFactoryBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import club.pinea.school.shiro.ShiroDBRealm;
import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.JedisCluster;
import redis.clients.jedis.JedisPool;
@Configuration
public class ShiroConfig {
@Value("${spring.redis.cluster.nodes}")
private String clusterNodes;
@Bean
public ShiroDBRealm shiroDbRealm() {
return new ShiroDBRealm();
}
@Bean
public JedisCluster jedisCluster() {
// 截取集群节点
String[] cluster = clusterNodes.split(",");
// 创建set集合
Set nodes = new HashSet();
// 循环数组把集群节点添加到set集合中
for (String node : cluster) {
String[] host = node.split(":");
// 添加集群节点
nodes.add(new HostAndPort(host[0], Integer.parseInt(host[1])));
}
JedisCluster jc = new JedisCluster(nodes);
return jc;
}
/**
* 权限管理,配置主要是Realm的管理认证
*/
@Bean
public DefaultWebSecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(shiroDbRealm());
// 自定义缓存实现 使用redis
securityManager.setCacheManager(cacheManager());
// 自定义session管理 使用redis
securityManager.setSessionManager(SessionManager());
return securityManager;
}
/**
* shiro session的管理
*/
public DefaultWebSessionManager SessionManager() {
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
sessionManager.setSessionDAO(redisSessionDAO());
return sessionManager;
}
/**
* 配置shiro redisManager
*
* @return
*/
public RedisManager redisManager() {
RedisManager redisManager = new RedisManager();
JedisCluster cluster = jedisCluster();
Iterator> iterator = cluster.getClusterNodes().entrySet().iterator();
JedisPool pool = null;
if(iterator.hasNext()) {
pool = iterator.next().getValue();
}
redisManager.setJedisPool(pool);
redisManager.setExpire(1800);// 配置过期时间
// redisManager.setTimeout(timeout);
// redisManager.setPassword(password);
return redisManager;
}
/**
* cacheManager 缓存 redis实现
*
* @return
*/
public RedisCacheManager cacheManager() {
RedisCacheManager redisCacheManager = new RedisCacheManager();
redisCacheManager.setRedisManager(redisManager());
return redisCacheManager;
}
/**
* RedisSessionDAO shiro sessionDao层的实现 通过redis
*/
public RedisSessionDAO redisSessionDAO() {
RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
redisSessionDAO.setRedisManager(redisManager());
return redisSessionDAO;
}
/**
* Filter工厂,设置对应的过滤条件和跳转条件
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
Map map = new HashMap<>();
map.put("/test/**", "anon");//测试不需要认证
map.put("/login/**", "anon");//登录接口不需要认证
map.put("/noUser", "anon");//不需要认证
// 对所有用户认证
map.put("/**", "authc");
// 登录
shiroFilterFactoryBean.setLoginUrl("/noUser");
// 首页
shiroFilterFactoryBean.setSuccessUrl("/index");
// 没有权限跳转的页面
shiroFilterFactoryBean.setUnauthorizedUrl("/unauth");
// 错误页面,认证不通过跳转
shiroFilterFactoryBean.setUnauthorizedUrl("/error");
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
return shiroFilterFactoryBean;
}
/**
* 加入注解的使用,不加入这个注解不生效
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
/**
* 在方法中 注入 securityManager,进行代理控制
* @return
*/
@Bean
public MethodInvokingFactoryBean methodInvokingFactoryBean() {
MethodInvokingFactoryBean methodInvokingFactoryBean = new MethodInvokingFactoryBean();
methodInvokingFactoryBean.setStaticMethod("org.apache.shiro.SecurityUtils.setSecurityManager");
methodInvokingFactoryBean.setArguments(securityManager());
return methodInvokingFactoryBean;
}
@Bean
@DependsOn("lifecycleBeanPostProcessor")
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
return new DefaultAdvisorAutoProxyCreator();
}
// /**
// * 保证实现了Shiro内部lifecycle函数的bean执行
// * 这边由于运行的时候会报springboot配置文件属性注入为空的问题,所以将该配置移到了springbootApplication中
// * @return
// */
// @Bean
// public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
// return new LifecycleBeanPostProcessor();
// }
/**
* 启用shrio授权注解拦截方式
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager());
return authorizationAttributeSourceAdvisor;
}
}
这里是启动类SpringbootApplication的配置
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@SpringBootApplication
@Configuration
@MapperScan("club.pinea.school.mapper")//mapper扫描配置
public class SchoolApplication {
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
public static void main(String[] args) {
SpringApplication.run(SchoolApplication.class, args);
}
}
这边配置完之后再启动,终于解决了问题,但是还是有一些不好的地方是这个插件只能支持单机redis。并不能支持集群。我也联系了插件的作者,给他提了这方面的建议,也希望以后这个插件可以支持redis的集群支持。不然难免有可能出现单机redis宕机的情况就不是很好处理了。