24

目前我有以下代码用于阅读InputStream. 我将整个文件存储到一个StringBuilder变量中,然后处理这个字符串。

public static String getContentFromInputStream(InputStream inputStream)
// public static String getContentFromInputStream(InputStream inputStream,
// int maxLineSize, int maxFileSize)
{

    StringBuilder stringBuilder = new StringBuilder();
    BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
    String lineSeparator = System.getProperty("line.separator");
    String fileLine;

    boolean firstLine = true;
    try {
        // Expect some function which checks for line size limit.
        // eg: reading character by character to an char array and checking for
        // linesize in a loop until line feed is encountered.
        // if max line size limit is passed then throw an exception
        // if a line feed is encountered append the char array to a StringBuilder
        // after appending check the size of the StringBuilder
        // if file size exceeds the max file limit then throw an exception

        fileLine = bufferedReader.readLine();

        while (fileLine != null) {
            if (!firstLine) stringBuilder.append(lineSeparator);
            stringBuilder.append(fileLine);
            fileLine = bufferedReader.readLine();
            firstLine = false;
        }
    } catch (IOException e) {
        //TODO : throw or handle the exception
    }
    //TODO : close the stream

    return stringBuilder.toString();

}

该代码已与安全团队进行审查,并收到以下评论:

  1. BufferedReader.readLine容易受到 DOS(拒绝服务)攻击(无限长的行,不包含换行/回车的大文件)

  2. 变量的资源耗尽StringBuilder(文件包含的数据大于可用内存的情况)

以下是我能想到的解决方案:

  1. readLine创建方法 ( )的替代实现readLine(int limit),它检查否。读取的字节数,如果超过指定的限制,则抛出自定义异常。

  2. 逐行处理文件而不加载整个文件。(纯非Java解决方案:))

请建议是否有任何现有的库可以实现上述解决方案。还建议提供比提议的解决方案更稳健或更便于实施的任何替代解决方案。尽管性能也是一项主要要求,但安全性是第一位的。

4

8 回答 8

41

更新的答案

您想避免各种 DOS 攻击(在行、文件大小等方面)。但在函数结束时,您正试图将整个文件转换为一个文件String!!!假设您将行限制为 8 KB,但是如果有人向您发送包含两个 8 KB 行的文件会发生什么?行阅读部分会通过,但是当你最终将所有内容组合成一个字符串时,字符串将阻塞所有可用内存。

因此,由于最终您将所有内容都转换为一个字符串,因此限制行大小无关紧要,也不安全。您必须限制文件的整个大小。

其次,您基本上想要做的是,您正在尝试以块的形式读取数据。因此,您正在BufferedReader逐行使用和阅读它。但是你想要做的,以及你最后真正想要的 - 是一种逐个读取文件的方式。与其一次读取一行,不如一次读取 2 KB?

BufferedReader- 顾名思义 - 里面有一个缓冲区。您可以配置该缓冲区。假设您创建了一个BufferedReader缓冲区大小为 2 KB 的缓冲区:

BufferedReader reader = new BufferedReader(..., 2048);

现在,如果InputStream您传递给BufferedReader的数据有 100 KB 的数据,BufferedReader则会一次自动读取 2 KB。因此它将读取流 50 次,每次 2 KB (50x2KB = 100 KB)。同样,如果您BufferedReader使用 10 KB 的缓冲区大小创建,它将读取输入 10 次 (10x10KB = 100 KB)。

BufferedReader已经完成了逐块读取文件的工作。所以你不想在它上面逐行添加一个额外的层。只关注最终结果——如果你最后的文件太大(> 可用 RAM)——你将如何将它转换为String最后?

一种更好的方法是将事物作为CharSequence. 这就是安卓所做的。在整个 Android API 中,您会看到它们CharSequence随处返回。由于StringBuilder也是 的子类CharSequence,Android 将根据输入的大小/性质在内部使用 aString或 aStringBuilder或其他优化的字符串类。因此,您可以在阅读完所有内容后直接返回StringBuilder对象本身,而不是将其转换为String. 这对于大数据会更安全。StringBuilder它内部也保持了相同的缓冲区概念,它会在内部为大字符串分配多个缓冲区,而不是一个长字符串。

所以总的来说:

  • 限制整体文件大小,因为您将在某个时候处理整个内容。忘记限制或分割线
  • 分块阅读

使用 Apache Commons IO,您可以将数据从 a 读取BoundedInputStream到 aStringBuilder中,按 2 KB 块而不是行分割:

// import org.apache.commons.io.output.StringBuilderWriter;
// import org.apache.commons.io.input.BoundedInputStream;
// import org.apache.commons.io.IOUtils;

BoundedInputStream boundedInput = new BoundedInputStream(originalInput, <max-file-size>);
BufferedReader reader = new BufferedReader(new InputStreamReader(boundedInput), 2048);

StringBuilder output = new StringBuilder();
StringBuilderWriter writer = new StringBuilderWriter(output);

IOUtils.copy(reader, writer); // copies data from "reader" => "writer"
return output;

原始答案

使用Apache Commons IO库中的BoundedInputStream。您的工作变得更加轻松。

以下代码将执行您想要的操作:

public static String getContentFromInputStream(InputStream inputStream) {
  inputStream = new BoundedInputStream(inputStream, <number-of-bytes>);
  // Rest code are all same

您只需简单地InputStream用 a包裹您BoundedInputStream并指定最大尺寸。BoundedInputStream将负责将读取限制为最大大小。

或者您可以在创建阅读器时执行此操作:

BufferedReader bufferedReader = new BufferedReader(
  new InputStreamReader(
    new BoundedInputStream(inputStream, <no-of-bytes>)
  )
);

基本上我们在这里所做的是,我们在InputStream层本身限制读取大小,而不是在读取行时这样做。所以你最终得到了一个可重用的组件,比如BoundedInputStream它限制了 InputStream 层的读取,你可以在任何你想要的地方使用它。

编辑:添加脚注

编辑 2:根据评论添加了更新的答案

于 2013-06-17T07:25:36.753 回答
16

文件处理基本上有4种方法:

  1. 基于流的处理java.io.InputStream模型):可以选择在流周围放置一个 bufferedReader,迭代并从流中读取下一个可用文本(如果没有文本可用,则阻塞直到有一些可用),在读取时独立处理每段文本(迎合各种大小的文本)

  2. Chunk-Based Non-Blocking Processingjava.nio.channels.Channel模型):创建一组固定大小的缓冲区(表示要处理的“块”),依次读入每个缓冲区而不阻塞(nio API 委托给本地 IO,使用快速 O/S 级线程),当其他缓冲区继续异步加载时,您的主处理线程会在每个缓冲区被填充后依次选择它并处理固定大小的块。

  3. 部分文件处理(包括逐行处理)(可以利用 (1) 或 (2) 来隔离或构建每个“部分”):将文件格式分解为语义上有意义的子部分(如果可能!闯入行可能!),遍历流片段或块并在内存中建立内容,直到下一部分完全构建,一旦构建就处理每个部分。

  4. 整个文件处理java.nio.file.Files模型):一次操作将整个文件读入内存,处理完整的内容

你应该使用哪一个?
这取决于 - 您的文件内容和您需要的处理类型。
从资源使用效率的角度来看(从最好到最差)是:1,2,3,4。
从处理速度和效率的角度来看(从最好到最差)是:2,1,3,4。
从易于编程的角度来看(从最好到最差):4,3,1,2。
但是,某些类型的处理可能需要的不仅仅是最小的文本(排除 1,也可能是 2),并且某些文件格式可能没有内部部分(排除 3)。

你正在做 4。如果可以的话,我建议你换成 3(或更低)。

在 4 下,只有一种方法可以避免 DOS - 在将其读入内存之前限制大小(或就此复制到您的文件系统)。一旦读入就为时已晚。如果这不可能,请尝试 3、2 或 1。

限制文件大小

通常文件是通过 HTML 表单上传的。

如果使用 Servlet@MultipartConfig注释和上传request.getPart().getInputStream(),您可以控制从流中读取的数据量。另外,request.getPart().getSize()提前返回文件大小,如果足够小,您可以request.getPart().write(path)将文件写入磁盘。

如果使用 JSF 上传,那么 JSF 2.2(非常新)有标准的 html 组件<h:inputFile>( javax.faces.component.html.InputFile),它有一个 ; 的属性maxLength。JSF 2.2 之前的实现具有类似的自定义组件(例如,Tomahawk 具有<t:InputFileUpload>withmaxLength属性;PrimeFaces 具有<p:FileUpload>withsizeLimit属性)。

读取整个文件的替代方法

您使用InputStream,StringBuilder等的代码是读取整个文件的有效方式,但不一定是最简单的方式(代码行数最少)。

当您处理整个文件时,初级/普通开发人员可能会误解您正在执行有效的基于流的处理 - 因此请包含适当的注释。

如果您想要更少的代码,您可以尝试以下方法之一:

 List<String> stringList = java.nio.file.Files.readAllLines(path, charset);

 or 

 byte[] byteContents =  java.nio.file.Files.readAllBytes(path);

但它们需要小心,否则它们在资源使用方面可能效率低下。如果您使用readAllLines然后将List元素连接成一个String,那么您将消耗双倍的内存(对于List元素+连接的String)。同样,如果您使用readAllBytes, 然后编码为String( new String(byteContents, charset)),那么您再次使用“双”内存。因此最好直接针对List<String>or进行处理byte[],除非您将文件限制为足够小的大小。

于 2013-06-18T09:31:26.387 回答
3

而不是 readLine 使用 read 读取给定数量的字符。

在每个循环中检查已经读取了多少数据,如果超过一定数量,超过预期输入的最大值,停止它并返回错误并记录它。

于 2013-06-13T11:10:05.937 回答
2

复制巨大的二进制文件(通常不包含换行符)时,我遇到了类似的问题。执行 readline() 会导致将整个二进制文件读入一个字符串,从而导致OutOfMemory堆空间。

这是一个简单的 JDK 替代方案:

public static void main(String[] args) throws Exception
{
    byte[] array = new byte[1024];
    FileInputStream fis = new FileInputStream(new File("<Path-to-input-file>"));
    FileOutputStream fos = new FileOutputStream(new File("<Path-to-output-file>"));
    int length = 0;
    while((length = fis.read(array)) != -1)
    {
        fos.write(array, 0, length);
    }
    fis.close();
    fos.close();
}

注意事项:

  • 上面的示例使用 1K 字节的缓冲区复制文件。但是,如果您通过网络进行此复制,您可能需要调整缓冲区大小。

  • 如果您想使用FileChannelCommons IO之类的库,只需确保实现归结为上述内容

于 2013-06-22T05:01:20.473 回答
1

这对我有用,没有任何问题。

    char charArray[] = new char[ MAX_BUFFER_SIZE ];
    int i = 0;
    int c = 0;
    while((c = br.read()) != -1 && i < MAX_BUFFER_SIZE) {
        char character = (char) c;
        charArray[i++] = character;
   }
   return Arrays.copyOfRange(charArray,0,i); 
于 2016-12-30T05:02:25.833 回答
0

Apache httpCore 下有 EntityUtils 类。使用该类的 getString() 方法从响应内容中获取字符串。

于 2014-04-23T18:41:34.863 回答
0

除了Apache Commons IO FileUtils,我想不出其他解决方案。 FileUtils 类非常简单,因为所谓的 DOS 攻击不会直接来自顶层。读取和写入文件非常简单,只需一行代码即可完成

String content =FileUtils.readFileToString(new File(filePath));

您可以对此进行更多探索。

于 2013-06-21T11:14:19.283 回答
0

来自 Fortify Scan 的建议。您可以适应InputStream其他资源,例如HTTP request InputStream.

InputStream zipInput = zipFile.getInputStream(zipEntry);
Reader zipReader = new InputStreamReader(zipInput);
BufferedReader br = new BufferedReader(zipReader);
StringBuffer sb = new StringBuffer();
int intC;
while ((intC = br.read()) != -1){
    char c = (char)intC;
    if (c == "\n"){
       break;
    }
    if (sb.length >= MAX_STR_LEN){
       throw new Exception("Input too long");
    }
    sb.append(c);
}
String line = sb.toString();
于 2021-07-15T05:22:03.803 回答