Skip to content

Commit 3b9638b

Browse files
committed
enh: added case-insensitive path filtering
1 parent f27f46e commit 3b9638b

13 files changed

Lines changed: 180 additions & 24 deletions

File tree

core/src/main/java/org/apache/shiro/util/AntPathMatcher.java

Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ public class AntPathMatcher implements PatternMatcher {
6969
public static final String DEFAULT_PATH_SEPARATOR = "/";
7070

7171
private String pathSeparator = DEFAULT_PATH_SEPARATOR;
72+
private boolean caseInsensitive;
7273

7374

7475
/**
@@ -79,6 +80,16 @@ public void setPathSeparator(String pathSeparator) {
7980
this.pathSeparator = (pathSeparator != null ? pathSeparator : DEFAULT_PATH_SEPARATOR);
8081
}
8182

83+
@Override
84+
public boolean isCaseInsensitive() {
85+
return caseInsensitive;
86+
}
87+
88+
@Override
89+
public void setCaseInsensitive(boolean caseInsensitive) {
90+
this.caseInsensitive = caseInsensitive;
91+
}
92+
8293
/**
8394
* Checks if {@code path} is a pattern (i.e. contains a '*', or '?').
8495
* For example the {@code /foo/**} would return {@code true}, while {@code /bar/} would return {@code false}.
@@ -281,11 +292,9 @@ private boolean matchStrings(String pattern, String str) {
281292
}
282293
for (int i = 0; i <= patIdxEnd; i++) {
283294
ch = patArr[i];
284-
if (ch != '?') {
285-
if (ch != strArr[i]) {
286-
// Character mismatch
287-
return false;
288-
}
295+
if (ch != '?' && checkCase(ch) != checkCase(strArr[i])) {
296+
// Character mismatch
297+
return false;
289298
}
290299
}
291300
// String matches against pattern
@@ -300,12 +309,11 @@ private boolean matchStrings(String pattern, String str) {
300309

301310
// Process characters before first star
302311
while ((ch = patArr[patIdxStart]) != '*' && strIdxStart <= strIdxEnd) {
303-
if (ch != '?') {
304-
if (ch != strArr[strIdxStart]) {
305-
// Character mismatch
306-
return false;
307-
}
312+
if (ch != '?' && checkCase(ch) != checkCase(strArr[strIdxStart])) {
313+
// Character mismatch
314+
return false;
308315
}
316+
309317
patIdxStart++;
310318
strIdxStart++;
311319
}
@@ -322,12 +330,11 @@ private boolean matchStrings(String pattern, String str) {
322330

323331
// Process characters after last star
324332
while ((ch = patArr[patIdxEnd]) != '*' && strIdxStart <= strIdxEnd) {
325-
if (ch != '?') {
326-
if (ch != strArr[strIdxEnd]) {
327-
// Character mismatch
328-
return false;
329-
}
333+
if (ch != '?' && checkCase(ch) != checkCase(strArr[strIdxEnd])) {
334+
// Character mismatch
335+
return false;
330336
}
337+
331338
patIdxEnd--;
332339
strIdxEnd--;
333340
}
@@ -366,10 +373,8 @@ private boolean matchStrings(String pattern, String str) {
366373
for (int i = 0; i <= strLength - patLength; i++) {
367374
for (int j = 0; j < patLength; j++) {
368375
ch = patArr[patIdxStart + j + 1];
369-
if (ch != '?') {
370-
if (ch != strArr[strIdxStart + i + j]) {
371-
continue strLoop;
372-
}
376+
if (ch != '?' && checkCase(ch) != checkCase(strArr[strIdxStart + i + j])) {
377+
continue strLoop;
373378
}
374379
}
375380

@@ -434,5 +439,7 @@ public String extractPathWithinPattern(String pattern, String path) {
434439
return builder.toString();
435440
}
436441

437-
442+
private char checkCase(char ch) {
443+
return isCaseInsensitive() ? Character.toLowerCase(ch) : ch;
444+
}
438445
}

core/src/main/java/org/apache/shiro/util/PatternMatcher.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,19 @@ public interface PatternMatcher {
3939
* <code>false</code> otherwise.
4040
*/
4141
boolean matches(String pattern, String source);
42+
43+
/**
44+
* Returns {@code true} if pattern matching should be case-insensitive.
45+
*/
46+
default boolean isCaseInsensitive() {
47+
return false;
48+
}
49+
50+
/**
51+
* Sets whether pattern matching should be case-insensitive.
52+
*
53+
* @param caseInsensitive {@code true} if pattern matching should be case-insensitive.
54+
*/
55+
default void setCaseInsensitive(boolean caseInsensitive) {
56+
}
4257
}

core/src/main/java/org/apache/shiro/util/RegExPatternMatcher.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ public boolean matches(String pattern, String source) {
6262
*
6363
* @return true if regex match should be case-insensitive.
6464
*/
65+
@Override
6566
public boolean isCaseInsensitive() {
6667
return caseInsensitive;
6768
}
@@ -71,6 +72,7 @@ public boolean isCaseInsensitive() {
7172
*
7273
* @param caseInsensitive true if patterns should match case-insensitive.
7374
*/
75+
@Override
7476
public void setCaseInsensitive(boolean caseInsensitive) {
7577
this.caseInsensitive = caseInsensitive;
7678
}

core/src/test/java/org/apache/shiro/util/AntPathMatcherTests.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,4 +332,12 @@ void matches() {
332332
void isPatternWithNullPath() {
333333
assertFalse(pathMatcher.isPattern(null));
334334
}
335+
336+
@Test
337+
void caseInsensitiveMatch() {
338+
pathMatcher.setCaseInsensitive(true);
339+
assertTrue(pathMatcher.match("/Test/Path", "/test/path"));
340+
assertTrue(pathMatcher.match("/TEST/PATH/*", "/test/path/extra"));
341+
assertFalse(pathMatcher.match("/TEST/PATH", "/different/path"));
342+
}
335343
}

support/spring/src/main/java/org/apache/shiro/spring/web/ShiroFilterFactoryBean.java

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ public class ShiroFilterFactoryBean implements FactoryBean, BeanPostProcessor {
135135
private String loginUrl;
136136
private String successUrl;
137137
private String unauthorizedUrl;
138+
private boolean caseInsensitive;
138139

139140
private AbstractShiroFilter instance;
140141

@@ -283,6 +284,21 @@ public void setUnauthorizedUrl(String unauthorizedUrl) {
283284
this.unauthorizedUrl = unauthorizedUrl;
284285
}
285286

287+
/**
288+
* @return true if filter chain matching should be case insensitive.
289+
*/
290+
public boolean isCaseInsensitive() {
291+
return caseInsensitive;
292+
}
293+
294+
/**
295+
* Sets whether filter chain matching should be case insensitive.
296+
* @param caseInsensitive true if filter chain matching should be case insensitive.
297+
*/
298+
public void setCaseInsensitive(boolean caseInsensitive) {
299+
this.caseInsensitive = caseInsensitive;
300+
}
301+
286302
/**
287303
* Returns the filterName-to-Filter map of filters available for reference when defining filter chain definitions.
288304
* All filter chain definitions will reference filters by the names in this map (i.e. the keys).
@@ -409,6 +425,7 @@ public boolean isSingleton() {
409425
protected FilterChainManager createFilterChainManager() {
410426

411427
DefaultFilterChainManager manager = new DefaultFilterChainManager();
428+
manager.setCaseInsensitive(caseInsensitive);
412429
Map<String, Filter> defaultFilters = manager.getFilters();
413430
//apply global settings if necessary:
414431
for (Filter filter : defaultFilters.values()) {
@@ -489,7 +506,7 @@ protected AbstractShiroFilter createInstance() throws Exception {
489506
//Expose the constructed FilterChainManager by first wrapping it in a
490507
// FilterChainResolver implementation. The AbstractShiroFilter implementations
491508
// do not know about FilterChainManagers - only resolvers:
492-
PathMatchingFilterChainResolver chainResolver = new PathMatchingFilterChainResolver();
509+
PathMatchingFilterChainResolver chainResolver = new PathMatchingFilterChainResolver().caseInsensitive(caseInsensitive);
493510
chainResolver.setFilterChainManager(manager);
494511

495512
//Now create a concrete ShiroFilter instance and apply the acquired SecurityManager and built

support/spring/src/main/java/org/apache/shiro/spring/web/config/AbstractShiroWebFilterConfiguration.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ public class AbstractShiroWebFilterConfiguration {
5656
@Value("#{ @environment['shiro.unauthorizedUrl'] ?: null }")
5757
protected String unauthorizedUrl;
5858

59+
@Value("#{ @environment['shiro.caseInsensitive'] ?: false }")
60+
protected boolean caseInsensitive;
61+
5962
protected List<String> globalFilters() {
6063
return Collections.singletonList(DefaultFilter.invalidRequest.name());
6164
}
@@ -72,6 +75,7 @@ protected ShiroFilterFactoryBean shiroFilterFactoryBean() {
7275
filterFactoryBean.setLoginUrl(loginUrl);
7376
filterFactoryBean.setSuccessUrl(successUrl);
7477
filterFactoryBean.setUnauthorizedUrl(unauthorizedUrl);
78+
filterFactoryBean.setCaseInsensitive(caseInsensitive);
7579

7680
filterFactoryBean.setSecurityManager(securityManager);
7781
filterFactoryBean.setShiroFilterConfiguration(shiroFilterConfiguration());

support/spring/src/test/groovy/org/apache/shiro/spring/config/ShiroWebFilterConfigurationTest.groovy

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,12 @@ import org.apache.shiro.spring.web.config.DefaultShiroFilterChainDefinition
2525
import org.apache.shiro.spring.web.config.ShiroFilterChainDefinition
2626
import org.apache.shiro.spring.web.config.ShiroWebFilterConfiguration
2727
import org.apache.shiro.web.filter.InvalidRequestFilter
28+
import org.apache.shiro.web.filter.PathConfigProcessor
2829
import org.apache.shiro.web.filter.mgt.FilterChainManager
30+
import org.apache.shiro.web.filter.mgt.PathMatchingFilterChainResolver
31+
import org.apache.shiro.web.mgt.DefaultWebSecurityManager
32+
import org.apache.shiro.web.servlet.AbstractShiroFilter
33+
import org.junit.jupiter.api.AfterEach
2934
import org.junit.jupiter.api.Test
3035
import org.junit.jupiter.api.extension.ExtendWith
3136
import org.springframework.beans.factory.annotation.Autowired
@@ -47,6 +52,7 @@ import static org.hamcrest.Matchers.contains
4752
import static org.hamcrest.Matchers.instanceOf
4853
import static org.hamcrest.Matchers.notNullValue
4954
import static org.hamcrest.MatcherAssert.assertThat
55+
import static org.hamcrest.Matchers.is;
5056

5157
/**
5258
* Test ShiroWebFilterConfiguration creates a ShiroFilterFactoryBean that contains Servlet filters that are available for injection.
@@ -62,6 +68,13 @@ class ShiroWebFilterConfigurationTest extends AbstractJUnit4SpringContextTests {
6268
@Autowired
6369
private ShiroFilterFactoryBean shiroFilterFactoryBean
6470

71+
private static final ThreadLocal<Boolean> caseInsensitiveCalled = ThreadLocal.withInitial { false }
72+
73+
@AfterEach
74+
void tearDown() {
75+
caseInsensitiveCalled.remove()
76+
}
77+
6578
@Test
6679
void testShiroFilterFactoryBeanContainsSpringFilters() {
6780

@@ -73,6 +86,31 @@ class ShiroWebFilterConfigurationTest extends AbstractJUnit4SpringContextTests {
7386
assertThat filterChainManager.getChain("/test-me"), contains(instanceOf(InvalidRequestFilter), instanceOf(ExpectedTestFilter))
7487
}
7588

89+
@Test
90+
void caseInsensitiveChainManager() {
91+
shiroFilterFactoryBean.setCaseInsensitive true
92+
FilterChainManager filterChainManager = shiroFilterFactoryBean.createFilterChainManager()
93+
assertThat filterChainManager.caseInsensitive, is(true)
94+
}
95+
96+
@Test
97+
void caseInsensitiveResolverAndPathMatcher() {
98+
shiroFilterFactoryBean.setCaseInsensitive true
99+
shiroFilterFactoryBean.setSecurityManager new DefaultWebSecurityManager()
100+
AbstractShiroFilter filter = shiroFilterFactoryBean.getObject()
101+
PathMatchingFilterChainResolver resolver = filter.filterChainResolver;
102+
assertThat resolver.caseInsensitive, is(true)
103+
assertThat resolver.pathMatcher.caseInsensitive, is(true)
104+
assertThat caseInsensitiveCalled.get(), is(true)
105+
}
106+
107+
@Test
108+
void caseInsensitivePathConfigProcessor() {
109+
shiroFilterFactoryBean.setCaseInsensitive true
110+
shiroFilterFactoryBean.createFilterChainManager()
111+
assertThat caseInsensitiveCalled.get(), is(true)
112+
}
113+
76114
@Configuration
77115
static class FilterConfiguration {
78116

@@ -91,7 +129,7 @@ class ShiroWebFilterConfigurationTest extends AbstractJUnit4SpringContextTests {
91129
}
92130
}
93131

94-
static class ExpectedTestFilter implements Filter {
132+
static class ExpectedTestFilter implements Filter, PathConfigProcessor {
95133
@Override
96134
void init(FilterConfig filterConfig) throws ServletException {}
97135

@@ -100,5 +138,15 @@ class ShiroWebFilterConfigurationTest extends AbstractJUnit4SpringContextTests {
100138

101139
@Override
102140
void destroy() {}
141+
142+
@Override
143+
Filter processPathConfig(String path, String config) {
144+
return null
145+
}
146+
147+
@Override
148+
void setCaseInsensitive(boolean caseInsensitive) {
149+
caseInsensitiveCalled.set caseInsensitive
150+
}
103151
}
104152
}

web/src/main/java/org/apache/shiro/web/config/IniFilterChainResolverFactory.java

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ public class IniFilterChainResolverFactory extends IniFactorySupport<FilterChain
6161

6262
private List<String> globalFilters = Collections.singletonList(DefaultFilter.invalidRequest.name());
6363

64+
private boolean caseInsensitive;
65+
6466
public IniFilterChainResolverFactory() {
6567
super();
6668
}
@@ -90,6 +92,14 @@ public void setGlobalFilters(List<String> globalFilters) {
9092
this.globalFilters = globalFilters;
9193
}
9294

95+
public boolean isCaseInsensitive() {
96+
return caseInsensitive;
97+
}
98+
99+
public void setCaseInsensitive(boolean caseInsensitive) {
100+
this.caseInsensitive = caseInsensitive;
101+
}
102+
93103
protected FilterChainResolver createInstance(Ini ini) {
94104
FilterChainResolver filterChainResolver = createDefaultInstance();
95105
if (filterChainResolver instanceof PathMatchingFilterChainResolver) {
@@ -103,9 +113,9 @@ protected FilterChainResolver createInstance(Ini ini) {
103113
protected FilterChainResolver createDefaultInstance() {
104114
FilterConfig filterConfig = getFilterConfig();
105115
if (filterConfig != null) {
106-
return new PathMatchingFilterChainResolver(filterConfig);
116+
return new PathMatchingFilterChainResolver(filterConfig).caseInsensitive(caseInsensitive);
107117
} else {
108-
return new PathMatchingFilterChainResolver();
118+
return new PathMatchingFilterChainResolver().caseInsensitive(caseInsensitive);
109119
}
110120
}
111121

web/src/main/java/org/apache/shiro/web/filter/PathConfigProcessor.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,10 @@ public interface PathConfigProcessor {
3636
* @return the {@code Filter} that should execute for the given path/config combination.
3737
*/
3838
Filter processPathConfig(String path, String config);
39+
40+
/**
41+
* Sets whether the path matching performed by this processor is case insensitive.
42+
* @param caseInsensitive true if case insensitive, false otherwise
43+
*/
44+
default void setCaseInsensitive(boolean caseInsensitive) { }
3945
}

web/src/main/java/org/apache/shiro/web/filter/PathMatchingFilter.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,13 @@ public Filter processPathConfig(String path, String config) {
9191
return this;
9292
}
9393

94+
@Override
95+
public void setCaseInsensitive(boolean caseInsensitive) {
96+
if (pathMatcher != null) {
97+
pathMatcher.setCaseInsensitive(caseInsensitive);
98+
}
99+
}
100+
94101
/**
95102
* Returns the context path within the application based on the specified <code>request</code>.
96103
* <p/>

0 commit comments

Comments
 (0)