Spring Boot + Shiro 使用 DefaultWebSessionManager 導致 Druid Monitor 監聽不到 Session 問題解決方案

一、問題配置

項目中使用了 shiro-spring 快速集成 Shiro 到當前 Spring 環境中,配置如下:

  1. pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>  
   // ...
	<properties>
		<!-- shiro -->
		<shiro-spring.version>1.4.0</shiro-spring.version>
		<shiro-ehcache.version>1.5.1</shiro-ehcache.version>
	</properties>

	<dependencies>
		// ...
		<!-- apache-shiro -->
		<dependency>
			<groupId>org.apache.shiro</groupId>
			<artifactId>shiro-spring</artifactId>
			<version>${shiro-spring.version}</version>
		</dependency>

		<!-- apache-shiro-ehcache 支持 -->
		<dependency>
			<groupId>org.apache.shiro</groupId>
			<artifactId>shiro-ehcache</artifactId>
			<version>${shiro-ehcache.version}</version>
		</dependency>
	</dependencies>
    // ...
</project>

2. ShiroConfig.java

ShiroConfig 中的 ShiroFilter 配置方式在此處是必須的,網上部分教程使用的是如下的配置方式:

/**
 * 權限校驗配置
 */
@Configuration
public class ShiroConfig {

    /**
     * 配置 Shiro 攔截器工廠
     *
     * @param securityManager
     * @return
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        // 由於系統中所有的權限攔截已經通過 SpringMVC interceptor 攔截實現了, 這裏將所有的請求交給 anno 處理,
        // 具體攔截由 SpringMVC interceptor 處理, 後續看情況遷移至 shiro 實現
        Map<String, String> filterChainDefinitionMap = new HashMap<>();
        filterChainDefinitionMap.put("/**", "anon");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }
    
    // ...

   /**
     * 採用 DefaultWebSessionManager 替代掉容器內置的 Session 實現。
     */
    @Bean("sessionManager")
    public SessionManager sessionManager() {
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        //配置監聽
        sessionManager.setSessionIdCookie(sessionIdCookie());
        // 設置 sessionDAO
        sessionManager.setSessionDAO(sessionDAO());
        // 配置 session
        sessionManager.setDeleteInvalidSessions(true);
        sessionManager.setSessionValidationSchedulerEnabled(true);
        // 10 分鐘檢查一遍失效 session
        sessionManager.setSessionValidationInterval(10 * 60 * 1000);

        return sessionManager;
    }

}

這種配置本身並沒有問題,但是在集成 Spring Monitor 監聽 Session 的場景會存在問題。通常我們在 Spring-Boot 項目中繼承 Druid 引入如下依賴:

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.1.10</version>
</dependency>

然後 happy 的在 application.yml 中添加如下配置:

spring.datasource.druid:
  web-stat-filter:
    enabled: true
    url-pattern: /*
    exclusions: "/druid/*,/static/*,/webjars/*,/web-admin/*"
    # 開啓 session 監控
    session-stat-enable: true
    session-stat-max-count: 1000
    # 監聽 session 中的 login-user 屬性
    principal-session-name: login-user
    profile-enable: true
  stat-view-servlet:
    enabled: true
    url-pattern: /druid/*
    reset-enable: true
    login-username: admin
    login-password: 123456
    allow:
    deny:
  filter:
    slf4j:
      enabled: true
      statement-create-after-log-enabled: false
      statement-log-enabled: false
      statement-executable-sql-log-enable: true
      statement-log-error-enabled: true
      result-set-log-enabled: false

但是當我們打開 druid monitor 會發現 session 監控並沒有生效。

二、原因分析

 druid-spring-boot-starter 自動配置 com.alibaba.druid.support.http.WebStatFilter 的時候使用的是 FilterRegistrationBean,源代碼如下:

/*
 * Copyright 1999-2018 Alibaba Group Holding Ltd.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.alibaba.druid.spring.boot.autoconfigure.stat;

import com.alibaba.druid.spring.boot.autoconfigure.properties.DruidStatProperties;
import com.alibaba.druid.support.http.WebStatFilter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;

/**
 * @author lihengming [[email protected]]
 */
@ConditionalOnWebApplication
@ConditionalOnProperty(name = "spring.datasource.druid.web-stat-filter.enabled", havingValue = "true", matchIfMissing = true)
public class DruidWebStatFilterConfiguration {
    @Bean
    public FilterRegistrationBean webStatFilterRegistrationBean(DruidStatProperties properties) {
        DruidStatProperties.WebStatFilter config = properties.getWebStatFilter();
        FilterRegistrationBean registrationBean = new FilterRegistrationBean();
        WebStatFilter filter = new WebStatFilter();
        registrationBean.setFilter(filter);
        registrationBean.addUrlPatterns(config.getUrlPattern() != null ? config.getUrlPattern() : "/*");
        registrationBean.addInitParameter("exclusions", config.getExclusions() != null ? config.getExclusions() : "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*");
        if (config.getSessionStatEnable() != null) {
            registrationBean.addInitParameter("sessionStatEnable", config.getSessionStatEnable());
        }
        if (config.getSessionStatMaxCount() != null) {
            registrationBean.addInitParameter("sessionStatMaxCount", config.getSessionStatMaxCount());
        }
        if (config.getPrincipalSessionName() != null) {
            registrationBean.addInitParameter("principalSessionName", config.getPrincipalSessionName());
        }
        if (config.getPrincipalCookieName() != null) {
            registrationBean.addInitParameter("principalCookieName", config.getPrincipalCookieName());
        }
        if (config.getProfileEnable() != null) {
            registrationBean.addInitParameter("profileEnable", config.getProfileEnable());
        }
        return registrationBean;
    }
}

DruidWebStatFilterConfiguration 在配置 WebStatFilter 的時候並沒有指定順序(其實 Servlet 標準中也沒有通過指定 order 指定 Filter 攔截器順序的機制,Filter 順序按照其配置的先後,先配置的在前,後配置的在後),所以其默認的順序爲 FilterRegistrationBean.REQUEST_WRAPPER_FILTER_MAX_ORDER(越小優先級越高),而通過 ShiroFilterFactoryBean 配置的 Filter 是不會參加這個排序過程的,永遠都是最後一個,因爲 ShiroFilterFactoryBean 向容器註冊 Filter 的時機在 FilterRegistrationBean 之後。

 Druid 的 WebStatFilter 在獲取 Session 信息的時候,此時的 Session 是原容器的 Session 信息,故不能獲取 Shiro 管理的 Session 信息(Shiro  管理的 Session 信息需要再經過 org.apache.shiro.web.servlet.OncePerRequestFilter#doFilter)之後纔會生效,所以就會出現上述的問題。

三、解決方案

從 Shiro Filter 配置入手,直接通過 ShiroFactoryBean 傳進 Filter, 然後創建 FilterRegistrationBean,配置 order 小於 FilterRegistrationBean.REQUEST_WRAPPER_FILTER_MAX_ORDER 即可。具體配置代碼如下:

/**
 * 權限校驗配置
 */
@Configuration
public class ShiroConfig {

    /**
     * 配置 Shiro 攔截器配置
     *
     * @param securityManager
     * @return
     */
    @SneakyThrows
    @Bean
    public FilterRegistrationBean shiroFilter(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        // 由於系統中所有的權限攔截已經通過 SpringMVC interceptor 攔截實現了, 這裏將所有的請求交給 anno 處理,
        // 具體攔截由 SpringMVC interceptor 處理, 後續看情況遷移至 shiro 實現
        Map<String, String> filterChainDefinitionMap = new HashMap<>();
        filterChainDefinitionMap.put("/**", "anon");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        Object filter = shiroFilterFactoryBean.getObject();
        FilterRegistrationBean<Filter> register = new FilterRegistrationBean<>();
        register.setFilter((Filter) filter);
        register.setOrder(FilterRegistrationBean.REQUEST_WRAPPER_FILTER_MAX_ORDER - 10);
        register.addUrlPatterns("/*");
        return register;
    }

    // ...
    
    /**
     * 採用 DefaultWebSessionManager 替代掉容器內置的 Session 實現。
     */
    @Bean("sessionManager")
    public SessionManager sessionManager() {
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        //配置監聽
        sessionManager.setSessionIdCookie(sessionIdCookie());
        // 設置 sessionDAO
        sessionManager.setSessionDAO(sessionDAO());
        // 配置 session
        sessionManager.setDeleteInvalidSessions(true);
        sessionManager.setSessionValidationSchedulerEnabled(true);
        // 10 分鐘檢查一遍失效 session
        sessionManager.setSessionValidationInterval(10 * 60 * 1000);

        return sessionManager;
    }
}

四、驗證

項目啓動,打開 Druid Monitor,打開 Session 監控界面(注意:系統中需要有登錄用戶),結果如下:

具體代碼可以參看筆者個開源項目 QW-ADMIN 中 com.qiwen.base.config.shiro.ShiroConfig 相關配置。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章