SpringSecurity解决路径中含有%2F的问题

近期有个需求要求Controller可以处理URL中有%2F的请求,比如:

less 复制代码
@RestController
@RequestMapping("/hello")
public class HelloController {
​
    @GetMapping("/name/{path}")
    @ResponseBody
    public String test(@PathVariable String path) {
        return path;
    }
}

请求URL可以是:http://localhost:8080/hello/name/test%2F123

原始的Spring也是不支持路径中有%2F的情况的,直接请求页面会直接报错,但是这种情况已经能搜到很多处理方式了,我亲测最简单且有效的方法就是在Spring启动类中,增加如下语句允许SLASH出现。

arduino 复制代码
public static void main( String[] args ) {
    System.setProperty("org.apache.tomcat.util.buf.UDecoder.ALLOW_ENCODED_SLASH", "true");
    SpringApplication.run(App.class, args);
}

解决Spring的问题之后,如果路径中还是有%2F,访问URL会出现如下的错误:

vbnet 复制代码
Whitelabel Error Page
This application has no explicit mapping for /error, so you are seeing this as a fallback.
​
Sun Mar 17 01:19:43 CST 2024
There was an unexpected error (type=Internal Server Error, status=500).
The requestURI cannot contain encoded slash. Got /xxx/xxx/test%2F123

这个错误就是Spring Security抛出的,如果观察控制台,也可以看到响应的堆栈打印:

bash 复制代码
org.springframework.security.web.firewall.RequestRejectedException: The requestURI cannot contain encoded slash. Got /security/name/test%2F123
    at org.springframework.security.web.firewall.DefaultHttpFirewall.getFirewalledRequest(DefaultHttpFirewall.java:62) ~[spring-security-web-4.2.9.R

因此正式进入Spring Security的处理环节,这里我给出了三种方案,分别应对安全程度从低到高的场景,当然DIY程度也是从小到大,如果是自己的项目,只是单纯的想消除这个讨厌的requestURI cannot contain encoded slash,建议使用方式一就行。

方式一:全局处理%2F存在

这个方案其实有博主已经提到了:www.jb51.net/program/290...

但是我按这个操作之后发现还是不生效,后来还是在Stack Overflow上才发现这里只是新建了Bean,没有注入,那当然毛用没有了。。

在继承了WebMvcConfigurerWebMvcConfig写入如下代码,这里如果不配置的话,后面即使允许slash也会404,应该是Spring把%2F转换为/所以不匹配Controller,方式三就不用这步了。

java 复制代码
import org.springframework.context.annotation.Configuration;
import org.springframework.format.FormatterRegistry;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.validation.MessageCodesResolver;
import org.springframework.validation.Validator;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.config.annotation.*;
import org.springframework.web.util.UrlPathHelper;
​
import java.util.List;
​
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    @Override
    public void configurePathMatch(PathMatchConfigurer configurer) {
        UrlPathHelper urlPathHelper = new UrlPathHelper();
        urlPathHelper.setUrlDecode(false);
        configurer.setUrlPathHelper(urlPathHelper);
    }
    // ... 省略其他未修改的方法
}

在继承了WebSecurityConfigurerAdapterWebSecurityConfig(没有就新建一个),写入如下代码:

scala 复制代码
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.firewall.DefaultHttpFirewall;
import org.springframework.security.web.firewall.HttpFirewall;
​
@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Bean
    public HttpFirewall allowUrlEncodedSlashHttpFirewall() {
        DefaultHttpFirewall defaultHttpFirewall = new DefaultHttpFirewall();
        defaultHttpFirewall.setAllowUrlEncodedSlash(true);
        return defaultHttpFirewall;
    }
​
    public void configure(WebSecurity web) throws Exception {
        // 这里一定要注册,不然不会生效的
        web.httpFirewall(allowUrlEncodedSlashHttpFirewall());
    }
}

这样操作完,所有的Controller就都可以处理Path中有%2F的请求了。

方式二:白名单处理%2F,其余请求走DefaultHttpFirewall

全局都允许路径有%2F是很不安全的,因此在生产环境中,这种路径中含有%2F的URL一定是可枚举,可使用白名单处理的,只有命中白名单的URL才允许%2F出现,其他的URL则仍然使用DefaultHttpFirewall去判断URL是否能够访问。

首先在继承了WebMvcConfigurerWebMvcConfig写入如下代码,这里如果不配置的话,后面即使允许slash也会404,应该是Spring把%2F转换为/所以不匹配Controller,方式三就不用这步了。

java 复制代码
import org.springframework.context.annotation.Configuration;
import org.springframework.format.FormatterRegistry;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.validation.MessageCodesResolver;
import org.springframework.validation.Validator;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.config.annotation.*;
import org.springframework.web.util.UrlPathHelper;
​
import java.util.List;
​
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    @Override
    public void configurePathMatch(PathMatchConfigurer configurer) {
        UrlPathHelper urlPathHelper = new UrlPathHelper();
        urlPathHelper.setUrlDecode(false);
        configurer.setUrlPathHelper(urlPathHelper);
    }
    // ... 省略其他未修改的方法
}

自己继承DefaultHttpFirewall实现一个防火墙重写getFirewalledRequest方法,大概翻一下代码就可以看到,原本的getFirewallRequest是根据allowUrlEncodedSlash变量判断URL中是否允许%2F的,而这个变量可以通过setAllowUrlEncodedSlash进行设置,因此只需要对在白名单的URL临时允许%2F

scala 复制代码
import org.springframework.security.web.firewall.DefaultHttpFirewall;
import org.springframework.security.web.firewall.FirewalledRequest;
import org.springframework.security.web.firewall.RequestRejectedException;
​
import javax.servlet.http.HttpServletRequest;
​
public class CustomHttpFirewall extends DefaultHttpFirewall {
​
    public FirewalledRequest getFirewalledRequest(HttpServletRequest request) throws RequestRejectedException {
        // 如果在允许urlEncodeSlash的白名单内
        if (isInSlashWhiteList(request.getRequestURI())) {
            // 临时允许slash
            setAllowUrlEncodedSlash(true);
        }
        FirewalledRequest res = super.getFirewalledRequest(request);
        // 关闭允许,这里如果不关闭,相当于全局开启了允许slash
        setAllowUrlEncodedSlash(false);
        return res;
    }
​
    /**
     * 判断URL是否在Slash允许的白名单内
     * @param url
     * @return
     */
    private boolean isInSlashWhiteList(String url) {
        return url.contains("hello");
    }
}

但是需要注意的是,在Spring中,Bean是单例,因此在某个请求中设置allowUrlEncodedSlash,可能会影响另一个请求,在并发度不高的服务中,这种现象应该不会出现,但是如果并发度很高,就要考虑这种相互影响的因素,这时也可以考虑使用方式三的思路去解决。

方式三:白名单处理%2F,其余请求走StrictHttpFirewall

如果项目中强制要求使用StrictHttpFirewallStrictHttpFirewall只允许URL中有ASCII字符出现,因此方式二不再适用),或者是考虑到方式二中并发度的问题,就可以考虑用本方式来解决问题,这里的思路整体上是捕捉白名单中的请求,将%2F替换为一个不会出现在URL中的字符串,比如@temp@,然后在Controller再把这个字符串替换回去。

首先需要实现一个FirewalledRequest的包装类,该包装类能够改写原来的URI,以实现修改HttpServletRequest的修改的requestURI的目的:

typescript 复制代码
import org.springframework.security.web.firewall.FirewalledRequest;
​
import javax.servlet.http.HttpServletRequest;
​
public class CustomHttpServletRequestWrapper extends FirewalledRequest {
    private String newUri;
    private String originUri;
    public CustomHttpServletRequestWrapper(HttpServletRequest request, String newUri) {
        super(request);
        originUri = request.getRequestURI();
        this.newUri = newUri;
    }
​
    @Override
    public void reset() {
​
    }
​
    @Override
    public StringBuffer getRequestURL() {
        return new StringBuffer(super.getRequestURL().toString().replace(originUri, newUri));
    }
​
    @Override
    public String getRequestURI() {
        // 返回新的URI
        return newUri;
    }
​
    @Override
    public String getServletPath() {
        // 替换原本的URI为新的URI
        // 这里把%2F换为/是因为getServletPath取出的有可能已经是把%2F转换为/的URI了
        return super.getServletPath().replace(originUri, newUri).replace(originUri.replace("%2F", "/"), newUri);
    }
​
    @Override
    public String getPathInfo() {
        // 返回新的URI
        String pathInfo = super.getPathInfo();
        return (pathInfo != null) ? pathInfo.replace(originUri, newUri): null;
    }
}

然后继承StrictHttpFirewall重写getFirewalledRequest方法,在这里实现对%2F的替换。

scala 复制代码
import org.springframework.security.web.firewall.FirewalledRequest;
import org.springframework.security.web.firewall.RequestRejectedException;
import org.springframework.security.web.firewall.StrictHttpFirewall;
​
import javax.servlet.http.HttpServletRequest;
​
public class CustomStrictHttpFirewall extends StrictHttpFirewall {
​
    public static final String replaceSlash = "@temp@";
​
    @Override
    public FirewalledRequest getFirewalledRequest(HttpServletRequest request) throws RequestRejectedException {
        if (isInSlashWhiteList(request.getRequestURI())) {
            // 在白名单中,替换%2F
            return new CustomHttpServletRequestWrapper(request, request.getRequestURI().replace("%2F", replaceSlash));
        }
        // 否则使用严格模式的生成方法
        return super.getFirewalledRequest(request);
    }
​
​
    /**
     * 判断URL是否在Slash允许的白名单内
     * @param url
     * @return
     */
    private boolean isInSlashWhiteList(String url) {
        return url.contains("hello");
    }
}

最后,注册这个自定义的StrictHttpFirewall

scala 复制代码
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.firewall.DefaultHttpFirewall;
import org.springframework.security.web.firewall.HttpFirewall;
import org.springframework.security.web.firewall.StrictHttpFirewall;
​
@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Bean
    public HttpFirewall allowUrlEncodedSlashHttpFirewall() {
        return new CustomStrictHttpFirewall();
    }
​
    public void configure(WebSecurity web) throws Exception {
        web.httpFirewall(allowUrlEncodedSlashHttpFirewall());
    }
}

在实际的Controller中,把这个特殊字符串再转换回去就行了:

less 复制代码
@RestController
@RequestMapping("/hello")
public class HelloController {
​
    @GetMapping("/name/{path}")
    @ResponseBody
    public String test(@PathVariable String path) {
        return path.replace(CustomStrictHttpFirewall.replaceSlash, "/");
    }
}
相关推荐
啾啾Fun15 分钟前
Java反射操作百倍性能优化
java·性能优化·反射·缓存思想
20岁30年经验的码农22 分钟前
若依微服务Openfeign接口调用超时问题
java·微服务·架构
曲莫终31 分钟前
SpEl表达式之强大的集合选择(Collection Selection)和集合投影(Collection Projection)
java·spring boot·spring
ajassi20001 小时前
开源 java android app 开发(十二)封库.aar
android·java·linux·开源
q567315231 小时前
Java使用Selenium反爬虫优化方案
java·开发语言·分布式·爬虫·selenium
kaikaile19951 小时前
解密Spring Boot:深入理解条件装配与条件注解
java·spring boot·spring
广州山泉婚姻1 小时前
解锁高效开发:Spring Boot 3和MyBatis-Flex在智慧零工平台后端的应用实战
人工智能·spring boot·spring
守护者1701 小时前
JAVA学习-练习试用Java实现“一个词频统计工具 :读取文本文件,统计并输出每个单词的频率”
java·学习
bing_1581 小时前
Spring Boot 中ConditionalOnClass、ConditionalOnMissingBean 注解详解
java·spring boot·后端
ergdfhgerty1 小时前
斐讯N1部署Armbian与CasaOS实现远程存储管理
java·docker