java安全編碼指南之:字符串和編碼

2020-09-16 flydean程序那些事

簡介

字符串是我們日常編碼過程中使用到最多的java類型了。全球各個地區的語言不同,即使使用了Unicode也會因為編碼格式的不同採用不同的編碼方式,如UTF-8,UTF-16,UTF-32等。

我們在使用字符和字符串編碼的過程中會遇到哪些問題呢?一起來看看吧。

使用變長編碼的不完全字符來創建字符串

在java中String的底層存儲char[]是以UTF-16進行編碼的。

注意,在JDK9之後,String的底層存儲已經變成了byte[]。

StringBuilder和StringBuffer還是使用的是char[]。

那麼當我們在使用InputStreamreader,OutputStreamWriter和String類進行String讀寫和構建的時候,就需要涉及到UTF-16和其他編碼的轉換。

我們來看一下從UTF-8轉換到UTF-16可能會遇到的問題。

先看一下UTF-8的編碼:

UTF-8使用1到4個字節表示對應的字符,而UTF-16使用2個或者4個字節來表示對應的字符。

轉換起來可能會出現什麼問題呢?

public String readByteWrong(InputStream inputStream) throws IOException { byte[] data = new byte[1024]; int offset = 0; int bytesRead = 0; String str=&34;; while ((bytesRead = inputStream.read(data, offset, data.length - offset)) != -1) { str += new String(data, offset, bytesRead, &34;); offset += bytesRead; if (offset >= data.length) { throw new IOException(&34;); } } return str; }

上面的代碼中,我們從Stream中讀取byte,每讀一次byte就將其轉換成為String。很明顯,UTF-8是變長的編碼,如果讀取byte的過程中,恰好讀取了部分UTF-8的代碼,那麼構建出來的String將是錯誤的。

我們需要下面這樣操作:

public String readByteCorrect(InputStream inputStream) throws IOException { Reader r = new InputStreamReader(inputStream, &34;); char[] data = new char[1024]; int offset = 0; int charRead = 0; String str=&34;; while ((charRead = r.read(data, offset, data.length - offset)) != -1) { str += new String(data, offset, charRead); offset += charRead; if (offset >= data.length) { throw new IOException(&34;); } } return str; }

我們使用了InputStreamReader,reader將會自動把讀取的數據轉換成為char,也就是說自動進行UTF-8到UTF-16的轉換。

所以不會出現問題。

char不能表示所有的Unicode

因為char是使用UTF-16來進行編碼的,對於UTF-16來說,U+0000 to U+D7FF 和 U+E000 to U+FFFF,這個範圍的字符,可以直接用一個char來表示。

但是對於U+010000 to U+10FFFF是使用兩個0xD800–0xDBFF和0xDC00–0xDFFF範圍的char來表示的。

這種情況下,兩個char合併起來才有意思,單獨一個char是沒有任何意義的。

考慮下面的我們的的一個subString的方法,該方法的本意是從輸入的字符串中找到第一個非字母的位置,然後進行字符串截取。

public static String subStringWrong(String string) { char ch; int i; for (i = 0; i < string.length(); i += 1) { ch = string.charAt(i); if (!Character.isLetter(ch)) { break; } } return string.substring(i); }

上面的例子中,我們一個一個的取出string中的char字符進行比較。如果遇到U+010000 to U+10FFFF範圍的字符,就可能報錯,誤以為該字符不是letter。

我們可以這樣修改:

public static String subStringCorrect(String string) { int ch; int i; for (i = 0; i < string.length(); i += Character.charCount(ch)) { ch = string.codePointAt(i); if (!Character.isLetter(ch)) { break; } } return string.substring(i); }

我們使用string的codePointAt方法,來返回字符串的Unicode code point,然後使用該code point來進行isLetter的判斷就好了。

注意Locale的使用

為了實現國際化支持,java引入了Locale的概念,而因為有了Locale,所以會導致字符串在進行轉換的過程中,產生意想不到變化。

考慮下面的例子:

public void toUpperCaseWrong(String input){ if(input.toUpperCase().equals(&34;)){ System.out.println(&34;); } }

我們期望的是英語,如果系統設置了Locale是其他語種的話,input.toUpperCase()可能得到完全不一樣的結果。

幸好,toUpperCase提供了一個locale的參數,我們可以這樣修改:

public void toUpperCaseRight(String input){ if(input.toUpperCase(Locale.ENGLISH).equals(&34;)){ System.out.println(&34;); } }

同樣的, DateFormat也存在著問題:

public void getDateInstanceWrong(Date date){ String myString = DateFormat.getDateInstance().format(date); } public void getDateInstanceRight(Date date){ String myString = DateFormat.getDateInstance(DateFormat.MEDIUM, Locale.US).format(date); }

我們在進行字符串比較的時候,一定要考慮到Locale影響。

文件讀寫中的編碼格式

我們在使用InputStream和OutputStream進行文件對寫的時候,因為是二進位,所以不存在編碼轉換的問題。

但是如果我們使用Reader和Writer來進行文件的對象,就需要考慮到文件編碼的問題。

如果文件是UTF-8編碼的,我們是用UTF-16來讀取,肯定會出問題。

考慮下面的例子:

public void fileOperationWrong(String inputFile,String outputFile) throws IOException { BufferedReader reader = new BufferedReader(new FileReader(inputFile)); PrintWriter writer = new PrintWriter(new FileWriter(outputFile)); int line = 0; while (reader.ready()) { line++; writer.println(line + &34; + reader.readLine()); } reader.close(); writer.close(); }

我們希望讀取源文件,然後插入行號到新的文件中,但是我們並沒有考慮到編碼的問題,所以可能會失敗。

上面的代碼我們可以修改成這樣:

BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(inputFile), Charset.forName(&34;)));PrintWriter writer = new PrintWriter(new OutputStreamWriter(new FileOutputStream(outputFile), Charset.forName(&34;)));

通過強制指定編碼格式,從而保證了操作的正確性。

不要將非字符數據編碼為字符串

我們經常會有這樣的需求,就是將二進位數據編碼成為字符串String,然後存儲在資料庫中。

二進位是以Byte來表示的,但是從我們上面的介紹可以得知不是所有的Byte都可以表示成為字符。如果將不能表示為字符的Byte進行字符的轉化,就有可能出現問題。

看下面的例子:

public void convertBigIntegerWrong(){ BigInteger x = new BigInteger(&34;); System.out.println(x); byte[] byteArray = x.toByteArray(); String s = new String(byteArray); byteArray = s.getBytes(); x = new BigInteger(byteArray); System.out.println(x); }

上面的例子中,我們將BigInteger轉換為byte數字(大端序列),然後再將byte數字轉換成為String。最後再將String轉換成為BigInteger。

先看下結果:

123456789101180908592843917379

發現沒有轉換成功。

雖然String可以接收第二個參數,傳入字符編碼,目前java支持的字符編碼是:ASCII,ISO-8859-1,UTF-8,UTF-8BE, UTF-8LE,UTF-16,這幾種。默認情況下String也是大端序列的。

上面的例子怎麼修改呢?

public void convertBigIntegerRight(){ BigInteger x = new BigInteger(&34;); String s = x.toString(); //轉換成為可以存儲的字符串 byte[] byteArray = s.getBytes(); String ns = new String(byteArray); x = new BigInteger(ns); System.out.println(x); }

我們可以先將BigInteger用toString方法轉換成為可以表示的字符串,然後再進行轉換即可。

我們還可以使用Base64來對Byte數組進行編碼,從而不丟失任何字符,如下所示:

public void convertBigIntegerWithBase64(){ BigInteger x = new BigInteger(&34;); byte[] byteArray = x.toByteArray(); String s = Base64.getEncoder().encodeToString(byteArray); byteArray = Base64.getDecoder().decode(s); x = new BigInteger(byteArray); System.out.println(x); }

本文的代碼:

learn-java-base-9-to-20/tree/master/security

本文已收錄於 http://www.flydean.com/java-security-code-line-string/

最通俗的解讀,最深刻的乾貨,最簡潔的教程,眾多你不知道的小技巧等你來發現!

歡迎關注我的公眾號:「程序那些事」,懂技術,更懂你!

相關焦點

  • java安全編碼指南之:輸入校驗
    簡介為了保證java程序的安全,任何外部用戶的輸入我們都認為是可能有惡意攻擊意圖,我們需要對所有的用戶輸入都進行一定程度的校驗。在字符串標準化之後進行校驗通常我們在進行字符串校驗的時候需要對一些特殊字符進行過濾,過濾之後再進行字符串的校驗。我們知道在java中字符是基於Unicode進行編碼的。
  • java安全編碼指南之:Denial of Service
    簡介DOS不是那個windows的前身,而是Denial of Service,有做過系統安全方面的小夥伴可能對這個再熟悉不過了,簡單點講,DOS就是服務型響應不過來,從而拒絕了正常的服務請求。今天本文不是要講怎麼發起一個DOS攻擊,而是講一下怎麼在java的代碼層面儘量減少DOS的可能性。
  • Java安全編碼之SQL注入
    Java安全編碼規範早已成為SDL中不可或缺的一部分。本文以Java項目廣泛採用的兩個框架Hibernate和MyBatis 為例來介紹,如何在編碼過程中避免SQL注入的幾種編碼方法,包括對預編譯的深度解析,以及對預編譯理解的幾個「誤區」進行了解釋。
  • java安全編碼指南之:基礎篇
    所以,安全很重要,今天本文將會探討一下java中的安全編碼指南。java平臺本身的安全性作為一個強類型語言,java平臺本身已經儘可能的考慮到了安全性的,為我們屏蔽了大多數安全性的細節。java程序是類型安全的,並且在運行時提供了自動內存管理和數組邊界檢查,Java會儘可能的及早發現程序中的問題,從而使Java程序具有很高的抵抗堆棧破壞的能力。儘管Java安全體系結構在許多情況下可以幫助保護用戶和系統免受惡意代碼或行為不當的攻擊,但它無法防禦可信任代碼中發生的錯誤。
  • java安全編碼指南之拒絕Denial of Service
    >簡介DOS不是那個windows的前身,而是Denial of Service,有做過系統安全方面的小夥伴可能對這個再熟悉不過了今天本文不是要講怎麼發起一個DOS攻擊,而是講一下怎麼在java的代碼層面儘量減少DOS的可能性。為什麼會有DOS為什麼會有DOS呢?排除惡意攻擊的情況下,DOS的原因就是資源的使用不當。一般意義上我們所說的資源有CPU周期,內存,磁碟空間,和文件描述符等。
  • java安全編碼指南之:Mutability可變性
    簡介mutable(可變)和immutable(不可變)對象是我們在java程序編寫的過程中經常會使用到的。可變類型對象就是說,對象在創建之後,其內部的數據可能會被修改。所以它的安全性沒有保證。那麼可變性在java的安全編碼中的最佳實踐是怎麼樣的呢? 一起來看看吧。可變對象和不可變對象知道了可變對象和不可變對象的不同之處之後,我們看一下怎麼才能判斷這個對象是可變對象還是不可變對象呢?
  • Java、JS、OC、Flutter的Base64編碼和解碼
    如微信中暱稱特殊字符的處理,在向資料庫中保存編碼,取用時解碼。可以方便的將用戶的任何輸入轉換成只包含特定字符的安全格式某些系統中只能使用ASCII字符,通過Base64可以將非ASCII字符的數據轉換成ASCII字符如http協議當中的key value欄位,必須進行URLEncode 不然出現的等號可能使解析失敗 空格也會使http請求解析出現問題
  • java之轉換文件編碼
    各位小夥伴們大家好,在之前的文章中小編介紹了,java之轉換流,InputStreamReader的簡單介紹和java之轉換流,OutputStreamWriter的簡單介紹,這次小編要介紹的是文件編碼的轉換,集體如下:將Unicode編碼的文件,轉換為utf-8編碼的文件。
  • java安全編碼指南之:表達式規則
    簡介在java編寫過程中,我們會使用到各種各樣的表達式,在使用表達式的過程中,有哪些安全問題需要我們注意的呢?一起來看看吧。在回答這個問題之前,我們看一下字符串的比較: String stringA=&34;; String stringB=&34;; System.out.println(stringA==stringB);這個我們大家應該都知道,因為String有一個字符串常量池
  • 什麼是base64編碼?編碼的原理是什麼?
    java中是怎樣實現base64編碼的測試結果為:V29ybGQ=base64 是怎麼編碼的?第一步: World 中的每個字母根據上面的ASCII碼,轉換成二進位根據下面的ASCII圖 ,得到下面的對應關係;第二步:把 第一步生成的二進位 重新編碼 每六個一組,但是所有的字母生成的二進位,必須是6的公倍數,所以需要在補 00000000
  • Java安全編碼實踐總結
    Java作為企業主流開發語言已流行多年,各種java安全編碼規範也層出不窮,本文將從實踐角度出發,整合工作中遇到過的多種常見安全漏洞,給出不同場景下的安全編碼方式。安全編碼實踐Sql注入防範常見安全編碼方法:預編譯+輸入驗證
  • 一文讓你讀懂JAVA.IO、字符編碼、
    , StandardCharsets.UTF_8);4 字符集和字符編碼的概念區分字符集和字符編碼的關係,字符集是規範,字符編碼是規範的具體實現;字符集規定了符號和二進位代碼值的唯一對應關係,但是沒有指定具體的存儲方式;
  • Java、JS、OC、Flutter的Base64編碼和解碼
    1 Base64編碼的應用場景分析如微信中暱稱特殊字符的處理,在向資料庫中保存編碼,取用時解碼。可以方便地將用戶的任何輸入轉換成只包含特定字符的安全格式某些系統中只能使用ASCII字符,通過Base64可以將非ASCII字符的數據轉換成ASCII字符如http協議當中的key value欄位,必須進行URLEncode 不然出現的等號可能使解析失敗
  • 一文解開java中字符串編碼的小秘密
    簡介在本文中你將了解到Unicode和UTF-8,UTF-16,UTF-32的關係,同時你還會了解變種UTF-8,並且探討一下UTF-8和變種UTF-8在java中的應用。一起來看看吧。於是國際組織出手了,制定了UNICODE字符集,為所有語言的所有字符都定義了一個唯一的編碼,unicode的字符集是從U+0000到U+10FFFF這麼多個編碼。那麼unicode和UTF-8,UTF-16,UTF-32有什麼關係呢?
  • 師弟不講武德,這編碼算法也講得忒溜了……
    2.Java中使用URL編碼算法Java標準庫提供了一個URLEncoder類來對任意字符串進行URL編碼import java.io.UnsupportedEncodingException要特別注意:URL編碼是`編碼算法`,`不是加密算法`。URL編碼的目的是`把任意文本數據編碼為%前綴表示的文本`,編碼後的文本僅包含`A~Z,a~z,0~9,-,_,.,和%,便於瀏覽器和伺服器處理。
  • 基礎篇:一文讓你讀懂JAVA.IO、字符編碼、URL和Spring.Resource
    , StandardCharsets.UTF_8);4 字符集和字符編碼的概念區分字符集和字符編碼的關係,字符集是規範,字符編碼是規範的具體實現;字符集規定了符號和二進位代碼值的唯一對應關係,但是沒有指定具體的存儲方式;
  • java安全編碼指南之:對象構建
    簡介程式設計師肯定是不缺對象的,因為隨時都可以構建一個,對象多了肯定會出現點安全問題,一起來看看在java的對象構建中怎麼保證對象的安全性吧。<init>(SensitiveOperation.java:11) at com.flydean.SensitiveUsage.main(SensitiveUsage.java:10)那麼問題來了,上面的這個class是不是安全的呢?
  • 如何在Linux中將文件編碼轉換為UTF-8
    你可能已經知道,計算機除了二進位數據,是不會理解和存儲字符、數字或者任何人類能夠理解的東西的。一個二進位位只有兩種可能的值,也就是 0 或 1,真或假,是或否。其它的任何事物,比如字符、數據和圖片,必須要以二進位的形式來表現,以供計算機處理。
  • java安全編碼指南之:聲明和初始化
    簡介在java對象和欄位的初始化過程中會遇到哪些安全性問題呢?一起來看看吧。這樣循環引用雖然不會報錯,但是根據class的初始化順序不同,會導致a和b生成兩種不同的結果。所以在我們編寫代碼的過程中,一定要避免這種循環初始化的情況。
  • java安全編碼指南之:可見性和原子性
    簡介java類中會定義很多變量,有類變量也有實例變量,這些變量在訪問的過程中,會遇到一些可見性和原子性的問題。這裡我們來詳細了解一下怎麼避免這些問題。最簡單的解決可見性的辦法就是加上volatile關鍵字,volatile關鍵字可以使用java內存模型的happens-before規則,從而保證volatile的變量修改對所有線程可見。