因爲 Web 應用程序須要聯合使用到多種語言,每種語言都包含一些特殊的字符,對於動態語言或標籤式的語言而言,若是須要動態構造語言的內容時,一個咱們常常會碰到的問題就是特殊字符轉義的問題。下面是 Web 開發者最常面對須要轉義的特殊字符類型:html
HTML 特殊字符;java
JavaScript 特殊字符;web
SQL 特殊字符;spring
若是不對這些特殊字符進行轉義處理,則不但可能破壞文檔結構,還能夠引起潛在的安全問題。Spring 爲 HTML 和 JavaScript 特殊字符提供了轉義操做工具類,它們分別是 HtmlUtils 和 JavaScriptUtils。sql
HTML 中 <,>,& 等字符有特殊含義,它們是 HTML 語言的保留字,所以不能直接使用。使用這些個字符時,應使用它們的轉義序列:express
&:&apache
" :"數組
< :<安全
> :>網絡
因爲 HTML 網頁自己就是一個文本型結構化文檔,若是直接將這些包含了 HTML 特殊字符的內容輸出到網頁中,極有可能破壞整個 HTML 文檔的結構。因此,通常狀況下須要對動態數據進行轉義處理,使用轉義序列表示 HTML 特殊字符。下面的 JSP 網頁將一些變量動態輸出到 HTML 網頁中:
<%@ page language="java" contentType="text/html; charset=utf-8"%> <%! String userName = "</td><tr></table>"; String address = " \" type=\"button"; %> <table border="1"> <tr> <td>姓名:</td><td><%=userName%></td> ① </tr> <tr> <td>年齡:</td><td>28</td> </tr> </table> <input value="<%=address%>" type="text" /> ②
在 ① 和 ② 處,咱們未經任何轉義處理就直接將變量輸出到 HTML 網頁中,因爲這些變量可能包含一些特殊的 HTML 的字符,它們將可能破壞整個 HTML 文檔的結構。咱們能夠從以上 JSP 頁面的一個具體輸出中瞭解這一問題:
<table border="1"> <tr> <td>姓名:</td><td></td><tr></table></td> ① 破壞了 <table> 的結構 </tr> <tr> <td>年齡:</td><td>28</td> </tr> </table> <input value=" " type="button" type="text" /> ② 將原本是輸入框組件偷樑換柱爲按鈕組件
融合動態數據後的 HTML 網頁已經面目全非,首先 ① 處的 <table> 結構被包含 HTML 特殊字符的 userName 變量截斷了,形成其後的 <table> 代碼變成無效的內容;其次,② 處 <input> 被動態數據改換爲按鈕類型的組件(type="button")。爲了不這一問題,咱們須要事先對可能破壞 HTML 文檔結構的動態數據進行轉義處理。Spring 爲咱們提供了一個簡單適用的 HTML 特殊字符轉義工具類,它就是 HtmlUtils。下面,咱們經過一個簡單的例子瞭解 HtmlUtils 的具體用法:
package com.baobaotao.escape; import org.springframework.web.util.HtmlUtils; public class HtmpEscapeExample { public static void main(String[] args) { String specialStr = "<div id=\"testDiv\">test1;test2</div>"; String str1 = HtmlUtils.htmlEscape(specialStr); ①轉換爲HTML轉義字符表示 System.out.println(str1); String str2 = HtmlUtils.htmlEscapeDecimal(specialStr); ②轉換爲數據轉義表示 System.out.println(str2); String str3 = HtmlUtils.htmlEscapeHex(specialStr); ③轉換爲十六進制數據轉義表示 System.out.println(str3); ④下面對轉義後字符串進行反向操做 System.out.println(HtmlUtils.htmlUnescape(str1)); System.out.println(HtmlUtils.htmlUnescape(str2)); System.out.println(HtmlUtils.htmlUnescape(str3)); } }
HTML 不但可使用通用的轉義序列表示 HTML 特殊字符,還可使用以 # 爲前綴的數字序列表示 HTML 特殊字符,它們在最終的顯示效果上是同樣的。HtmlUtils 提供了三個轉義方法:
方法 | 說明 |
---|---|
static String htmlEscape(String input) |
將 HTML 特殊字符轉義爲 HTML 通用轉義序列; |
static String htmlEscapeDecimal(String input) |
將 HTML 特殊字符轉義爲帶 # 的十進制數據轉義序列; |
static String htmlEscapeHex(String input) |
將 HTML 特殊字符轉義爲帶 # 的十六進制數據轉義序列; |
此外,HtmlUtils 還提供了一個可以將通過轉義內容還原的方法:htmlUnescape(String input),它能夠還原以上三種轉義序列的內容。運行以上代碼,您將能夠看到如下的輸出:
str1:<div id="testDiv">test1;test2</div> str2:<div id="testDiv">test1;test2</div> str3:<div id="testDiv">test1;test2</div> <div id="testDiv">test1;test2</div> <div id="testDiv">test1;test2</div> <div id="testDiv">test1;test2</div>
您只要使用 HtmlUtils 對代碼 清單 1 的 userName 和 address 進行轉義處理,最終輸出的 HTML 頁面就不會遭受破壞了。
JavaScript 中也有一些須要特殊處理的字符,若是直接將它們嵌入 JavaScript 代碼中,JavaScript 程序結構將會遭受破壞,甚至被嵌入一些惡意的程序。下面列出了須要轉義的特殊 JavaScript 字符:
' :\'
" :\"
\ :\\
走紙換頁: \f
換行:\n
換欄符:\t
回車:\r
回退符:\b
咱們經過一個具體例子演示動態變量是如何對 JavaScript 程序進行破壞的。假設咱們有一個 JavaScript 數組變量,其元素值經過一個 Java List 對象提供,下面是完成這一操做的 JSP 代碼片段:
<%@ page language="java" contentType="text/html; charset=utf-8"%> <jsp:directive.page import="java.util.*"/> <% List textList = new ArrayList(); textList.add("\";alert();j=\""); %> <script> var txtList = new Array(); <% for ( int i = 0 ; i < textList.size() ; i++) { %> txtList[<%=i%>] = "<%=textList.get(i)%>"; ① 未對可能包含特殊 JavaScript 字符的變量進行處理 <% } %> </script>
當客戶端調用這個 JSP 頁面後,將獲得如下的 HTML 輸出頁面:
<script> var txtList = new Array(); txtList[0] = "";alert();j=""; ① 原本是但願接受一個字符串,結果被植入了一段JavaScript代碼 </script>
因爲包含 JavaScript 特殊字符的 Java 變量直接合併到 JavaScript 代碼中,咱們原本指望 ① 處所示部分是一個普通的字符串,但結果變成了一段 JavaScript 代碼,網頁將彈出一個 alert 窗口。想像一下若是粗體部分的字符串是「";while(true)alert();j="」時會產生什麼後果呢?
所以,若是網頁中的 JavaScript 代碼須要經過拼接 Java 變量動態產生時,通常須要對變量的內容進行轉義處理,能夠經過 Spring 的 JavaScriptUtils 完成這件工做。下面,咱們使用 JavaScriptUtils 對以上代碼進行改造:
<%@ page language="java" contentType="text/html; charset=utf-8"%> <jsp:directive.page import="java.util.*"/> <jsp:directive.page import="org.springframework.web.util.JavaScriptUtils"/> <% List textList = new ArrayList(); textList.add("\";alert();j=\""); %> <script> var txtList = new Array(); <% for ( int i = 0 ; i < textList.size() ; i++) { %> ① 在輸出動態內容前事先進行轉義處理 txtList[<%=i%>] = "<%=JavaScriptUtils.javaScriptEscape(""+textList.get(i))%>"; <% } %> </script>
經過轉義處理後,這個 JSP 頁面輸出的結果網頁的 JavaScript 代碼就不會產生問題了:
<script> var txtList = new Array(); txtList[0] = "\";alert();j=\""; ① 粗體部分僅是一個普通的字符串,而非一段 JavaScript 的語句了 </script>
應該說,您即便沒有處理 HTML 或 JavaScript 的特殊字符,也不會帶來災難性的後果,可是若是不在動態構造 SQL 語句時對變量中特殊字符進行處理,將可能致使程序漏洞、數據盜取、數據破壞等嚴重的安全問題。網絡中有大量講解 SQL 注入的文章,感興趣的讀者能夠搜索相關的資料深刻研究。
雖然 SQL 注入的後果很嚴重,可是隻要對動態構造的 SQL 語句的變量進行特殊字符轉義處理,就能夠避免這一問題的發生了。來看一個存在安全漏洞的經典例子:
SELECT COUNT(userId) FROM t_user WHERE userName='"+userName+"' AND password ='"+password+"';
以上 SQL 語句根據返回的結果數判斷用戶提供的登陸信息是否正確,若是 userName 變量不通過特殊字符轉義處理就直接合併到 SQL 語句中,黑客就能夠經過將 userName 設置爲 「1' or '1'='1」繞過用戶名/密碼的檢查直接進入系統了。
因此除非必要,通常建議經過 PreparedStatement 參數綁定的方式構造動態 SQL 語句,由於這種方式能夠避免 SQL 注入的潛在安全問題。可是每每很難在應用中徹底避免經過拼接字符串構造動態 SQL 語句的方式。爲了防止他人使用特殊 SQL 字符破壞 SQL 的語句結構或植入惡意操做,必須在變量拼接到 SQL 語句以前對其中的特殊字符進行轉義處理。Spring 並無提供相應的工具類,您能夠經過 jakarta commons lang 通用類包中(spring/lib/jakarta-commons/commons-lang.jar)的 StringEscapeUtils 完成這一工做:
package com.baobaotao.escape; import org.apache.commons.lang.StringEscapeUtils; public class SqlEscapeExample { public static void main(String[] args) { String userName = "1' or '1'='1"; String password = "123456"; userName = StringEscapeUtils.escapeSql(userName); password = StringEscapeUtils.escapeSql(password); String sql = "SELECT COUNT(userId) FROM t_user WHERE userName='" + userName + "' AND password ='" + password + "'"; System.out.println(sql); } }
事實上,StringEscapeUtils 不但提供了 SQL 特殊字符轉義處理的功能,還提供了 HTML、XML、JavaScript、Java 特殊字符的轉義和還原的方法。若是您不介意引入 jakarta commons lang 類包,咱們更推薦您使用 StringEscapeUtils 工具類完成特殊字符轉義處理的工做。
Web 應用在接受表單提交的數據後都須要對其進行合法性檢查,若是表單數據不合法,請求將被駁回。相似的,當咱們在編寫類的方法時,也經常須要對方法入參進行合法性檢查,若是入參不符合要求,方法將經過拋出異常的方式拒絕後續處理。舉一個例子:有一個根據文件名獲取輸入流的方法:InputStream getData(String file),爲了使方法可以成功執行,必須保證 file 入參不能爲 null 或空白字符,不然根本無須進行後繼的處理。這時方法的編寫者一般會在方法體的最前面編寫一段對入參進行檢測的代碼,以下所示:
public InputStream getData(String file) { if (file == null || file.length() == 0|| file.replaceAll("\\s", "").length() == 0) { throw new IllegalArgumentException("file入參不是有效的文件地址"); } … }
相似以上檢測方法入參的代碼是很是常見,可是在每一個方法中都使用手工編寫檢測邏輯的方式並非一個好主意。閱讀 Spring 源碼,您會發現 Spring 採用一個 org.springframework.util.Assert 通用類完成這一任務。
Assert 翻譯爲中文爲「斷言」,使用過 JUnit 的讀者都熟知這個概念,它判定某一個實際的運行值和預期想同樣,不然就拋出異常。Spring 對方法入參的檢測借用了這個概念,其提供的 Assert 類擁有衆多按規則對方法入參進行斷言的方法,能夠知足大部分方法入參檢測的要求。這些斷言方法在入參不知足要求時就會拋出 IllegalArgumentException。下面,咱們來認識一下 Assert 類中的經常使用斷言方法:
斷言方法 | 說明 |
---|---|
notNull(Object object) |
當 object 不爲 null 時拋出異常,notNull(Object object, String message) 方法容許您經過 message 定製異常信息。和 notNull() 方法斷言規則相反的方法是 isNull(Object object)/isNull(Object object, String message),它要求入參必定是 null; |
isTrue(boolean expression) / isTrue(boolean expression, String message) |
當 expression 不爲 true 拋出異常; |
notEmpty(Collection collection) / notEmpty(Collection collection, String message) |
當集合未包含元素時拋出異常。notEmpty(Map map) / notEmpty(Map map, String message) 和 notEmpty(Object[] array, String message) / notEmpty(Object[] array, String message) 分別對 Map 和 Object[] 類型的入參進行判斷; |
hasLength(String text) / hasLength(String text, String message) |
當 text 爲 null 或長度爲 0 時拋出異常; |
hasText(String text) / hasText(String text, String message) |
text 不能爲 null 且必須至少包含一個非空格的字符,不然拋出異常; |
isInstanceOf(Class clazz, Object obj) / isInstanceOf(Class type, Object obj, String message) |
若是 obj 不能被正確造型爲 clazz 指定的類將拋出異常; |
isAssignable(Class superType, Class subType) / isAssignable(Class superType, Class subType, String message) |
subType 必須能夠按類型匹配於 superType,不然將拋出異常; |
使用 Assert 斷言類能夠簡化方法入參檢測的代碼,如 InputStream getData(String file) 在應用 Assert 斷言類後,其代碼能夠簡化爲如下的形式:
public InputStream getData(String file){ Assert.hasText(file,"file入參不是有效的文件地址"); ① 使用 Spring 斷言類進行方法入參檢測 … }
可見使用 Spring 的 Assert 替代自編碼實現的入參檢測邏輯後,方法的簡潔性獲得了很多的提升。Assert 不依賴於 Spring 容器,您能夠大膽地在本身的應用中使用這個工具類。
本文介紹了一些經常使用的 Spring 工具類,其中大部分 Spring 工具類不但能夠在基於 Spring 的應用中使用,還能夠在其它的應用中使用。
對於 Web 應用來講,因爲有不少關聯的腳本代碼,若是這些代碼經過拼接字符串的方式動態產生,就須要對動態內容中特殊的字符進行轉義處理,不然就有可能產生意想不到的後果。Spring 爲此提供了 HtmlUtils 和 JavaScriptUtils 工具類,只要將動態內容在拼接以前使用工具類進行轉義處理,就能夠避免相似問題的發生了。若是您不介意引入一個第三方類包,那麼 jakarta commons lang 通用類包中的 StringEscapeUtils 工具類可能更加適合,由於它提供了更加全面的轉義功能。
最後咱們還介紹了 Spring 的 Assert 工具類,Assert 工具類是通用性很強的工具類,它使用面向對象的方式解決方法入參檢測的問題,您能夠在本身的應用中使用 Assert 對方法入參進行檢查。