转义字符:Spring 的优秀工具类盘点 第 2 部分: 特殊字符转义和思路方法入参检测工具类

  Spring 不但提供了个功能全面应用开发框架本身还拥有众多可以在编写时直接使用工具类您不但可以在 Spring 应用中使用这些工具类也可以在其它应用中使用这些工具类中大部分是可以在脱离 Spring 框架时使用了解 Spring 中有哪些好用工具类并在编写时适当使用将有助于提高开发效率、增强代码质量

  在这个分为两部分文章中我们将从众多 Spring 工具类中遴选出那些好用工具类介绍给大家第 1 部分 介绍了和文件资源操作和 Web 相关工具类在第 2 部分中将介绍特殊转义和思路方法入参检测工具类

  特殊转义

  由于 Web 应用需要联合使用到多种语言每种语言都包含些特殊对于动态语言或标签式语言而言如果需要动态构造语言内容时个我们经常会碰到问题就是特殊转义问题下面是 Web 开发者最常面对需要转义特殊类型:

  HTML 特殊

  JavaScript 特殊

  SQL 特殊

  如果不对这些特殊进行转义处理则不但可能破坏文档结构还可以引发潜在安全问题Spring 为 HTML 和 JavaScript 特殊提供了转义操作工具类它们分别是 HtmlUtils 和 JavaScriptUtils

  HTML 特殊转义

  HTML 中 <>& 等有特殊含义它们是 HTML 语言保留字因此不能直接使用使用这些个应使用它们转义序列:

  &:&amp;

  " :&quot;

  < :&lt;

  > :&gt;

  由于 HTML 网页本身就是个文本型结构化文档如果直接将这些包含了 HTML 特殊内容输出到网页中极有可能破坏整个 HTML 文档结构所以般情况下需要对动态数据进行转义处理使用转义序列表示 HTML 特殊下面 JSP 网页将些变量动态输出到 HTML 网页中:

  清单 1. 未进行 HTML 特殊转义处理网页

<%@ page language="java" contentType="text/html; char=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 具体使用方法:

  清单 2. HtmpEscapeExample

package com.baobaotao.escape;
import org.springframework.web.util.HtmlUtils;
public HtmpEscapeExample {
  public void (String args) {
    String specialStr = "<div id="testDiv">test1;test2</div>";
    String str1 = HtmlUtils.htmlEscape(specialStr); ①转换为HTML转义表示
    .out.prln(str1);
   
    String str2 = HtmlUtils.htmlEscapeDecimal(specialStr); ②转换为数据转义表示
    .out.prln(str2);
   
    String str3 = HtmlUtils.htmlEscapeHex(specialStr); ③转换为十 6进制数据转义表示
    .out.prln(str3);
   
    ④下面对转义后串进行反向操作
    .out.prln(HtmlUtils.htmlUnescape(str1));
    .out.prln(HtmlUtils.htmlUnescape(str2));
    .out.prln(HtmlUtils.htmlUnescape(str3));
  }
}


  HTML 不但可以使用通用转义序列表示 HTML 特殊还可以使用以 # 为前缀数字序列表示 HTML 特殊它们在最终显示效果上是HtmlUtils 提供了 3个转义思路方法:

思路方法 介绍说明
String htmlEscape(String input) 将 HTML 特殊转义为 HTML 通用转义序列;
String htmlEscapeDecimal(String input) 将 HTML 特殊转义为带 # 十进制数据转义序列;
String htmlEscapeHex(String input) 将 HTML 特殊转义为带 # 十 6进制数据转义序列;

  此外HtmlUtils 还提供了个能够将经过转义内容还原思路方法:htmlUnescape(String input)它可以还原以上 3种转义序列内容运行以上代码您将可以看到以下输出:

str1:&lt;div id=&quot;testDiv&quot;&gt;test1;test2&lt;/div&gt;
str2:&#60;div id=&#34;testDiv&#34;&#62;test1;test2&#60;/div&#62;
str3:&#x3c;div id=&#x22;testDiv&#x22;&#x3e;test1;test2&#x3c;/div&#x3e;
<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 结构将会遭受破坏甚至被嵌入些恶意下面列出了需要转义特殊 JavaScript :

  ' :'

  " :"

  :

  走纸换页: f

  换行:n

  换栏符:t

  回车:r

  回退符:b

     我们通过个具体例子演示动态变量是如何对 JavaScript 进行破坏假设我们有个 JavaScript 变量其元素值通过个 Java List 对象提供下面是完成这操作 JSP 代码片断:

  清单 3. jsTest.jsp:未对 JavaScript 特殊进行处理

<%@ page language="java" contentType="text/html; char=utf-8"%>
<jsp:directive.page import="java.util.*"/>
<%
 List textList = ArrayList;
 textList.add("";alert;j="");
%>
<script>
 var txtList = Array;
  <% for ( i = 0 ; i < textList.size ; i) { %>
   txtList[<%=i%>] = "<%=textList.get(i)%>";
   ① 未对可能包含特殊 JavaScript 变量进行处理
  <% } %>
</script>


  当客户端这个 JSP 页面后将得到以下 HTML 输出页面:

<script>
 var txtList = 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; char=utf-8"%>
<jsp:directive.page import="java.util.*"/>
<jsp:directive.page import="org.springframework.web.util.JavaScriptUtils"/>
<%
 List textList = ArrayList;
 textList.add("";alert;j="");
%>
<script>
  var txtList = Array;
  <% for ( i = 0 ; i < textList.size ; i) { %>
  ① 在输出动态内容前事先进行转义处理
  txtList[<%=i%>] = "<%=JavaScriptUtils.javaScriptEscape(""+textList.get(i))%>";
  <% } %>
</script>


  通过转义处理后这个 JSP 页面输出结果网页 JavaScript 代码就不会产生问题了:

<script>
  var txtList = Array;
  txtList[0] = "";alert;j="";
  ① 粗体部分仅是个普通而非段 JavaScript 语句了
</script>


  SQL特殊转义

  应该说您即使没有处理 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 完成这工作:

  清单 4. SqlEscapeExample

package com.baobaotao.escape;
import org.apache.commons.lang.StringEscapeUtils;
public SqlEscapeExample {
  public void (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 + "'";
    .out.prln(sql);
  }
}


  事实上StringEscapeUtils 不但提供了 SQL 特殊转义处理功能还提供了 HTML、XML、JavaScript、Java 特殊转义和还原思路方法如果您不介意引入 jakarta commons lang 类包我们更推荐您使用 StringEscapeUtils 工具类完成特殊转义处理工作

  思路方法入参检测工具类

  Web 应用在接受表单提交数据后都需要对其进行合法性检查如果表单数据不合法请求将被驳回类似当我们在编写类思路方法时也常常需要对思路方法入参进行合法性检查如果入参不符合要求思路方法将通过抛出异常方式拒绝后续处理个例子:有个根据文件名获取输入流思路方法:InputStream getData(String file)为了使思路方法能够成功执行必须保证 file 入参不能为 null 或空白否则根本无须进行后继处理这时思路方法编写者通常会在思路方法体最前面编写段对入参进行检测代码如下所示:

public InputStream getData(String file) {
   (file null || file.length 0|| file.replaceAll("s", "").length 0) {
    throw 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 .gif' />, String message) / notEmpty(Object .gif' />, 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 工具类只要将动态内容在拼接的前使用工具类进行转义处理就可以避免类似问题发生了如果您不介意引入个第 3方类包那么 jakarta commons lang 通用类包中 StringEscapeUtils 工具类可能更加适合它提供了更加全面转义功能

  最后我们还介绍了 Spring Assert 工具类Assert 工具类是通用性很强工具类它使用面向对象方式解决思路方法入参检测问题您可以在自己应用中使用 Assert 对思路方法入参进行检查

Tags:  xml转义 java转义字符 转义符 转义字符

延伸阅读

最新评论

发表评论