1. 程式人生 > >springboot+nginx+shiro叢集共享session

springboot+nginx+shiro叢集共享session

  今天在搭建springboot+shiro+nginx的多伺服器應用時,遇到了在一個伺服器shiro認證通過之後另一個伺服器沒有認證shiro,所以在訪問另一個伺服器的時候會丟擲shiro未認證的錯誤,後來發現是shiro中session沒有共享的問題。

網上找了一些部落格文件也描述的不是很清楚,因為我本身也是用的redis叢集,所以在看到網上序列化session儲存的時候就想到了將單機redis改redis叢集來使用,結果反而還被自己坑了,當然也是部落格上面描述的不是很清楚。這邊整理了一下之後再分享一下這個坑的解決方法。

首先必須有springboot的一些基本配置以及springboot整合shiro的一些配置。這裡還需要引入shiro-redis這個jar包

<!-- shiro+redis快取外掛 -->
<dependency>
	<groupId>org.crazycake</groupId>
	<artifactId>shiro-redis</artifactId>
	<version>2.8.24</version>
</dependency>

下面是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<HostAndPort> nodes = new HashSet<HostAndPort>();
		// 迴圈陣列把叢集節點新增到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<Entry<String, JedisPool>> 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<String, String> 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宕機的情況就不是很好處理了。