Spring 框架內建大量生產級工具類,脫離容器即可獨立使用。本文聚焦 Web 安全核心領域——特殊字符轉義與方法契約防禦,深度解析 HtmlUtils、JavaScriptUtils、StringEscapeUtils、Assert 四大工具類的實戰應用。
HtmlUtils動態數據直接輸出到 HTML 會破壞文檔結構,甚至引發 XSS。
// 危險示例
String userName = ""; // 截斷表格結構
String address = "\" type=\"button"; // 劫持 input 類型
// HtmlUtils 三種轉義模式
String special = "test1;test2";
// 1. 標準實體引用(推薦)
HtmlUtils.htmlEscape(special);
// <div id="testDiv">test1;test2</div>
// 2. 十進制數字實體
HtmlUtils.htmlEscapeDecimal(special);
// <div id="testDiv">test1;test2</div>
// 3. 十六進制數字實體
HtmlUtils.htmlEscapeHex(special);
// <div id="testDiv">test1;test2</div>
// 反轉義(三種模式通用)
HtmlUtils.htmlUnescape(escaped); // 還原原始字符串
JavaScriptUtils動態拼接 JS 代碼時,未轉義字符串會被注入為可執行腳本。
// JSP 片段
List<String> list = Arrays.asList("\";alert();j=\""); // 攜帶 JS 代碼
// 危險:直接拼接
txtList[0] = "<%=list.get(0)%>";
// 輸出:txtList[0] = "";alert();j=""; → 彈窗執行
// 安全:JavaScriptUtils 轉義
txtList[0] = "<%=JavaScriptUtils.javaScriptEscape(list.get(0))%>";
// 輸出:txtList[0] = "\";alert();j=\""; → 純字符串字面量
轉義規則:' → \'、" → \"、\ → \\、\f \n \t \r \b 對應控制字符轉義。
StringEscapeUtils (Apache Commons Lang)Spring 未提供 SQL 轉義,推薦引入 commons-lang3 使用 StringEscapeUtils,功能更全面。
String userName = "1' or '1'='1"; // 經典注入 Payload
String password = "123456";
userName = StringEscapeUtils.escapeSql(userName); // 1\' or \'1\'=\'1
password = StringEscapeUtils.escapeSql(password);
String sql = "SELECT * FROM t_user WHERE user='" + userName + "' AND pwd='" + password + "'";
// 生成安全 SQL,單引號被轉義,注入失效
最佳實踐:優先使用 PreparedStatement 參數綁定,從根源消除拼接風險;僅在無法使用參數化查詢的舊代碼/動態 DDL 場景下用轉義兜底。
手寫 if (arg == null) throw ... 碎片化、易漏、可讀性差。Spring org.springframework.util.Assert 提供流式、語義化的契約聲明。
| 方法 | 契約語義 | 異常類型 |
|---|---|---|
notNull(obj, msg) | 參數必須非 null | IllegalArgumentException |
isNull(obj, msg) | 參數必須為 null | |
isTrue(expr, msg) | 布爾表達式必須為 true | |
notEmpty(coll, msg) | 集合/Map/數組必須含元素 | |
hasLength(str, msg) | 字符串非 null 且 length > 0 | |
hasText(str, msg) | 字符串非 null 且含非空白字符 (推薦) | |
isInstanceOf(clazz, obj, msg) | 對象必須可轉型為指定類 | |
isAssignable(super, sub, msg) | sub 必須是 super 的子類/實現類 | |
notNull(obj) / hasText(str) | 無消息版,默認異常信息 |
// 手寫版:冗長、易漏邊界
public InputStream getData(String file) {
if (file == null || file.length() == 0 || file.trim().isEmpty()) {
throw new IllegalArgumentException("file 參數無效");
}
// ...
}
// Assert 版:一行聲明契約,語義精確
public InputStream getData(String file) {
Assert.hasText(file, "file 參數必須為非空白字符串");
// ...
}
// 檢查對象內部狀態
public void withdraw(Account account, BigDecimal amount) {
Assert.state(account.isActive(), "賬戶已凍結");
Assert.state(account.getBalance().compareTo(amount) >= 0, "餘額不足");
// ...
}
Assert.state() 拋出 IllegalStateException,語義區分「調用者傳參錯誤」與「對象狀態不滿足前置條件」。
需要轉義?
├─ HTML 輸出上下文 → HtmlUtils.htmlEscape()
├─ JavaScript 字面量上下文 → JavaScriptUtils.javaScriptEscape()
├─ SQL 拼接上下文(無法用 PreparedStatement) → StringEscapeUtils.escapeSql()
└─ 統一入口、全覆蓋、可接受第三方依賴 → StringEscapeUtils (commons-lang3) ✅ 推薦
方法入參驗證?
├─ Spring 專案 / 可引入 spring-core → Assert ✅ 推薦
├─ 非 Spring、追求零依賴 → Objects.requireNonNull / 自定義微型斷言庫
└─ Bean Validation (JSR-380) → @NotNull @Size 等註解(聲明式,運行時需 Provider)
HtmlUtils/JavaScriptUtils 解決輸出編碼問題,StringEscapeUtils 覆蓋全上下文轉義,Assert 落地契約式防禦。這四個工具類體積小、零依賴(除 Commons Lang)、API 穩定,是構建安全、健壯 Java 應用的基石組件。建議將其納入團隊編碼規約「必用清單」。