Spring 启动请求正文 (POJO) 不处理特殊字符
Spring Boot Request Body (POJO) doesn't handle special characters
我有一个 Spring 带 REST 的启动 API:
@Operation(summary = "Update your user")
@PatchMapping(consumes = {MediaType.APPLICATION_JSON_VALUE})
@PreAuthorize("hasAuthority('MANAGE_MY_ACCOUNT')")
@ApiResponse(responseCode = "204", description = "Successfully updated user")
@ApiResponse(responseCode = "400", description = "Cannot update user",
content = @Content(schema = @Schema(implementation = ApiErrorResponse.class)))
@ApiResponse(responseCode = "404", description = "Unknown User Id",
content = @Content(schema = @Schema(implementation = ApiErrorResponse.class)))
ResponseEntity<Void> updateUser(@RequestBody UpdateUserRequest request);
它在请求正文中接受 UpdateUserRequest
作为 POJO。
@Getter
@NoArgsConstructor
public class UpdateUserRequest {
@Length(max = 100)
private String name;
@Length(max = 100)
private String givenName;
@Length(max = 100)
private String familyName;
}
当我使用以下请求调用 API 时:
{
"name": "Rafał Laskowski",
"givenName": "Rafał",
"familyName": "Laskowski"
}
name
和 givenName
编码不正确。我得到的是 RafaÅ
。在调试 @RestController
时,我注意到这些名称被错误地转换为字节数组,如下所示:
目前我尝试过的:
- 将
@PatchMapping
的 consumes
参数设置为 MediaType.APPLICATION_JSON_VALUE
- 以上为字符串添加
;charset=UTF-8
- 设置 JVM -Dfile.encoding=UTF-8
- 设置-Dsun.jnu.encoding=UTF-8
- 在 application.yml
中设置 sping.mandatory-file-encoding: UTF-8
- 在application.yml
中设置server.servlet.encoding.charset: UTF-8
log.info(System.getProperty("file.encoding"));
返回 UTF-8
我认为它可能是 Jackson 的对象映射器配置,但我读到它不是这种情况,因为 JSON 只接受 UTF-x 编码。
我不知道为什么没有从请求中正确读取字符 ł
。
看起来问题出在我们应用程序的安全范围内。我们已经通过以下 class:
实现了 XSS 安全
public class XSSRequestWrapper extends HttpServletRequestWrapper {
private final HttpServletRequest request;
private final ResettableServletInputStream servletStream;
private byte[] rawData;
public XSSRequestWrapper(HttpServletRequest request) {
super(request);
this.request = request;
this.servletStream = new ResettableServletInputStream();
}
public static String stripXSS(String value) {
if (value == null) {
return null;
}
value = ESAPI.encoder()
.canonicalize(value)
.replaceAll("[=10=]", "");
return Jsoup.clean(value, Whitelist.none());
}
public void resetInputStream(byte[] newRawData) {
rawData = newRawData;
servletStream.stream = new ByteArrayInputStream(newRawData);
}
@Override
public ServletInputStream getInputStream() throws IOException {
if (rawData == null) {
rawData = IOUtils.toByteArray(this.request.getReader(), Charsets.UTF_8);
servletStream.stream = new ByteArrayInputStream(rawData);
}
return servletStream;
}
@Override
public BufferedReader getReader() throws IOException {
if (rawData == null) {
rawData = IOUtils.toByteArray(this.request.getReader(), Charsets.UTF_8);
servletStream.stream = new ByteArrayInputStream(rawData);
}
return new BufferedReader(new InputStreamReader(servletStream));
}
@Override
public String[] getParameterValues(String parameter) {
String[] values = super.getParameterValues(parameter);
if (values == null) {
return null;
}
int count = values.length;
String[] encodedValues = new String[count];
for (int i = 0; i < count; i++) {
encodedValues[i] = stripXSS(values[i]);
}
return encodedValues;
}
@Override
public String getParameter(String parameter) {
String value = super.getParameter(parameter);
return stripXSS(value);
}
@Override
public String getHeader(String name) {
String value = super.getHeader(name);
return stripXSS(value);
}
@Override
public Enumeration<String> getHeaders(String name) {
List<String> result = new ArrayList<>();
Enumeration<String> headers = super.getHeaders(name);
while (headers.hasMoreElements()) {
String header = headers.nextElement();
String[] tokens = header.split(",");
for (String token : tokens) {
result.add(stripXSS(token));
}
}
return Collections.enumeration(result);
}
private class ResettableServletInputStream extends ServletInputStream {
private InputStream stream;
@Override
public int read() throws IOException {
return stream.read();
}
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener listener) {
}
}
}
解决问题的真正方法是改变 getInputStream()
方法的工作方式。
已更改:
@Override
public ServletInputStream getInputStream() throws IOException {
if (rawData == null) {
rawData = IOUtils.toByteArray(this.request.getReader(), Charsets.UTF_8);
servletStream.stream = new ByteArrayInputStream(rawData);
}
return servletStream;
}
到
@Override
public ServletInputStream getInputStream() throws IOException {
if (rawData == null) {
rawData = IOUtils.toByteArray(this.request.getReader(), Charsets.ISO_8859_1);
servletStream.stream = new ByteArrayInputStream(rawData);
}
return servletStream;
}
问题解决了。
免责声明:我不知道为什么:)
我有一个 Spring 带 REST 的启动 API:
@Operation(summary = "Update your user")
@PatchMapping(consumes = {MediaType.APPLICATION_JSON_VALUE})
@PreAuthorize("hasAuthority('MANAGE_MY_ACCOUNT')")
@ApiResponse(responseCode = "204", description = "Successfully updated user")
@ApiResponse(responseCode = "400", description = "Cannot update user",
content = @Content(schema = @Schema(implementation = ApiErrorResponse.class)))
@ApiResponse(responseCode = "404", description = "Unknown User Id",
content = @Content(schema = @Schema(implementation = ApiErrorResponse.class)))
ResponseEntity<Void> updateUser(@RequestBody UpdateUserRequest request);
它在请求正文中接受 UpdateUserRequest
作为 POJO。
@Getter
@NoArgsConstructor
public class UpdateUserRequest {
@Length(max = 100)
private String name;
@Length(max = 100)
private String givenName;
@Length(max = 100)
private String familyName;
}
当我使用以下请求调用 API 时:
{
"name": "Rafał Laskowski",
"givenName": "Rafał",
"familyName": "Laskowski"
}
name
和 givenName
编码不正确。我得到的是 RafaÅ
。在调试 @RestController
时,我注意到这些名称被错误地转换为字节数组,如下所示:
目前我尝试过的:
- 将
@PatchMapping
的consumes
参数设置为MediaType.APPLICATION_JSON_VALUE
- 以上为字符串添加
;charset=UTF-8
- 设置 JVM -Dfile.encoding=UTF-8
- 设置-Dsun.jnu.encoding=UTF-8
- 在 application.yml 中设置
- 在application.yml 中设置
log.info(System.getProperty("file.encoding"));
返回 UTF-8
sping.mandatory-file-encoding: UTF-8
server.servlet.encoding.charset: UTF-8
我认为它可能是 Jackson 的对象映射器配置,但我读到它不是这种情况,因为 JSON 只接受 UTF-x 编码。
我不知道为什么没有从请求中正确读取字符 ł
。
看起来问题出在我们应用程序的安全范围内。我们已经通过以下 class:
实现了 XSS 安全public class XSSRequestWrapper extends HttpServletRequestWrapper {
private final HttpServletRequest request;
private final ResettableServletInputStream servletStream;
private byte[] rawData;
public XSSRequestWrapper(HttpServletRequest request) {
super(request);
this.request = request;
this.servletStream = new ResettableServletInputStream();
}
public static String stripXSS(String value) {
if (value == null) {
return null;
}
value = ESAPI.encoder()
.canonicalize(value)
.replaceAll("[=10=]", "");
return Jsoup.clean(value, Whitelist.none());
}
public void resetInputStream(byte[] newRawData) {
rawData = newRawData;
servletStream.stream = new ByteArrayInputStream(newRawData);
}
@Override
public ServletInputStream getInputStream() throws IOException {
if (rawData == null) {
rawData = IOUtils.toByteArray(this.request.getReader(), Charsets.UTF_8);
servletStream.stream = new ByteArrayInputStream(rawData);
}
return servletStream;
}
@Override
public BufferedReader getReader() throws IOException {
if (rawData == null) {
rawData = IOUtils.toByteArray(this.request.getReader(), Charsets.UTF_8);
servletStream.stream = new ByteArrayInputStream(rawData);
}
return new BufferedReader(new InputStreamReader(servletStream));
}
@Override
public String[] getParameterValues(String parameter) {
String[] values = super.getParameterValues(parameter);
if (values == null) {
return null;
}
int count = values.length;
String[] encodedValues = new String[count];
for (int i = 0; i < count; i++) {
encodedValues[i] = stripXSS(values[i]);
}
return encodedValues;
}
@Override
public String getParameter(String parameter) {
String value = super.getParameter(parameter);
return stripXSS(value);
}
@Override
public String getHeader(String name) {
String value = super.getHeader(name);
return stripXSS(value);
}
@Override
public Enumeration<String> getHeaders(String name) {
List<String> result = new ArrayList<>();
Enumeration<String> headers = super.getHeaders(name);
while (headers.hasMoreElements()) {
String header = headers.nextElement();
String[] tokens = header.split(",");
for (String token : tokens) {
result.add(stripXSS(token));
}
}
return Collections.enumeration(result);
}
private class ResettableServletInputStream extends ServletInputStream {
private InputStream stream;
@Override
public int read() throws IOException {
return stream.read();
}
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener listener) {
}
}
}
解决问题的真正方法是改变 getInputStream()
方法的工作方式。
已更改:
@Override
public ServletInputStream getInputStream() throws IOException {
if (rawData == null) {
rawData = IOUtils.toByteArray(this.request.getReader(), Charsets.UTF_8);
servletStream.stream = new ByteArrayInputStream(rawData);
}
return servletStream;
}
到
@Override
public ServletInputStream getInputStream() throws IOException {
if (rawData == null) {
rawData = IOUtils.toByteArray(this.request.getReader(), Charsets.ISO_8859_1);
servletStream.stream = new ByteArrayInputStream(rawData);
}
return servletStream;
}
问题解决了。 免责声明:我不知道为什么:)