一、問題配置
項目中使用了 shiro-spring 快速集成 Shiro 到當前 Spring 環境中,配置如下:
- 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 相關配置。