使用Java读取文件或流的最强大方法(防止DoS攻击)

目前我有以下代码用于读取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(拒绝服务)攻击(infinte长度的行,包含没有换行/ cariage retrun的巨大文件)

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

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

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

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

请建议是否有任何现有的库实施上述解决方案。 还建议任何替代解决方案,其提供比提出的更强大或更方便实现。 虽然性能也是一项主要要求,但安全性是第一位的。

提前致谢。

更新的答案

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

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

其次,你基本上要做的是,你试图以块的forms读取数据。 所以你正在使用BufferedReader并逐行读取它。 但是你要做的是什么,以及你最终想要的是什么 – 是一种逐段阅读文件的方式。 为什么不一次读取2 KB,而不是一次读取一行?

BufferedReader – 按名称 – 里面有一个缓冲区。 您可以配置该缓冲区。 假设您创建一个缓冲区大小为2 KB的BufferedReader

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

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

BufferedReader已经完成了以块为单位读取文件的工作。 因此,您不希望在其上方添加额外的逐行图层。 只关注最终结果 – 如果你的文件结尾太大(>可用内存) – 你最后如何将它转换为String

一种更好的方法是将事物作为CharSequence传递。 这就是Android的function。 在整个Android API中,您将看到它们在任何地方都返回CharSequence 。 由于StringBuilder也是CharSequence的子类,因此Android将根据输入的大小/性质在内部使用StringStringBuilder或其他一些优化的字符串类。 因此,一旦读完所有内容,您就可以直接返回StringBuilder对象,而不是将其转换为String 。 这对大数据更安全。 StringBuilder还在其中维护相同的缓冲区概念,并且它将在内部为大字符串分配多个缓冲区,而不是一个长字符串。

整体而言:

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

使用Apache Commons IO,以下是将BoundedInputStream数据读入StringBuilder ,分为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, ); 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, ); // Rest code are all same 

您只需使用BoundedInputStream包装InputStream ,并指定最大大小。 BoundedInputStream将负责限制读取到最大大小。

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

 BufferedReader bufferedReader = new BufferedReader( new InputStreamReader( new BoundedInputStream(inputStream, ) ) ); 

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

编辑:添加脚注

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

基本上有4种方法可以进行文件处理:

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

  2. 基于块的非阻塞处理java.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组件javax.faces.component.html.InputFile ),它具有maxLength的属性; pre-JSF 2.2实现具有类似的自定义组件(例如,Tomahawk具有具有maxLength属性; PrimeFaces具有具有sizeLimit属性)。

读取整个文件的替代方法

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

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

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

  List stringList = java.nio.file.Files.readAllLines(path, charset); or byte[] byteContents = java.nio.file.Files.readAllBytes(path); 

但它们需要小心,否则它们在资源使用方面效率低下。 如果您使用readAllLines然后将List元素连接成一个String ,那么您将消耗双倍的内存(对于List元素+连接的String )。 类似地,如果你使用readAllBytes ,然后编码为Stringnew String(byteContents, charset) ),那么你再次使用“双”内存。 因此,最好直接对Listbyte[] ,除非您将文件限制为足够小的大小。

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

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

另外一点,我注意到你没有关闭你的BufferedInputStream。 你应该关闭你的BufferedReader finally块,因为它容易受到内存泄漏的影响。

 ... } catch (IOException e) { // throw or handle the exception } finally{ bufferedReader.close(); } 

无需显式关闭new InputStreamReader(inputStream)因为当您调用以关闭包装类bufferedReader时,它将自动关闭

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

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

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

注意事项:

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

  • 如果您想使用FileChannel或Commons IO之类的库,请确保实现归结为类似于上面的内容

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

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

您可以探索更多相关信息。

Apache httpCore下有类EntityUtils。 使用此类的getString()方法从Response内容中获取String。

这对我没有任何问题。

  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);