This is an automated email from the ASF dual-hosted git repository.
lprimak pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/shiro.git
The following commit(s) were added to refs/heads/main by this push:
new 3b9638b95 enh: added case-insensitive path filtering
3b9638b95 is described below
commit 3b9638b957495004599aeaf24ba8949e309f26e8
Author: lprimak <[email protected]>
AuthorDate: Sat Jan 31 14:15:08 2026 -0600
enh: added case-insensitive path filtering
---
.../java/org/apache/shiro/util/AntPathMatcher.java | 47 +++++++++++---------
.../java/org/apache/shiro/util/PatternMatcher.java | 15 +++++++
.../org/apache/shiro/util/RegExPatternMatcher.java | 2 +
.../org/apache/shiro/util/AntPathMatcherTests.java | 8 ++++
.../shiro/spring/web/ShiroFilterFactoryBean.java | 19 +++++++-
.../AbstractShiroWebFilterConfiguration.java | 4 ++
.../config/ShiroWebFilterConfigurationTest.groovy | 50 +++++++++++++++++++++-
.../web/config/IniFilterChainResolverFactory.java | 14 +++++-
.../shiro/web/filter/PathConfigProcessor.java | 6 +++
.../shiro/web/filter/PathMatchingFilter.java | 7 +++
.../web/filter/mgt/DefaultFilterChainManager.java | 8 ++++
.../shiro/web/filter/mgt/FilterChainManager.java | 6 +++
.../mgt/PathMatchingFilterChainResolver.java | 18 ++++++++
13 files changed, 180 insertions(+), 24 deletions(-)
diff --git a/core/src/main/java/org/apache/shiro/util/AntPathMatcher.java
b/core/src/main/java/org/apache/shiro/util/AntPathMatcher.java
index 0c31e306e..15bb00079 100644
--- a/core/src/main/java/org/apache/shiro/util/AntPathMatcher.java
+++ b/core/src/main/java/org/apache/shiro/util/AntPathMatcher.java
@@ -69,6 +69,7 @@ public class AntPathMatcher implements PatternMatcher {
public static final String DEFAULT_PATH_SEPARATOR = "/";
private String pathSeparator = DEFAULT_PATH_SEPARATOR;
+ private boolean caseInsensitive;
/**
@@ -79,6 +80,16 @@ public class AntPathMatcher implements PatternMatcher {
this.pathSeparator = (pathSeparator != null ? pathSeparator :
DEFAULT_PATH_SEPARATOR);
}
+ @Override
+ public boolean isCaseInsensitive() {
+ return caseInsensitive;
+ }
+
+ @Override
+ public void setCaseInsensitive(boolean caseInsensitive) {
+ this.caseInsensitive = caseInsensitive;
+ }
+
/**
* Checks if {@code path} is a pattern (i.e. contains a '*', or '?').
* For example the {@code /foo/**} would return {@code true}, while {@code
/bar/} would return {@code false}.
@@ -281,11 +292,9 @@ public class AntPathMatcher implements PatternMatcher {
}
for (int i = 0; i <= patIdxEnd; i++) {
ch = patArr[i];
- if (ch != '?') {
- if (ch != strArr[i]) {
- // Character mismatch
- return false;
- }
+ if (ch != '?' && checkCase(ch) != checkCase(strArr[i])) {
+ // Character mismatch
+ return false;
}
}
// String matches against pattern
@@ -300,12 +309,11 @@ public class AntPathMatcher implements PatternMatcher {
// Process characters before first star
while ((ch = patArr[patIdxStart]) != '*' && strIdxStart <= strIdxEnd) {
- if (ch != '?') {
- if (ch != strArr[strIdxStart]) {
- // Character mismatch
- return false;
- }
+ if (ch != '?' && checkCase(ch) != checkCase(strArr[strIdxStart])) {
+ // Character mismatch
+ return false;
}
+
patIdxStart++;
strIdxStart++;
}
@@ -322,12 +330,11 @@ public class AntPathMatcher implements PatternMatcher {
// Process characters after last star
while ((ch = patArr[patIdxEnd]) != '*' && strIdxStart <= strIdxEnd) {
- if (ch != '?') {
- if (ch != strArr[strIdxEnd]) {
- // Character mismatch
- return false;
- }
+ if (ch != '?' && checkCase(ch) != checkCase(strArr[strIdxEnd])) {
+ // Character mismatch
+ return false;
}
+
patIdxEnd--;
strIdxEnd--;
}
@@ -366,10 +373,8 @@ public class AntPathMatcher implements PatternMatcher {
for (int i = 0; i <= strLength - patLength; i++) {
for (int j = 0; j < patLength; j++) {
ch = patArr[patIdxStart + j + 1];
- if (ch != '?') {
- if (ch != strArr[strIdxStart + i + j]) {
- continue strLoop;
- }
+ if (ch != '?' && checkCase(ch) !=
checkCase(strArr[strIdxStart + i + j])) {
+ continue strLoop;
}
}
@@ -434,5 +439,7 @@ public class AntPathMatcher implements PatternMatcher {
return builder.toString();
}
-
+ private char checkCase(char ch) {
+ return isCaseInsensitive() ? Character.toLowerCase(ch) : ch;
+ }
}
diff --git a/core/src/main/java/org/apache/shiro/util/PatternMatcher.java
b/core/src/main/java/org/apache/shiro/util/PatternMatcher.java
index ac2d7a764..d72ffdc23 100644
--- a/core/src/main/java/org/apache/shiro/util/PatternMatcher.java
+++ b/core/src/main/java/org/apache/shiro/util/PatternMatcher.java
@@ -39,4 +39,19 @@ public interface PatternMatcher {
* <code>false</code> otherwise.
*/
boolean matches(String pattern, String source);
+
+ /**
+ * Returns {@code true} if pattern matching should be case-insensitive.
+ */
+ default boolean isCaseInsensitive() {
+ return false;
+ }
+
+ /**
+ * Sets whether pattern matching should be case-insensitive.
+ *
+ * @param caseInsensitive {@code true} if pattern matching should be
case-insensitive.
+ */
+ default void setCaseInsensitive(boolean caseInsensitive) {
+ }
}
diff --git a/core/src/main/java/org/apache/shiro/util/RegExPatternMatcher.java
b/core/src/main/java/org/apache/shiro/util/RegExPatternMatcher.java
index b03d96b3d..3109d57d9 100644
--- a/core/src/main/java/org/apache/shiro/util/RegExPatternMatcher.java
+++ b/core/src/main/java/org/apache/shiro/util/RegExPatternMatcher.java
@@ -62,6 +62,7 @@ public class RegExPatternMatcher implements PatternMatcher {
*
* @return true if regex match should be case-insensitive.
*/
+ @Override
public boolean isCaseInsensitive() {
return caseInsensitive;
}
@@ -71,6 +72,7 @@ public class RegExPatternMatcher implements PatternMatcher {
*
* @param caseInsensitive true if patterns should match case-insensitive.
*/
+ @Override
public void setCaseInsensitive(boolean caseInsensitive) {
this.caseInsensitive = caseInsensitive;
}
diff --git a/core/src/test/java/org/apache/shiro/util/AntPathMatcherTests.java
b/core/src/test/java/org/apache/shiro/util/AntPathMatcherTests.java
index b065a89cb..d682343f8 100644
--- a/core/src/test/java/org/apache/shiro/util/AntPathMatcherTests.java
+++ b/core/src/test/java/org/apache/shiro/util/AntPathMatcherTests.java
@@ -332,4 +332,12 @@ public class AntPathMatcherTests {
void isPatternWithNullPath() {
assertFalse(pathMatcher.isPattern(null));
}
+
+ @Test
+ void caseInsensitiveMatch() {
+ pathMatcher.setCaseInsensitive(true);
+ assertTrue(pathMatcher.match("/Test/Path", "/test/path"));
+ assertTrue(pathMatcher.match("/TEST/PATH/*", "/test/path/extra"));
+ assertFalse(pathMatcher.match("/TEST/PATH", "/different/path"));
+ }
}
diff --git
a/support/spring/src/main/java/org/apache/shiro/spring/web/ShiroFilterFactoryBean.java
b/support/spring/src/main/java/org/apache/shiro/spring/web/ShiroFilterFactoryBean.java
index f02fdd4b7..c4e2ea5ee 100644
---
a/support/spring/src/main/java/org/apache/shiro/spring/web/ShiroFilterFactoryBean.java
+++
b/support/spring/src/main/java/org/apache/shiro/spring/web/ShiroFilterFactoryBean.java
@@ -135,6 +135,7 @@ public class ShiroFilterFactoryBean implements FactoryBean,
BeanPostProcessor {
private String loginUrl;
private String successUrl;
private String unauthorizedUrl;
+ private boolean caseInsensitive;
private AbstractShiroFilter instance;
@@ -283,6 +284,21 @@ public class ShiroFilterFactoryBean implements
FactoryBean, BeanPostProcessor {
this.unauthorizedUrl = unauthorizedUrl;
}
+ /**
+ * @return true if filter chain matching should be case insensitive.
+ */
+ public boolean isCaseInsensitive() {
+ return caseInsensitive;
+ }
+
+ /**
+ * Sets whether filter chain matching should be case insensitive.
+ * @param caseInsensitive true if filter chain matching should be case
insensitive.
+ */
+ public void setCaseInsensitive(boolean caseInsensitive) {
+ this.caseInsensitive = caseInsensitive;
+ }
+
/**
* Returns the filterName-to-Filter map of filters available for reference
when defining filter chain definitions.
* All filter chain definitions will reference filters by the names in
this map (i.e. the keys).
@@ -409,6 +425,7 @@ public class ShiroFilterFactoryBean implements FactoryBean,
BeanPostProcessor {
protected FilterChainManager createFilterChainManager() {
DefaultFilterChainManager manager = new DefaultFilterChainManager();
+ manager.setCaseInsensitive(caseInsensitive);
Map<String, Filter> defaultFilters = manager.getFilters();
//apply global settings if necessary:
for (Filter filter : defaultFilters.values()) {
@@ -489,7 +506,7 @@ public class ShiroFilterFactoryBean implements FactoryBean,
BeanPostProcessor {
//Expose the constructed FilterChainManager by first wrapping it in a
// FilterChainResolver implementation. The AbstractShiroFilter
implementations
// do not know about FilterChainManagers - only resolvers:
- PathMatchingFilterChainResolver chainResolver = new
PathMatchingFilterChainResolver();
+ PathMatchingFilterChainResolver chainResolver = new
PathMatchingFilterChainResolver().caseInsensitive(caseInsensitive);
chainResolver.setFilterChainManager(manager);
//Now create a concrete ShiroFilter instance and apply the acquired
SecurityManager and built
diff --git
a/support/spring/src/main/java/org/apache/shiro/spring/web/config/AbstractShiroWebFilterConfiguration.java
b/support/spring/src/main/java/org/apache/shiro/spring/web/config/AbstractShiroWebFilterConfiguration.java
index ff9df7a0b..2a62cdc0f 100644
---
a/support/spring/src/main/java/org/apache/shiro/spring/web/config/AbstractShiroWebFilterConfiguration.java
+++
b/support/spring/src/main/java/org/apache/shiro/spring/web/config/AbstractShiroWebFilterConfiguration.java
@@ -56,6 +56,9 @@ public class AbstractShiroWebFilterConfiguration {
@Value("#{ @environment['shiro.unauthorizedUrl'] ?: null }")
protected String unauthorizedUrl;
+ @Value("#{ @environment['shiro.caseInsensitive'] ?: false }")
+ protected boolean caseInsensitive;
+
protected List<String> globalFilters() {
return Collections.singletonList(DefaultFilter.invalidRequest.name());
}
@@ -72,6 +75,7 @@ public class AbstractShiroWebFilterConfiguration {
filterFactoryBean.setLoginUrl(loginUrl);
filterFactoryBean.setSuccessUrl(successUrl);
filterFactoryBean.setUnauthorizedUrl(unauthorizedUrl);
+ filterFactoryBean.setCaseInsensitive(caseInsensitive);
filterFactoryBean.setSecurityManager(securityManager);
filterFactoryBean.setShiroFilterConfiguration(shiroFilterConfiguration());
diff --git
a/support/spring/src/test/groovy/org/apache/shiro/spring/config/ShiroWebFilterConfigurationTest.groovy
b/support/spring/src/test/groovy/org/apache/shiro/spring/config/ShiroWebFilterConfigurationTest.groovy
index 5c6ee9f50..04f7e6377 100644
---
a/support/spring/src/test/groovy/org/apache/shiro/spring/config/ShiroWebFilterConfigurationTest.groovy
+++
b/support/spring/src/test/groovy/org/apache/shiro/spring/config/ShiroWebFilterConfigurationTest.groovy
@@ -25,7 +25,12 @@ import
org.apache.shiro.spring.web.config.DefaultShiroFilterChainDefinition
import org.apache.shiro.spring.web.config.ShiroFilterChainDefinition
import org.apache.shiro.spring.web.config.ShiroWebFilterConfiguration
import org.apache.shiro.web.filter.InvalidRequestFilter
+import org.apache.shiro.web.filter.PathConfigProcessor
import org.apache.shiro.web.filter.mgt.FilterChainManager
+import org.apache.shiro.web.filter.mgt.PathMatchingFilterChainResolver
+import org.apache.shiro.web.mgt.DefaultWebSecurityManager
+import org.apache.shiro.web.servlet.AbstractShiroFilter
+import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.springframework.beans.factory.annotation.Autowired
@@ -47,6 +52,7 @@ import static org.hamcrest.Matchers.contains
import static org.hamcrest.Matchers.instanceOf
import static org.hamcrest.Matchers.notNullValue
import static org.hamcrest.MatcherAssert.assertThat
+import static org.hamcrest.Matchers.is;
/**
* Test ShiroWebFilterConfiguration creates a ShiroFilterFactoryBean that
contains Servlet filters that are available for injection.
@@ -62,6 +68,13 @@ class ShiroWebFilterConfigurationTest extends
AbstractJUnit4SpringContextTests {
@Autowired
private ShiroFilterFactoryBean shiroFilterFactoryBean
+ private static final ThreadLocal<Boolean> caseInsensitiveCalled =
ThreadLocal.withInitial { false }
+
+ @AfterEach
+ void tearDown() {
+ caseInsensitiveCalled.remove()
+ }
+
@Test
void testShiroFilterFactoryBeanContainsSpringFilters() {
@@ -73,6 +86,31 @@ class ShiroWebFilterConfigurationTest extends
AbstractJUnit4SpringContextTests {
assertThat filterChainManager.getChain("/test-me"),
contains(instanceOf(InvalidRequestFilter), instanceOf(ExpectedTestFilter))
}
+ @Test
+ void caseInsensitiveChainManager() {
+ shiroFilterFactoryBean.setCaseInsensitive true
+ FilterChainManager filterChainManager =
shiroFilterFactoryBean.createFilterChainManager()
+ assertThat filterChainManager.caseInsensitive, is(true)
+ }
+
+ @Test
+ void caseInsensitiveResolverAndPathMatcher() {
+ shiroFilterFactoryBean.setCaseInsensitive true
+ shiroFilterFactoryBean.setSecurityManager new
DefaultWebSecurityManager()
+ AbstractShiroFilter filter = shiroFilterFactoryBean.getObject()
+ PathMatchingFilterChainResolver resolver = filter.filterChainResolver;
+ assertThat resolver.caseInsensitive, is(true)
+ assertThat resolver.pathMatcher.caseInsensitive, is(true)
+ assertThat caseInsensitiveCalled.get(), is(true)
+ }
+
+ @Test
+ void caseInsensitivePathConfigProcessor() {
+ shiroFilterFactoryBean.setCaseInsensitive true
+ shiroFilterFactoryBean.createFilterChainManager()
+ assertThat caseInsensitiveCalled.get(), is(true)
+ }
+
@Configuration
static class FilterConfiguration {
@@ -91,7 +129,7 @@ class ShiroWebFilterConfigurationTest extends
AbstractJUnit4SpringContextTests {
}
}
- static class ExpectedTestFilter implements Filter {
+ static class ExpectedTestFilter implements Filter, PathConfigProcessor {
@Override
void init(FilterConfig filterConfig) throws ServletException {}
@@ -100,5 +138,15 @@ class ShiroWebFilterConfigurationTest extends
AbstractJUnit4SpringContextTests {
@Override
void destroy() {}
+
+ @Override
+ Filter processPathConfig(String path, String config) {
+ return null
+ }
+
+ @Override
+ void setCaseInsensitive(boolean caseInsensitive) {
+ caseInsensitiveCalled.set caseInsensitive
+ }
}
}
diff --git
a/web/src/main/java/org/apache/shiro/web/config/IniFilterChainResolverFactory.java
b/web/src/main/java/org/apache/shiro/web/config/IniFilterChainResolverFactory.java
index 07dfbbad6..c57fec54d 100644
---
a/web/src/main/java/org/apache/shiro/web/config/IniFilterChainResolverFactory.java
+++
b/web/src/main/java/org/apache/shiro/web/config/IniFilterChainResolverFactory.java
@@ -61,6 +61,8 @@ public class IniFilterChainResolverFactory extends
IniFactorySupport<FilterChain
private List<String> globalFilters =
Collections.singletonList(DefaultFilter.invalidRequest.name());
+ private boolean caseInsensitive;
+
public IniFilterChainResolverFactory() {
super();
}
@@ -90,6 +92,14 @@ public class IniFilterChainResolverFactory extends
IniFactorySupport<FilterChain
this.globalFilters = globalFilters;
}
+ public boolean isCaseInsensitive() {
+ return caseInsensitive;
+ }
+
+ public void setCaseInsensitive(boolean caseInsensitive) {
+ this.caseInsensitive = caseInsensitive;
+ }
+
protected FilterChainResolver createInstance(Ini ini) {
FilterChainResolver filterChainResolver = createDefaultInstance();
if (filterChainResolver instanceof PathMatchingFilterChainResolver) {
@@ -103,9 +113,9 @@ public class IniFilterChainResolverFactory extends
IniFactorySupport<FilterChain
protected FilterChainResolver createDefaultInstance() {
FilterConfig filterConfig = getFilterConfig();
if (filterConfig != null) {
- return new PathMatchingFilterChainResolver(filterConfig);
+ return new
PathMatchingFilterChainResolver(filterConfig).caseInsensitive(caseInsensitive);
} else {
- return new PathMatchingFilterChainResolver();
+ return new
PathMatchingFilterChainResolver().caseInsensitive(caseInsensitive);
}
}
diff --git
a/web/src/main/java/org/apache/shiro/web/filter/PathConfigProcessor.java
b/web/src/main/java/org/apache/shiro/web/filter/PathConfigProcessor.java
index aa5be8169..7eae3150e 100644
--- a/web/src/main/java/org/apache/shiro/web/filter/PathConfigProcessor.java
+++ b/web/src/main/java/org/apache/shiro/web/filter/PathConfigProcessor.java
@@ -36,4 +36,10 @@ public interface PathConfigProcessor {
* @return the {@code Filter} that should execute for the given
path/config combination.
*/
Filter processPathConfig(String path, String config);
+
+ /**
+ * Sets whether the path matching performed by this processor is case
insensitive.
+ * @param caseInsensitive true if case insensitive, false otherwise
+ */
+ default void setCaseInsensitive(boolean caseInsensitive) { }
}
diff --git
a/web/src/main/java/org/apache/shiro/web/filter/PathMatchingFilter.java
b/web/src/main/java/org/apache/shiro/web/filter/PathMatchingFilter.java
index 4ac41845c..0d252b57b 100644
--- a/web/src/main/java/org/apache/shiro/web/filter/PathMatchingFilter.java
+++ b/web/src/main/java/org/apache/shiro/web/filter/PathMatchingFilter.java
@@ -91,6 +91,13 @@ public abstract class PathMatchingFilter extends
AdviceFilter implements PathCon
return this;
}
+ @Override
+ public void setCaseInsensitive(boolean caseInsensitive) {
+ if (pathMatcher != null) {
+ pathMatcher.setCaseInsensitive(caseInsensitive);
+ }
+ }
+
/**
* Returns the context path within the application based on the specified
<code>request</code>.
* <p/>
diff --git
a/web/src/main/java/org/apache/shiro/web/filter/mgt/DefaultFilterChainManager.java
b/web/src/main/java/org/apache/shiro/web/filter/mgt/DefaultFilterChainManager.java
index 59893db95..00443da6b 100644
---
a/web/src/main/java/org/apache/shiro/web/filter/mgt/DefaultFilterChainManager.java
+++
b/web/src/main/java/org/apache/shiro/web/filter/mgt/DefaultFilterChainManager.java
@@ -66,6 +66,8 @@ public class DefaultFilterChainManager implements
FilterChainManager {
*/
private Map<String, NamedFilterList> filterChains;
+ private boolean caseInsensitive;
+
public DefaultFilterChainManager() {
this.filters = new LinkedHashMap<String, Filter>();
this.filterChains = new LinkedHashMap<String, NamedFilterList>();
@@ -121,6 +123,11 @@ public class DefaultFilterChainManager implements
FilterChainManager {
return this.filters.get(name);
}
+ @Override
+ public void setCaseInsensitive(boolean caseInsensitive) {
+ this.caseInsensitive = caseInsensitive;
+ }
+
public void addFilter(String name, Filter filter) {
addFilter(name, filter, false);
}
@@ -327,6 +334,7 @@ public class DefaultFilterChainManager implements
FilterChainManager {
}
if (filter instanceof PathConfigProcessor) {
((PathConfigProcessor) filter).processPathConfig(chainName,
chainSpecificFilterConfig);
+ ((PathConfigProcessor) filter).setCaseInsensitive(caseInsensitive);
} else {
if (StringUtils.hasText(chainSpecificFilterConfig)) {
//they specified a filter configuration, but the Filter
doesn't implement PathConfigProcessor
diff --git
a/web/src/main/java/org/apache/shiro/web/filter/mgt/FilterChainManager.java
b/web/src/main/java/org/apache/shiro/web/filter/mgt/FilterChainManager.java
index 980235206..fa57fa767 100644
--- a/web/src/main/java/org/apache/shiro/web/filter/mgt/FilterChainManager.java
+++ b/web/src/main/java/org/apache/shiro/web/filter/mgt/FilterChainManager.java
@@ -220,4 +220,10 @@ public interface FilterChainManager {
* @since 1.6
*/
void setGlobalFilters(List<String> globalFilterNames) throws
ConfigurationException;
+
+ /**
+ * Sets whether or not path matching should be case insensitive.
+ * @param caseInsensitive boolean value indicating whether path matching
should be case insensitive.
+ */
+ default void setCaseInsensitive(boolean caseInsensitive) { }
}
diff --git
a/web/src/main/java/org/apache/shiro/web/filter/mgt/PathMatchingFilterChainResolver.java
b/web/src/main/java/org/apache/shiro/web/filter/mgt/PathMatchingFilterChainResolver.java
index 052ccaa50..11c15ebe0 100644
---
a/web/src/main/java/org/apache/shiro/web/filter/mgt/PathMatchingFilterChainResolver.java
+++
b/web/src/main/java/org/apache/shiro/web/filter/mgt/PathMatchingFilterChainResolver.java
@@ -61,6 +61,11 @@ public class PathMatchingFilterChainResolver implements
FilterChainResolver {
this.filterChainManager = new DefaultFilterChainManager(filterConfig);
}
+ public PathMatchingFilterChainResolver caseInsensitive(boolean
caseInsensitive) {
+ setCaseInsensitive(caseInsensitive);
+ return this;
+ }
+
/**
* Returns the {@code PatternMatcher} used when determining if an incoming
request's path
* matches a configured filter chain. Unless overridden, the
@@ -85,6 +90,19 @@ public class PathMatchingFilterChainResolver implements
FilterChainResolver {
this.pathMatcher = pathMatcher;
}
+ public boolean isCaseInsensitive() {
+ return pathMatcher != null && pathMatcher.isCaseInsensitive();
+ }
+
+ public void setCaseInsensitive(boolean caseInsensitive) {
+ if (pathMatcher != null) {
+ pathMatcher.setCaseInsensitive(caseInsensitive);
+ }
+ if (filterChainManager != null) {
+ filterChainManager.setCaseInsensitive(caseInsensitive);
+ }
+ }
+
public FilterChainManager getFilterChainManager() {
return filterChainManager;
}