SpringBoot+Shiro+Nginx+Tomcat負(fù)載環(huán)境Session共享

一、背景

  • 一般情況下,我們?yōu)榱嗽黾泳W(wǎng)站的性能,會(huì)用Nginx去轉(zhuǎn)發(fā)請(qǐng)求負(fù)載到多臺(tái)Tomcat上,但是這就會(huì)出現(xiàn)一個(gè)問題。當(dāng)你在tomcatA上shiro登錄成功后session保存到了A服務(wù)器上,接下來你的請(qǐng)求可能被分發(fā)到tomcatB,這時(shí)A服務(wù)器中的session無法共享到B中,B會(huì)認(rèn)為你是沒有登錄的,就會(huì)跳轉(zhuǎn)到登錄頁。這就是我們要解決的,將Shiro的Session共享, 使得一處登錄,多處都能得到這個(gè)登錄態(tài)。此處我們使用redis來存儲(chǔ)這個(gè)Session。

二、配置代碼

  • 此處關(guān)于redis 的配置我就不寫了。只寫關(guān)于shiro實(shí)現(xiàn)session的相關(guān)代碼
1.配置pom文件
  • 需要引入shiro-redis這個(gè)jar包
<!-- shiro+redis緩存插件 -->
<dependency>
    <groupId>org.crazycake</groupId>
    <artifactId>shiro-redis</artifactId>
    <version>3.2.0</version>
</dependency>
2.修改shiro配置類
  • ShiroConfig類
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.mgt.eis.JavaUuidSessionIdGenerator;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
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.servlet.SimpleCookie;
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.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.LinkedHashMap;
import java.util.Map;

@Configuration
public class ShiroConfig {

    //redis session 過期時(shí)間24小時(shí)
    private int shiroTimeout = 86400;

    @Bean
    public ShiroRedisConfig shiroRedisConfig(){
        return new ShiroRedisConfig();
    }

    @Bean
    public RedisManager redisManager(){
        RedisManager redisManager = new RedisManager();     // crazycake 實(shí)現(xiàn)
        redisManager.setHost(shiroRedisConfig().redisHost + ":" + shiroRedisConfig().redisPort);
        redisManager.setPassword(shiroRedisConfig().redisPwd);
        redisManager.setTimeout(shiroTimeout);
        redisManager.setDatabase(shiroRedisConfig().redisDatabase);
        return redisManager;
    }

    @Bean
    public JavaUuidSessionIdGenerator sessionIdGenerator(){
        return new JavaUuidSessionIdGenerator();
    }

    @Bean
    public RedisSessionDAO sessionDAO(){
        RedisSessionDAO sessionDAO = new RedisSessionDAO(); // crazycake 實(shí)現(xiàn)
        sessionDAO.setExpire(shiroTimeout);
        sessionDAO.setRedisManager(redisManager());
        sessionDAO.setSessionIdGenerator(sessionIdGenerator()); //  Session ID 生成器
        return sessionDAO;
    }

    @Bean
    public SimpleCookie cookie(){
        SimpleCookie cookie = new SimpleCookie("shiro.sesssion"); //  cookie的name,對(duì)應(yīng)的默認(rèn)是 JSESSIONID
        cookie.setMaxAge(shiroTimeout);
        cookie.setHttpOnly(true);
        cookie.setPath("/");        //  path為 / 用于多個(gè)系統(tǒng)共享JSESSIONID
        return cookie;
    }

    @Bean(name="sessionManager")
    public ShiroSessionManager shiroSessionManager() {
        ShiroSessionManager sessionManager = new ShiroSessionManager();
        sessionManager.setGlobalSessionTimeout(24*60*60*1000);    // 設(shè)置session超時(shí),單位毫秒,此處設(shè)置24小時(shí)
        sessionManager.setDeleteInvalidSessions(true);      // 刪除無效session
        sessionManager.setSessionIdCookie(cookie());            // 設(shè)置JSESSIONID
        sessionManager.setSessionDAO(sessionDAO());         // 設(shè)置sessionDAO
        return sessionManager;
    }
//    @Bean(name="sessionManager")
//    public DefaultWebSessionManager defaultWebSessionManager() {
//        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
//        sessionManager.setGlobalSessionTimeout(24*60*60*1000);    // 設(shè)置session超時(shí)
//        sessionManager.setDeleteInvalidSessions(true);      // 刪除無效session
//        sessionManager.setSessionIdCookie(cookie());            // 設(shè)置JSESSIONID
//        sessionManager.setSessionDAO(sessionDAO());         // 設(shè)置sessionDAO
//        return sessionManager;
//    }

    @Bean(name="securityManager")
    public SecurityManager securityManager(@Qualifier("authRealm") CustomerShiroRealm authRealm) {
        System.err.println("--------------shiro已經(jīng)加載----------------");
        DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
        // 設(shè)置realm.
        manager.setRealm(authRealm);
        // 自定義session管理
        manager.setSessionManager(shiroSessionManager());
        return manager;
    }

    @Bean
    public RedisCacheManager redisCacheManager(){
        RedisCacheManager cacheManager = new RedisCacheManager();   // crazycake 實(shí)現(xiàn)
        cacheManager.setRedisManager(redisManager());
        cacheManager.setExpire(shiroTimeout);
        return cacheManager;
    }

    /**
     * 身份認(rèn)證realm; (這個(gè)需要自己寫,賬號(hào)密碼校驗(yàn);權(quán)限等)
     *
     * @return
     */
    //配置自定義的權(quán)限登錄器
    @Bean(name="authRealm")
    public CustomerShiroRealm authRealm(@Qualifier("credentialsMatcher") CredentialsMatcher matcher) {
        CustomerShiroRealm authRealm = new CustomerShiroRealm();
        authRealm.setCredentialsMatcher(matcher);
        return authRealm;
    }

    //配置自定義的密碼比較器
    @Bean(name="credentialsMatcher")
    public CredentialsMatcher credentialsMatcher() {
        return new CredentialsMatcher();
    }

    @Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor(){
        return new LifecycleBeanPostProcessor();
    }
    @Bean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator(){
        DefaultAdvisorAutoProxyCreator creator=new DefaultAdvisorAutoProxyCreator();
        creator.setProxyTargetClass(true);
        return creator;
    }
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(@Qualifier("securityManager") SecurityManager manager) {
        AuthorizationAttributeSourceAdvisor advisor=new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(manager);
        return advisor;
    }

    /**
     * ShiroFilterFactoryBean 處理攔截資源文件問題。
     * 注意:單獨(dú)一個(gè)ShiroFilterFactoryBean配置是或報(bào)錯(cuò)的,以為在
     * 初始化ShiroFilterFactoryBean的時(shí)候需要注入:SecurityManager
     *
     * Filter Chain定義說明 1、一個(gè)URL可以配置多個(gè)Filter,使用逗號(hào)分隔 2、當(dāng)設(shè)置多個(gè)過濾器時(shí),全部驗(yàn)證通過,才視為通過
     * 3、部分過濾器可指定參數(shù),如perms,roles
     *
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilter(@Qualifier("securityManager") SecurityManager manager) {
        ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
        

        // 必須設(shè)置 SecurityManager
        bean.setSecurityManager(manager);

        // 如果不設(shè)置默認(rèn)會(huì)自動(dòng)尋找Web工程根目錄下的"/login.jsp"頁面
        bean.setLoginUrl("/admin/loginPage");
        // 登錄成功后要跳轉(zhuǎn)的鏈接
        bean.setSuccessUrl("/admin/successPage");
        // 未授權(quán)界面;
        bean.setUnauthorizedUrl("/403");

        // 攔截器.
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
        // 配置不會(huì)被攔截的鏈接 順序判斷
        filterChainDefinitionMap.put("/static/**", "anon");
        filterChainDefinitionMap.put("/admin/login", "anon");
        // 配置退出過濾器,其中的具體的退出代碼Shiro已經(jīng)替我們實(shí)現(xiàn)了
        filterChainDefinitionMap.put("/logout", "logout");
        filterChainDefinitionMap.put("/add", "perms[權(quán)限添加]");

        // <!-- 過濾鏈定義,從上向下順序執(zhí)行,一般將 /**放在最為下邊 -->:這是一個(gè)坑呢,一不小心代碼就不好使了;
        // <!-- authc:所有url都必須認(rèn)證通過才可以訪問; anon:所有url都都可以匿名訪問-->
        filterChainDefinitionMap.put("/**", "authc");
        
        bean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        //System.out.println("Shiro攔截器工廠類注入成功");
        return bean;
    }

}


  • ShiroSessionManager類,該類是為了減少redis的訪問次數(shù)
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.SessionKey;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.web.session.mgt.WebSessionKey;
import org.springframework.stereotype.Component;

import javax.servlet.ServletRequest;
import java.io.Serializable;

/**
 * @Auther: LinJiaJia
 * @Date: 2019/3/11 11:54
 * @Description:自定義retrieveSession方法,把session放到request里面,這樣就不用每次去redis里面去取了,這樣大大提高了redis的性能。
 */
@Component
public class ShiroSessionManager extends DefaultWebSessionManager {

    protected static final Logger logger = LogManager.getLogger(ShiroSessionManager.class);

    public ShiroSessionManager() {
        super();
    }

    //重寫這個(gè)方法為了減少多次從redis中讀取session(自定義redisSessionDao中的doReadSession方法)
    @Override
    protected Session retrieveSession(SessionKey sessionKey) {
        // 獲取sessionId
        Serializable sessionId = getSessionId(sessionKey);

        //logger.info("嘗試獲取Session:" + sessionId);
        // 在 Web 下使用 shiro 時(shí)這個(gè) sessionKey 是 WebSessionKey 類型的
        // 若是在web下使用,則獲取request
        ServletRequest request = null;
        if (sessionKey instanceof WebSessionKey) {
            request = ((WebSessionKey) sessionKey).getServletRequest();
        }

        // 嘗試從request中獲取session
        if (request != null && sessionId != null) {

            Session session = (Session) request.getAttribute(sessionId.toString());
            if (session != null) {
                return session;
            }
        }

        // 若從request中獲取session失敗,則從redis中獲取session,并把獲取到的session存儲(chǔ)到request中方便下次獲取
        Session session = super.retrieveSession(sessionKey);
        if (request != null && sessionId != null) {
            //logger.info("存儲(chǔ)新session到request中:" + sessionId);
            request.setAttribute(sessionId.toString(), session);
        }
        return session;
    }

}

  • ShiroRedisConfig類
package com.syiti.aic.web.admin.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

/**
 * @Auther: LinJiaJia
 * @Date: 2019/3/7 16:44
 * @Description:
 */
@Component
public class ShiroRedisConfig {

    @Value("${spring.redis.host}")
    public String redisHost;

    @Value("${spring.redis.password}")
    public String redisPwd;

    @Value("${spring.redis.database}")
    public int redisDatabase;

}

3.redis配置
  • redis的properties配置
#Redis數(shù)據(jù)庫索引(默認(rèn)為0)
spring.redis.database=1
#Redis服務(wù)器地址
spring.redis.host=127.0.0.1
#Redis服務(wù)器連接端口
spring.redis.port=6379
#Redis服務(wù)器連接密碼(默認(rèn)為空)
spring.redis.password=123456
#連接池最大連接數(shù)(使用負(fù)值表示沒有限制)
spring.redis.pool.max-active=8
#連接池最大阻塞等待時(shí)間(使用負(fù)值表示沒有限制)
spring.redis.pool.max-wait=-1
#連接池中的最大空閑連接
spring.redis.pool.max-idle=8
#連接池中的最小空閑連接
spring.redis.pool.min-idle=0
#連接超時(shí)時(shí)間(毫秒)
spring.redis.timeout=1000

三、注意事項(xiàng)

  • 配置完成后,訪問項(xiàng)目,redis 中應(yīng)該生成如下的緩存數(shù)據(jù),沒一個(gè)鍵值對(duì)代表一個(gè)登錄的用戶


    image.png
  • 如果發(fā)生配置都是對(duì)的,redis中也有寫入值,但是就是無法共享redis。這時(shí)就要考慮,服務(wù)器之間是不是時(shí)間不對(duì),設(shè)置一下服務(wù)器時(shí)間,要保證集群里面的機(jī)器時(shí)間都是一致的。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容