天天看點

java安全編碼指南之:檔案IO操作

簡介

對于檔案的IO操作應該是我們經常會使用到的,因為檔案的複雜性,我們在使用File操作的時候也有很多需要注意的地方,下面我一起來看看吧。

建立檔案的時候指定合适的權限

不管是在windows還是linux,檔案都有權限控制的概念,我們可以設定檔案的owner,還有檔案的permission,如果檔案權限沒有控制好的話,惡意使用者就有可能對我們的檔案進行惡意操作。

是以我們在檔案建立的時候就需要考慮到權限的問題。

很遺憾的是,java并不是以檔案操作見長的,是以在JDK1.6之前,java的IO操作是非常弱的,基本的檔案操作類,比如FileOutputStream和FileWriter并沒有權限的選項。

Writer out = new FileWriter("file");      

那麼怎麼處理呢?

在JDK1.6之前,我們需要借助于一些本地方法來實作權限的修改功能。

在JDK1.6之後,java引入了NIO,可以通過NIO的一些特性來控制檔案的權限功能。

我們看一下Files工具類的createFile方法:

    public static Path createFile(Path path, FileAttribute<?>... attrs)
throws IOException
{
newByteChannel(path, DEFAULT_CREATE_OPTIONS, attrs).close();
return path;
}      

其中FileAttribute就是檔案的屬性,我們看一下怎麼指定檔案的權限:

    public void createFileWithPermission() throws IOException {
Set<PosixFilePermission> perms =
PosixFilePermissions.fromString("rw-------");
FileAttribute<Set<PosixFilePermission>> attr =
PosixFilePermissions.asFileAttribute(perms);
Path file = new File("/tmp/www.flydean.com").toPath();
Files.createFile(file,attr);
}      

注意檢查檔案操作的傳回值

java中很多檔案操作是有傳回值的,比如file.delete(),我們需要根據傳回值來判斷檔案操作是否完成,是以不要忽略了傳回值。

删除使用過後的臨時檔案

如果我們使用到不需要永久存儲的檔案時,就可以很友善的使用File的createTempFile來建立臨時檔案。臨時檔案的名字是随機生成的,我們希望在臨時檔案使用完畢之後将其删除。

怎麼删除呢?File提供了一個deleteOnExit方法,這個方法會在JVM退出的時候将檔案删除。

注意,這裡的JVM一定要是正常退出的,如果是非正常退出,檔案不會被删除。

我們看下面的例子:

    public void wrongDelete() throws IOException {
File f = File.createTempFile("tmpfile",".tmp");
FileOutputStream fop = null;
try {
            fop = new FileOutputStream(f);
String str = "Data";
            fop.write(str.getBytes());
            fop.flush();
} finally {
// 因為Stream沒有被關閉,是以檔案在windows平台上面不會被删除
            f.deleteOnExit(); // 在JVM退出的時候删除臨時檔案

if (fop != null) {
try {
                    fop.close();
} catch (IOException x) {
// Handle error
}
}
}
}      

上面的例子中,我們建立了一個臨時檔案,并且在finally中調用了deleteOnExit方法,但是因為在調用該方法的時候,Stream并沒有關閉,是以在windows平台上會出現檔案沒有被删除的情況。

怎麼解決呢?

NIO提供了一個DELETE_ON_CLOSE選項,可以保證檔案在關閉之後就被删除:

    public void correctDelete() throws IOException {
Path tempFile = null;
            tempFile = Files.createTempFile("tmpfile", ".tmp");
try (BufferedWriter writer =
Files.newBufferedWriter(tempFile, Charset.forName("UTF8"),
StandardOpenOption.DELETE_ON_CLOSE)) {
// Write to file
}
}      

上面的例子中,我們在writer的建立過程中加入了StandardOpenOption.DELETE_ON_CLOSE,那麼檔案将會在writer關閉之後被删除。

釋放不再被使用的資源

如果資源不再被使用了,我們需要記得關閉他們,否則就會造成資源的洩露。

但是很多時候我們可能會忘記關閉,那麼該怎麼辦呢?JDK7中引入了try-with-resources機制,隻要把實作了Closeable接口的資源放在try語句中就會自動被關閉,很友善。

注意Buffer的安全性

NIO中提供了很多非常有用的Buffer類,比如IntBuffer, CharBuffer 和 ByteBuffer等,這些Buffer實際上是對底層的數組的封裝,雖然建立了新的Buffer對象,但是這個Buffer是和底層的數組相關聯的,是以不要輕易的将Buffer暴露出去,否則可能會修改底層的數組。

    public CharBuffer getBuffer(){
char[] dataArray = new char[10];
return CharBuffer.wrap(dataArray);
}      

上面的例子暴露了CharBuffer,實際上也暴露了底層的char數組。

有兩種方式對其進行改進:

    public CharBuffer getBuffer1(){
char[] dataArray = new char[10];
return CharBuffer.wrap(dataArray).asReadOnlyBuffer();
}      

第一種方式就是将CharBuffer轉換成為隻讀的。

第二種方式就是建立一個新的Buffer,切斷Buffer和數組的聯系:

    public CharBuffer getBuffer2(){
char[] dataArray = new char[10];
CharBuffer cb = CharBuffer.allocate(dataArray.length);
        cb.put(dataArray);
return cb;
}      

注意 Process 的标準輸入輸出

java中可以通過Runtime.exec()來執行native的指令,而Runtime.exec()是有傳回值的,它的傳回值是一個Process對象,用來控制和擷取native程式的執行資訊。

預設情況下,建立出來的Process是沒有自己的I/O stream的,這就意味着Process使用的是父process的I/O(stdin, stdout, stderr),Process提供了下面的三種方法來擷取I/O:

getOutputStream()
getInputStream()
getErrorStream()      

如果是使用parent process的IO,那麼在有些系統上面,這些buffer空間比較小,如果出現大量輸入輸出操作的話,就有可能被阻塞,甚至是死鎖。

怎麼辦呢?我們要做的就是将Process産生的IO進行處理,以防止Buffer的阻塞。

public class StreamProcesser implements Runnable{
private final InputStream is;
private final PrintStream os;

StreamProcesser(InputStream is, PrintStream os){
this.is=is;
this.os=os;
}

@Override
public void run() {
try {
int c;
while ((c = is.read()) != -1)
                os.print((char) c);
} catch (IOException x) {
// Handle error
}
}

public static void main(String[] args) throws IOException, InterruptedException {
Runtime rt = Runtime.getRuntime();
Process proc = rt.exec("vscode");

Thread errorGobbler
= new Thread(new StreamProcesser(proc.getErrorStream(), System.err));

Thread outputGobbler
= new Thread(new StreamProcesser(proc.getInputStream(), System.out));

        errorGobbler.start();
        outputGobbler.start();

int exitVal = proc.waitFor();
        errorGobbler.join();
        outputGobbler.join();
}
}      

上面的例子中,我們建立了一個StreamProcesser來處理Process的Error和Input。

InputStream.read() 和 Reader.read()

InputStream和Reader都有一個read()方法,這兩個方法的不同之處就是InputStream read的是Byte,而Reader read的是char。

雖然Byte的範圍是-128到127,但是InputStream.read()會将讀取到的Byte轉換成0-255(0x00-0xff)範圍的int。

Char的範圍是0x0000-0xffff,Reader.read()将會傳回同樣範圍的int值:0x0000-0xffff。

如果傳回值是-1,表示的是Stream結束了。這裡-1的int表示是:0xffffffff。

我們在使用的過程中,需要對讀取的傳回值進行判斷,以用來區分Stream的邊界。

我們考慮這樣的一個問題:

FileInputStream in;
byte data;
while ((data = (byte) in.read()) != -1) {
}      

上面我們将InputStream的read結果先進行byte的轉換,然後再判斷是否等于-1。會有什麼問題呢?

如果Byte本身的值是0xff,本身是一個-1,但是InputStream在讀取之後,将其轉換成為0-255範圍的int,那麼轉換之後的int值是:0x000000FF, 再次進行byte轉換,将會截取最後的Oxff, Oxff == -1,最終導緻錯誤的判斷Stream結束。

是以我們需要先做傳回值的判斷,然後再進行轉換:

FileInputStream in;
int inbuff;
byte data;
while ((inbuff = in.read()) != -1) {
  data = (byte) inbuff;
// ... 
}      

拓展閱讀:

這段代碼的輸出結果是多少呢?(int)(char)(byte)-1

首先-1轉換成為byte:-1是0xffffffff,轉換成為byte直接截取最後幾位,得到0xff,也就是-1.

然後byte轉換成為char:0xff byte是有符号的,轉換成為2個位元組的char需要進行符号位擴充,變成0xffff,但是char是無符号的,對應的十進制是65535。

最後char轉換成為int,因為char是無符号的,是以擴充成為0x0000ffff,對應的十進制數是65535.

同樣的下面的例子中,如果提前使用char對int進行轉換,因為char的範圍是無符号的,是以永遠不可能等于-1.

FileReader in;
char data;
while ((data = (char) in.read()) != -1) {
// ...
}      

write() 方法不要超出範圍

在OutputStream中有一個很奇怪的方法,就是write,我們看下write方法的定義:

    public abstract void write(int b) throws IOException;      

write接收一個int參數,但是實際上寫入的是一個byte。

因為int和byte的範圍不一樣,是以傳入的int将會被截取最後的8位來轉換成一個byte。

是以我們在使用的時候一定要判斷寫入的範圍:

    public void writeInt(int value){
int intValue = Integer.valueOf(value);
if (intValue < 0 || intValue > 255) {
throw new ArithmeticException("Value超出範圍");
}
System.out.write(value);
System.out.flush();
}      

或者有些Stream操作是可以直接writeInt的,我們可以直接調用。

注意帶數組的read的使用

InputStream有兩種帶數組的read方法:

public int read(byte b[]) throws IOException      

public int read(byte b[], int off, int len) throws IOException      

如果我們使用了這兩種方法,那麼一定要注意讀取到的byte數組是否被填滿,考慮下面的一個例子:

    public String wrongRead(InputStream in) throws IOException {
byte[] data = new byte[1024];
if (in.read(data) == -1) {
throw new EOFException();
}
return new String(data, "UTF-8");
}      

如果InputStream的資料并沒有1024,或者說因為網絡的原因并沒有将1024填充滿,那麼我們将會得到一個沒有填充滿的數組,那麼我們使用起來其實是有問題的。

怎麼正确的使用呢?

    public String readArray(InputStream in) throws IOException {
int offset = 0;
int bytesRead = 0;
byte[] data = new byte[1024];
while ((bytesRead = in.read(data, offset, data.length - offset))
!= -1) {
            offset += bytesRead;
if (offset >= data.length) {
break;
}
}
String str = new String(data, 0, offset, "UTF-8");
return str;
}      

我們需要記錄實際讀取的byte數目,通過記載偏移量,我們得到了最終實際讀取的結果。

或者我們可以使用DataInputStream的readFully方法,保證讀取完整的byte數組。

little-endian和big-endian的問題

java中的資料預設是以big-endian的方式來存儲的,DataInputStream中的readByte(), readShort(), readInt(), readLong(), readFloat(), 和 readDouble()預設也是以big-endian來讀取資料的,如果在和其他的以little-endian進行互動的過程中,就可能出現問題。

我們需要的是将little-endian轉換成為big-endian。

怎麼轉換呢?

比如,我們想要讀取一個int,可以首先使用read方法讀取4個位元組,然後再對讀取的4個位元組做little-endian到big-endian的轉換。

    public void method1(InputStream inputStream) throws IOException {
try(DataInputStream dis = new DataInputStream(inputStream)) {
byte[] buffer = new byte[4];
int bytesRead = dis.read(buffer);  // Bytes are read into buffer
if (bytesRead != 4) {
throw new IOException("Unexpected End of Stream");
}
int serialNumber =
ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN).getInt();
}
}      

上面的例子中,我們使用了ByteBuffer提供的wrap和order方法來對Byte數組進行轉換。

當然我們也可以自己手動進行轉換。

還有一個最簡單的方法,就是調用JDK1.5之後的reverseBytes() 直接進行小端到大端的轉換。

    public  int reverse(int i) {
return Integer.reverseBytes(i);
}