我有一个带有Spring-Boot 2.5.5和Spring-Security 5.5.2的rest API.

On many API endpoints I use @PathVariable for resources identifiers.
But when some special (but valid in my domain context) characters are passed as PathVariables, the request fails.
The three failing special characters are : slash (/), semicolon (;) and percent (%).

让我们举一个非常简单的例子,如下所示:

import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/v1")
public class Controller {

    @GetMapping("echo/{value}")
    public ResponseEntity<String> echo(@PathVariable("value") String value) {
        return ResponseEntity
                .status(HttpStatus.OK)
                .contentType(MediaType.TEXT_XML)
                .body(value);
    }

}

对于值"Hello/There",我发送GET/API/v1/ECHO/Hello%2Fwhere并收到:

<!doctype html><html lang="en"><head><title>HTTP Status 400 – Bad Request</title><style type="text/css">body {font-family:Tahoma,Arial,sans-serif;} h1, h2, h3, b {color:white;background-color:#525D76;} h1 {font-size:22px;} h2 {font-size:16px;} h3 {font-size:14px;} p {font-size:12px;} a {color:black;} .line {height:1px;background-color:#525D76;border:none;}</style></head><body><h1>HTTP Status 400 – Bad Request</h1><hr class="line" /><p><b>Type</b> Status Report</p><p><b>Message</b> Invalid URI: noSlash</p><p><b>Description</b> The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing).</p><hr class="line" /><h3>Apache Tomcat/9.0.53</h3></body></html>

对于值"Hello;There",我发送GET/API/v1/ECHO/Hello%3Bwhere,并收到:

{
  "stackTrace": [
    {
      "classLoaderName": "app",
      "methodName": "handleAccessDeniedException",
      "fileName": "ExceptionTranslationFilter.java",
      "lineNumber": 194,
      "nativeMethod": false,
      "className": "org.springframework.security.web.access.ExceptionTranslationFilter"
    },
    {
      "classLoaderName": "app",
      "methodName": "handleSpringSecurityException",
      "fileName": "ExceptionTranslationFilter.java",
      "lineNumber": 173,
      "nativeMethod": false,
      "className": "org.springframework.security.web.access.ExceptionTranslationFilter"
    },
    {
      "classLoaderName": "app",
      "methodName": "doFilter",
      "fileName": "ExceptionTranslationFilter.java",
      "lineNumber": 142,
      "nativeMethod": false,
      "className": "org.springframework.security.web.access.ExceptionTranslationFilter"
    },
    {
      "classLoaderName": "app",
      "methodName": "doFilter",
      "fileName": "ExceptionTranslationFilter.java",
      "lineNumber": 115,
      "nativeMethod": false,
      "className": "org.springframework.security.web.access.ExceptionTranslationFilter"
    },
    {
      "classLoaderName": "app",
      "methodName": "doFilter",
      "fileName": "FilterChainProxy.java",
      "lineNumber": 336,
      "nativeMethod": false,
      "className": "org.springframework.security.web.FilterChainProxy$VirtualFilterChain"
    },
    {
      "classLoaderName": "app",
      "methodName": "doFilter",
      "fileName": "SessionManagementFilter.java",
      "lineNumber": 126,
      "nativeMethod": false,
      "className": "org.springframework.security.web.session.SessionManagementFilter"
    },
    {
      "classLoaderName": "app",
      "methodName": "doFilter",
      "fileName": "SessionManagementFilter.java",
      "lineNumber": 81,
      "nativeMethod": false,
      "className": "org.springframework.security.web.session.SessionManagementFilter"
    },
    {
      "classLoaderName": "app",
      "methodName": "doFilter",
      "fileName": "FilterChainProxy.java",
      "lineNumber": 336,
      "nativeMethod": false,
      "className": "org.springframework.security.web.FilterChainProxy$VirtualFilterChain"
    },
    {
      "classLoaderName": "app",
      "methodName": "doFilter",
      "fileName": "AnonymousAuthenticationFilter.java",
      "lineNumber": 105,
      "nativeMethod": false,
      "className": "org.springframework.security.web.authentication.AnonymousAuthenticationFilter"
    },
    {
      "classLoaderName": "app",
      "methodName": "doFilter",
      "fileName": "FilterChainProxy.java",
      "lineNumber": 336,
      "nativeMethod": false,
      "className": "org.springframework.security.web.FilterChainProxy$VirtualFilterChain"
    },
    {
      "classLoaderName": "app",
      "methodName": "doFilter",
      "fileName": "SecurityContextHolderAwareRequestFilter.java",
      "lineNumber": 149,
      "nativeMethod": false,
      "className": "org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter"
    },
    {
      "classLoaderName": "app",
      "methodName": "doFilter",
      "fileName": "FilterChainProxy.java",
      "lineNumber": 336,
      "nativeMethod": false,
      "className": "org.springframework.security.web.FilterChainProxy$VirtualFilterChain"
    },
    {
      "classLoaderName": "app",
      "methodName": "doFilter",
      "fileName": "RequestCacheAwareFilter.java",
      "lineNumber": 63,
      "nativeMethod": false,
      "className": "org.springframework.security.web.savedrequest.RequestCacheAwareFilter"
    },
    {
      "classLoaderName": "app",
      "methodName": "doFilter",
      "fileName": "FilterChainProxy.java",
      "lineNumber": 336,
      "nativeMethod": false,
      "className": "org.springframework.security.web.FilterChainProxy$VirtualFilterChain"
    },
    {
      "classLoaderName": "app",
      "methodName": "doFilter",
      "fileName": "OncePerRequestFilter.java",
      "lineNumber": 103,
      "nativeMethod": false,
      "className": "org.springframework.web.filter.OncePerRequestFilter"
    },
    {
      "classLoaderName": "app",
      "methodName": "doFilter",
      "fileName": "FilterChainProxy.java",
      "lineNumber": 336,
      "nativeMethod": false,
      "className": "org.springframework.security.web.FilterChainProxy$VirtualFilterChain"
    },
    {
      "classLoaderName": "app",
      "methodName": "doFilter",
      "fileName": "LogoutFilter.java",
      "lineNumber": 103,
      "nativeMethod": false,
      "className": "org.springframework.security.web.authentication.logout.LogoutFilter"
    },
    {
      "classLoaderName": "app",
      "methodName": "doFilter",
      "fileName": "LogoutFilter.java",
      "lineNumber": 89,
      "nativeMethod": false,
      "className": "org.springframework.security.web.authentication.logout.LogoutFilter"
    },
    {
      "classLoaderName": "app",
      "methodName": "doFilter",
      "fileName": "FilterChainProxy.java",
      "lineNumber": 336,
      "nativeMethod": false,
      "className": "org.springframework.security.web.FilterChainProxy$VirtualFilterChain"
    },
    {
      "classLoaderName": "app",
      "methodName": "doFilter",
      "fileName": "OncePerRequestFilter.java",
      "lineNumber": 103,
      "nativeMethod": false,
      "className": "org.springframework.web.filter.OncePerRequestFilter"
    },
    {
      "classLoaderName": "app",
      "methodName": "doFilter",
      "fileName": "FilterChainProxy.java",
      "lineNumber": 336,
      "nativeMethod": false,
      "className": "org.springframework.security.web.FilterChainProxy$VirtualFilterChain"
    },
    {
      "classLoaderName": "app",
      "methodName": "doFilter",
      "fileName": "SecurityContextPersistenceFilter.java",
      "lineNumber": 110,
      "nativeMethod": false,
      "className": "org.springframework.security.web.context.SecurityContextPersistenceFilter"
    },
    {
      "classLoaderName": "app",
      "methodName": "doFilter",
      "fileName": "SecurityContextPersistenceFilter.java",
      "lineNumber": 80,
      "nativeMethod": false,
      "className": "org.springframework.security.web.context.SecurityContextPersistenceFilter"
    },
    {
      "classLoaderName": "app",
      "methodName": "doFilter",
      "fileName": "FilterChainProxy.java",
      "lineNumber": 336,
      "nativeMethod": false,
      "className": "org.springframework.security.web.FilterChainProxy$VirtualFilterChain"
    },
    {
      "classLoaderName": "app",
      "methodName": "doFilter",
      "fileName": "OncePerRequestFilter.java",
      "lineNumber": 103,
      "nativeMethod": false,
      "className": "org.springframework.web.filter.OncePerRequestFilter"
    },
    {
      "classLoaderName": "app",
      "methodName": "doFilter",
      "fileName": "FilterChainProxy.java",
      "lineNumber": 336,
      "nativeMethod": false,
      "className": "org.springframework.security.web.FilterChainProxy$VirtualFilterChain"
    },
    {
      "classLoaderName": "app",
      "methodName": "doFilter",
      "fileName": "ChannelProcessingFilter.java",
      "lineNumber": 133,
      "nativeMethod": false,
      "className": "org.springframework.security.web.access.channel.ChannelProcessingFilter"
    },
    {
      "classLoaderName": "app",
      "methodName": "doFilter",
      "fileName": "FilterChainProxy.java",
      "lineNumber": 336,
      "nativeMethod": false,
      "className": "org.springframework.security.web.FilterChainProxy$VirtualFilterChain"
    },
    {
      "classLoaderName": "app",
      "methodName": "doFilterInternal",
      "fileName": "FilterChainProxy.java",
      "lineNumber": 211,
      "nativeMethod": false,
      "className": "org.springframework.security.web.FilterChainProxy"
    },
    {
      "classLoaderName": "app",
      "methodName": "doFilter",
      "fileName": "FilterChainProxy.java",
      "lineNumber": 183,
      "nativeMethod": false,
      "className": "org.springframework.security.web.FilterChainProxy"
    },
    {
      "classLoaderName": "app",
      "methodName": "invokeDelegate",
      "fileName": "DelegatingFilterProxy.java",
      "lineNumber": 358,
      "nativeMethod": false,
      "className": "org.springframework.web.filter.DelegatingFilterProxy"
    },
    {
      "classLoaderName": "app",
      "methodName": "doFilter",
      "fileName": "DelegatingFilterProxy.java",
      "lineNumber": 271,
      "nativeMethod": false,
      "className": "org.springframework.web.filter.DelegatingFilterProxy"
    },
    {
      "classLoaderName": "app",
      "methodName": "internalDoFilter",
      "fileName": "ApplicationFilterChain.java",
      "lineNumber": 189,
      "nativeMethod": false,
      "className": "org.apache.catalina.core.ApplicationFilterChain"
    },
    {
      "classLoaderName": "app",
      "methodName": "doFilter",
      "fileName": "ApplicationFilterChain.java",
      "lineNumber": 162,
      "nativeMethod": false,
      "className": "org.apache.catalina.core.ApplicationFilterChain"
    },
    {
      "classLoaderName": "app",
      "methodName": "doFilterInternal",
      "fileName": "RequestContextFilter.java",
      "lineNumber": 100,
      "nativeMethod": false,
      "className": "org.springframework.web.filter.RequestContextFilter"
    },
    {
      "classLoaderName": "app",
      "methodName": "doFilter",
      "fileName": "OncePerRequestFilter.java",
      "lineNumber": 119,
      "nativeMethod": false,
      "className": "org.springframework.web.filter.OncePerRequestFilter"
    },
    {
      "classLoaderName": "app",
      "methodName": "internalDoFilter",
      "fileName": "ApplicationFilterChain.java",
      "lineNumber": 189,
      "nativeMethod": false,
      "className": "org.apache.catalina.core.ApplicationFilterChain"
    },
    {
      "classLoaderName": "app",
      "methodName": "doFilter",
      "fileName": "ApplicationFilterChain.java",
      "lineNumber": 162,
      "nativeMethod": false,
      "className": "org.apache.catalina.core.ApplicationFilterChain"
    },
    {
      "classLoaderName": "app",
      "methodName": "doFilter",
      "fileName": "OncePerRequestFilter.java",
      "lineNumber": 103,
      "nativeMethod": false,
      "className": "org.springframework.web.filter.OncePerRequestFilter"
    },
    {
      "classLoaderName": "app",
      "methodName": "internalDoFilter",
      "fileName": "ApplicationFilterChain.java",
      "lineNumber": 189,
      "nativeMethod": false,
      "className": "org.apache.catalina.core.ApplicationFilterChain"
    },
    {
      "classLoaderName": "app",
      "methodName": "doFilter",
      "fileName": "ApplicationFilterChain.java",
      "lineNumber": 162,
      "nativeMethod": false,
      "className": "org.apache.catalina.core.ApplicationFilterChain"
    },
    {
      "classLoaderName": "app",
      "methodName": "doFilter",
      "fileName": "OncePerRequestFilter.java",
      "lineNumber": 103,
      "nativeMethod": false,
      "className": "org.springframework.web.filter.OncePerRequestFilter"
    },
    {
      "classLoaderName": "app",
      "methodName": "internalDoFilter",
      "fileName": "ApplicationFilterChain.java",
      "lineNumber": 189,
      "nativeMethod": false,
      "className": "org.apache.catalina.core.ApplicationFilterChain"
    },
    {
      "classLoaderName": "app",
      "methodName": "doFilter",
      "fileName": "ApplicationFilterChain.java",
      "lineNumber": 162,
      "nativeMethod": false,
      "className": "org.apache.catalina.core.ApplicationFilterChain"
    },
    {
      "classLoaderName": "app",
      "methodName": "invoke",
      "fileName": "ApplicationDispatcher.java",
      "lineNumber": 711,
      "nativeMethod": false,
      "className": "org.apache.catalina.core.ApplicationDispatcher"
    },
    {
      "classLoaderName": "app",
      "methodName": "processRequest",
      "fileName": "ApplicationDispatcher.java",
      "lineNumber": 461,
      "nativeMethod": false,
      "className": "org.apache.catalina.core.ApplicationDispatcher"
    },
    {
      "classLoaderName": "app",
      "methodName": "doForward",
      "fileName": "ApplicationDispatcher.java",
      "lineNumber": 385,
      "nativeMethod": false,
      "className": "org.apache.catalina.core.ApplicationDispatcher"
    },
    {
      "classLoaderName": "app",
      "methodName": "forward",
      "fileName": "ApplicationDispatcher.java",
      "lineNumber": 313,
      "nativeMethod": false,
      "className": "org.apache.catalina.core.ApplicationDispatcher"
    },
    {
      "classLoaderName": "app",
      "methodName": "custom",
      "fileName": "StandardHostValve.java",
      "lineNumber": 403,
      "nativeMethod": false,
      "className": "org.apache.catalina.core.StandardHostValve"
    },
    {
      "classLoaderName": "app",
      "methodName": "status",
      "fileName": "StandardHostValve.java",
      "lineNumber": 249,
      "nativeMethod": false,
      "className": "org.apache.catalina.core.StandardHostValve"
    },
    {
      "classLoaderName": "app",
      "methodName": "throwable",
      "fileName": "StandardHostValve.java",
      "lineNumber": 344,
      "nativeMethod": false,
      "className": "org.apache.catalina.core.StandardHostValve"
    },
    {
      "classLoaderName": "app",
      "methodName": "invoke",
      "fileName": "StandardHostValve.java",
      "lineNumber": 169,
      "nativeMethod": false,
      "className": "org.apache.catalina.core.StandardHostValve"
    },
    {
      "classLoaderName": "app",
      "methodName": "invoke",
      "fileName": "ErrorReportValve.java",
      "lineNumber": 92,
      "nativeMethod": false,
      "className": "org.apache.catalina.valves.ErrorReportValve"
    },
    {
      "classLoaderName": "app",
      "methodName": "invoke",
      "fileName": "StandardEngineValve.java",
      "lineNumber": 78,
      "nativeMethod": false,
      "className": "org.apache.catalina.core.StandardEngineValve"
    },
    {
      "classLoaderName": "app",
      "methodName": "service",
      "fileName": "CoyoteAdapter.java",
      "lineNumber": 357,
      "nativeMethod": false,
      "className": "org.apache.catalina.connector.CoyoteAdapter"
    },
    {
      "classLoaderName": "app",
      "methodName": "service",
      "fileName": "Http11Processor.java",
      "lineNumber": 382,
      "nativeMethod": false,
      "className": "org.apache.coyote.http11.Http11Processor"
    },
    {
      "classLoaderName": "app",
      "methodName": "process",
      "fileName": "AbstractProcessorLight.java",
      "lineNumber": 65,
      "nativeMethod": false,
      "className": "org.apache.coyote.AbstractProcessorLight"
    },
    {
      "classLoaderName": "app",
      "methodName": "process",
      "fileName": "AbstractProtocol.java",
      "lineNumber": 893,
      "nativeMethod": false,
      "className": "org.apache.coyote.AbstractProtocol$ConnectionHandler"
    },
    {
      "classLoaderName": "app",
      "methodName": "doRun",
      "fileName": "NioEndpoint.java",
      "lineNumber": 1726,
      "nativeMethod": false,
      "className": "org.apache.tomcat.util.net.NioEndpoint$SocketProcessor"
    },
    {
      "classLoaderName": "app",
      "methodName": "run",
      "fileName": "SocketProcessorBase.java",
      "lineNumber": 49,
      "nativeMethod": false,
      "className": "org.apache.tomcat.util.net.SocketProcessorBase"
    },
    {
      "classLoaderName": "app",
      "methodName": "runWorker",
      "fileName": "ThreadPoolExecutor.java",
      "lineNumber": 1191,
      "nativeMethod": false,
      "className": "org.apache.tomcat.util.threads.ThreadPoolExecutor"
    },
    {
      "classLoaderName": "app",
      "methodName": "run",
      "fileName": "ThreadPoolExecutor.java",
      "lineNumber": 659,
      "nativeMethod": false,
      "className": "org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker"
    },
    {
      "classLoaderName": "app",
      "methodName": "run",
      "fileName": "TaskThread.java",
      "lineNumber": 61,
      "nativeMethod": false,
      "className": "org.apache.tomcat.util.threads.TaskThread$WrappingRunnable"
    },
    {
      "moduleName": "java.base",
      "moduleVersion": "17.0.2",
      "methodName": "run",
      "fileName": "Thread.java",
      "lineNumber": 833,
      "nativeMethod": false,
      "className": "java.lang.Thread"
    }
  ],
  "type": "about:blank",
  "title": "Unauthorized",
  "status": "UNAUTHORIZED",
  "detail": "Full authentication is required to access this resource",
  "message": "Unauthorized: Full authentication is required to access this resource",
  "localizedMessage": "Unauthorized: Full authentication is required to access this resource"
}

对于值"Hello%here",我向那里发送GET/api/v1/ECHO/Hello%25here,我收到的结果与分号相同.

任何其他特殊字符似乎都能被Spring正确解码,但这3个字符却不能.

Am I missing something ?
Is there any good way to achieve this, without having to tell spring "hey don't decode the path variables, I will do it by myself" and without having to mess with security configuration (as mentioned in https://www.baeldung.com/spring-slash-character-in-url) ?

推荐答案

为了达到你的要求,你将不得不在tomcat和/或一些定制的重写规则中做一些深入的configurations.所有这些都很容易适得其反,因为它会在应用程序的uri匹配器上造成故障,甚至更糟,会造成安全故障.

在这里,最简单的方法是将控制器从期望使用特殊字符作为@PathVariable的字符串转换为使用@RequestParam的查询参数.

因此,与其使用

    @GetMapping("echo/{value}")
    public ResponseEntity<String> echo(@PathVariable("value") String value) {
        return ResponseEntity
                .status(HttpStatus.OK)
                .contentType(MediaType.TEXT_XML)
                .body(value);
    }

你可能会变成

   @GetMapping("echo/")
    public ResponseEntity<String> echo(@RequestParam("value") String value) {
        return ResponseEntity
                .status(HttpStatus.OK)
                .contentType(MediaType.TEXT_XML)
                .body(value);
    }

对于值"Hello/There",我发送GET/API/v1/ECHO/Hello%2Fhere

然后try 使用GET/API/v1/ECHO?VALUE=Hello%2Fhere

Java相关问答推荐

如何打印本系列的第n项y=-(1)-(1+2)+(1+2+3)+(1+2+3+4)-(1+2+3+4+5)...Java中的(1+2+3+4...+n)

为什么一个Test的instance?& gt;在构造函数中接受非空对象?

try 创建一个对象,使用它,然后使用一条语句将其存储为列表

不推荐使用的Environment.getExternalStorageDirectory().getAbsolutePath()返回的值不同于新的getExternalFilesDir(空)?

为什么不应用类型推断?

Spring Boot Maven包

呈现文本和四舍五入矩形时出现的JavaFX窗格白色瑕疵

通过移动一个类解决了潜在的StubbingProblem.它怎麽工作?

将Spring Boot 3.2.0升级到3.2.1后查询执行错误

Java Mooc.fi Part 12_01.Hideout -返回和删除方法

如何仅使用键/ID的一部分(组合)高效地返回映射值?

Oj算法 MatrixR032从字符串、归一化和余弦相似度计算创建

在实例化中指定泛型类型与不指定泛型类型之间的区别

具有多个分析模式的复杂分隔字符串的正则表达式

在Spring Boot中使用咖啡因进行缓存-根据输出控制缓存

Intellij 2023 IDE:始终在顶部显示菜单栏

如何判断元素计数并在流的中间抛出异常?

Maven创建带有特定类的Spring Boot jar和普通jar

找到差异最小的回文

为什么这个printKthToLast递归(java)从头到尾开始?