在 Java Servlet 中读取、解析 POST 请求数据

1、简介

Java Servlet 是一个服务端组件,用于处理客户端传入的 HTTP 请求,通常我们需要通过 Servlet 中的 HttpServletRequest 对象来获取到客户端提交的请求数据。

本文将带你了解在 Java Servlet 中读取 Payload(即请求体)数据的各种方法,以及最佳实践和注意事项。

2、理解 Request Payload

Post 请求主要用于通过 HTTP 请求向服务器发送数据。这些数据可以是任何内容,从包含用户输入的表单数据到结构化数据如 JSON 和 XML,甚至是二进制文件。这些数据位于请求体中,与 URL 分开。这样可以实现更广泛和安全的数据传输。我们可以通过请求中的 Content-Type Header 标识不同类型的数据。

常见的 Content-Type 包括:

  • application/x-www-form-urlencoded:用于以键值对形式编码的表单数据
  • application/json:用于 JSON 格式的数据
  • application/xml:用于 XML 格式的数据
  • text/plain:用于发送纯文本
  • multipart/form-data:用于上传二进制文件和常规表单数据(form data)

3、读取 Post 请求体的方式

接下来,让我们看看从 POST Payload 中提取数据的不同方法。

3.1、使用 getParameter() 获取 URL 编码的表单数据

我们可以使用 HttpServletRequest 接口提供的 getParameter() 方法,使用通过 POST 请求提交的参数名检索特定表单数据。该方法使用表单参数名作为参数,并以字符串(String)形式返回相应的值。

举个例子:

@WebServlet(name = "FormDataServlet", urlPatterns = "/form-data")
public class FormDataServlet extends HttpServlet {

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp)
      throws IOException {
        String firstName = StringEscapeUtils.escapeHtml4(req.getParameter("first_name"));
        String lastName = StringEscapeUtils.escapeHtml4(req.getParameter("last_name"));

        resp.getWriter()
          .append("Full Name: ")
          .append(firstName)
          .append(" ")
          .append(lastName);
    }
}

这种方法可以处理键值对,但不适合处理复杂的数据结构。

我们还使用了 apache commons 文本库中 StringEscapeUtils 类的 escapeHtml4() 方法,通过对输入进行过滤(对特殊字符进行编码),以防止 XSS 攻击。该库的依赖如下:

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-text</artifactId>
    <version>1.10.0</version>
</dependency>

3.2、读取原始的字符串请求体

为了提高灵活性,我们可以使用 HttpServletRequest 接口的 getReader() 方法访问 Servlet 请求原始的文本流。

该方法返回一个 BufferedReader 对象,我们可以逐行读取数据:

protected void doPost(HttpServletRequest req, HttpServletResponse resp) 
  throws IOException {

    StringBuilder payload = new StringBuilder();
    try(BufferedReader reader = req.getReader()){
        String line;
        while ((line = reader.readLine()) != null){
            payload.append(line);
        }
    }

    resp.getWriter().append(countWordsFrequency(payload.toString()).toString());
}

需要注意:

  • 在读取较大的请求体时需要小心,可能会内存溢出。
  • 可能需要根据请求处理字符编码差异。

3.3、解析结构化数据格式(JSON、XML)

JSON 和 XML 等结构化数据格式被广泛用于客户端和服务器之间的数据交换。我们可以使用专用库将 Payload 解析为 Java 对象。

要解析 JSON 数据,我们可以使用 JacksonGson 等流行的库。在本例中,我们使用 Gson。其依赖如下:

<dependency>
    <groupId>com.google.code.gson</groupId>
    <artifactId>gson</artifactId>
    <version>2.10.1</version>
</dependency>

我们可以使用 BufferedReader 对象从 Request Body(请求体)中以纯文本形式读取 JSON Payload,然后使用 Gson 将其解析为 Java 对象。

解析 JSON 数据的示例如下:

protected void doPost(HttpServletRequest req, HttpServletResponse resp) 
  throws IOException {
    String contentType = req.getContentType();
    if (!("application/json".equals(contentType))) {
        // 非 JSON 类的请求体
        resp.sendError(HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE, 
          "Invalid content type");
        return;
    }
    try (BufferedReader reader = req.getReader()) {
        Gson gson = new Gson();
        Product newProduct = gson.fromJson(reader, Product.class);
        resp.getWriter()
            .append("Added new Product with name: ")
            .append(newProduct.getName());

    } catch (IOException ex) {
        req.setAttribute("message", "There was an error: " + ex.getMessage());
    }
}

我们应该在解析前验证 Content Type,以避免非法的数据格式和安全问题。

如果需要解析 XML 数据的话,可以使用 XStream 库。其 依赖 如下:

<dependency>
    <groupId>com.thoughtworks.xstream</groupId>
    <artifactId>xstream</artifactId>
    <version>1.4.20</version>
</dependency>

要从 Request Body(请求体)中解析 XML,我们可以像读取 JSON Payload 一样,将其读作纯文本,然后使用 XStream 将其解析为 Java 对象。

解析 XML POST 请求的示例如下:

protected void doPost(HttpServletRequest req, HttpServletResponse resp) 
  throws IOException {
    String contentType = req.getContentType();
    if (!("application/xml".equals(contentType))) {
        // 请求体的类型不是 XML
        resp.sendError(HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE, 
          "Invalid content type");
        return;
    }
    try (BufferedReader reader = req.getReader()) {
        XStream xStream = new XStream();
        xStream.allowTypesByWildcard(new String[] { "com.baeldung.**" });
        xStream.alias("Order", Order.class);
        Order order = (Order) xStream.fromXML(reader);

        resp.getWriter()
            .append("Created new Order with orderId: ")
            .append(order.getOrderId())
            .append(" for Product: ")
            .append(order.getProduct());
    } catch (IOException ex) {
        req.setAttribute("message", "There was an error: " + ex.getMessage());
    }
}

3.4、处理 multipart/form-data

multipart/form-data Content Type,专门用于处理包含二进制数据(如图片、视频或文档)和常规文本数据的表单,通常用来上传文件。

要处理 multipart/form-data 请求,我们必须用 @MultipartConfig 或在 web.xml 中配置 Servlet。

@MultipartConfig 提供了各种参数来控制文件上传行为,如 location(临时存储目录)、maxFileSize(单个上传文件的最大大小)和 maxRequestSize(整个请求的最大大小):

@MultipartConfig(fileSizeThreshold = 1024 * 1024, 
  maxFileSize = 1024 * 1024 * 5, 
  maxRequestSize = 1024 * 1024 * 5 * 5)
public class FileUploadServlet extends HttpServlet {
    // ...
}

在 Servlet 中,我们可以使用 getPart(String name) 方法获取 multipart/form-data 中特定 Part(部分)的信息,或者使用 getParts() 方法获取所有 Part 的信息。Part 接口提供了访问文件名、内容类型、大小和输入流等详细信息的方法。

使用 POST 请求上传文件示例如下:

protected void doPost(HttpServletRequest request, HttpServletResponse response) 
  throws ServletException, IOException {

    String uploadPath = getServletContext().getRealPath("") + 
      File.separator + UPLOAD_DIRECTORY;
    File uploadDir = new File(uploadPath);
    if (!uploadDir.exists()) {
        uploadDir.mkdir();
    }

    Part filePart = request.getPart("file");
    if (filePart != null) {
        String fileName = Paths.get(filePart.getSubmittedFileName())
          .getFileName().toString();
        if(fileName.isEmpty()){
            response.getWriter().println("Invalid File Name!");
            return;
        }
        if(!fileName.endsWith(".txt")){
            response.getWriter().println("Only .txt files are allowed!");
            return;
        }

        File file = new File(uploadPath, fileName);

        try (InputStream fileContent = filePart.getInputStream()) {
            Files.copy(fileContent, file.toPath(), 
              StandardCopyOption.REPLACE_EXISTING);
        } catch (IOException e) {
            response.getWriter().println("Error writing file: " + 
              e.getMessage());
            return;
        }

        response.getWriter()
            .println("File uploaded to: " + file.toPath());
    } else {
        response.getWriter()
            .println("File upload failed!");
    }
}

对于文件上传来说,在安全方面需要考虑些东西:

  • 防止路径遍历
  • 检查文件的类型
  • 检查文件的大小
  • 安全地写入文件

4、最佳实践和常见的问题

4.1、Content-Type 校验

我们应始终验证传入请求的 Content-Type,以确保服务器正确处理请求。这有助于防止非法的数据格式和潜在的安全漏洞。

例如,如果我们的 Servlet 期望使用 JSON,我们就应该在处理之前检查 Content-Type Header 是否为 application/json 类型:

String contentType = req.getContentType();
if (!("application/json".equals(contentType))) {
    resp.sendError(HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE, 
      "Invalid content type");
    return;
}

或者,可以使用 Apache Tika (内容检测和分析框架)实现一些更强大的功能。

4.2、异常处理

在读取和处理 Payload 时,应始终执行适当的错误处理。这可以确保应用能够从容应对意外情况,提高程序的健壮性。

还应该通过 HTTP 状态代码提供有意义的信息,这有助于告诉开发人员和用户出了什么问题。

4.3、性能优化

处理非常大的 Payload 会影响性能。为了优化,应该考虑限制传入请求的大小、流式处理数据以及避免数据复制。可以使用诸如 Apache Commons IO 这样的库,它可以帮助高效处理 Payload。

此外,还应确保 Servlet 不会执行阻塞操作,以免阻塞请求处理。

4.4、安全

在处理 POST Payload 时,安全是一个重要的考虑因素。

一些关键做法包括:

  • 数据验证:始终对输入的数据进行验证和过滤,以防止注入攻击。
  • 认证和授权:确保只有授权用户才能访问某些端点。
  • CSRF 保护:实现 CSRF(跨站请求伪造)保护,以防止用户不经意间从恶意网站发送未经授权的命令。
  • 数据加密:使用 HTTPS 加密传输中的数据,保护敏感信息。
  • 限制上传大小:设置上传文件的大小限制,防止拒绝服务攻击。

5、总结

本文介绍了在 Servlet 中读取和解析 POST 请求 Payload 的各种方法,介绍了如何获取表单、JSON、XML 以及 Multipart 文件等各种类型请求的数据,最后还介绍了最佳实践及注意事项。


Ref:https://www.baeldung.com/java-servlet-post-request-payload