【导读】Java安全编码标准—第2章 输入验证和数据净化

章节概述

输入验证是防止各种注入攻击,SQL 注入,XML 注入,LDAP 注入,以及 XSS 跨站攻击的有效手段。

LDAP(Lightweight Directory Access Protocol):轻量级目录访问协议,是一种在线目录访问协议,主要用于目录中资源的搜索和查询,是X.500的一种简便的实现。随着互联网的广泛使用,web应用的数量呈爆炸式的增长,而这些应用的资源和数据呈分布式存储于目录中。通常不同的应用会有专属于自己相关数据的目录,即专有目录,专有目录数量的增长导致了信息孤岛(一种不能与其他相关信息系统之间进行互操作或者说协调工作的信息系统)的出现,系统和资源的共享及管理变得日益困难。

以查找联系人和加密证书为例,太多的目录明显会给计算机搜索带来巨大的压力,当然随之出现了相应的解决方案,如X.500,X.500申明了目录客户端和目录服务器使用的目录访问协议(DAP),然而作为应用层协议,DAP要求完整的7层OSI协议栈操作,会要求比小环境(配置、资源有限的环境)所能提供的更多的资源,因此需要一种轻量级的协议来代替X.500,LDAP正是因此而生。

LDAP注入攻击和SQL注入攻击相似,因此接下来的想法是利用用户引入的参数生成LDAP查询。一个安全的Web应用在构造和将查询发送给服务器前应该净化用户传入的参数。在有漏洞的环境中,这些参数没有得到合适的过滤,因而攻击者可以注入任意恶意代码。

XSS:跨站脚本(cross site script)为了避免与样式css混淆,所以简称为XSS

XSS是一种经常出现在web应用中的计算机安全漏洞,也是web中最主流的攻击方式。那么什么是XSS呢?

XSS是指恶意攻击者利用网站没有对用户提交数据进行转义处理或者过滤不足的缺点,进而添加一些代码,嵌入到web页面中去。使别的用户访问都会执行相应的嵌入代码。

从而盗取用户资料、利用用户身份进行某种动作或者对访问者进行病毒侵害的一种攻击方式。

XSS攻击的危害包括:

1、盗取各类用户帐号,如机器登录帐号、用户网银帐号、各类管理员帐号

2、控制企业数据,包括读取、篡改、添加、删除企业敏感数据的能力

3、盗窃企业重要的具有商业价值的资料

4、非法转账

5、强制发送电子邮件

6、网站挂马

7、控制受害者机器向其它网站发起攻击

主要原因:过于信任客户端提交的数据!

解决办法:不信任任何客户端提交的数据,只要是客户端提交的数据就应该先进行相应的过滤处理然后方可进行下一步的操作。

简单例子

正常发送消息:

http://www.test.com/message.php?send=Hello,World!

接收者将会接收信息并显示Hello,Word

非正常发送消息:

http://www.test.com/message.php?send=<script>alert(‘foolish!’)</script>!

接收者接收消息显示的时候将会弹出警告窗口

工具审计 Fortify 和 Findbugs 均可。

Fortify 和 Findbugs 均为静态代码扫描与分析工具。

尽可能先使用自动化分析工具,当一个规则不能由工具审计的时候,或者怀疑工具漏报误报的时候,必须采用人工审计的方式。

整改原则

当存在公用 Java 代码库和解析器的数据净化和验证方法时,应该优先考虑使用它们。

1560252685788.png

表 2. Java 输入验证和数据净化编程标准审计方法

标准 审计复杂度 严重性 整改代价 审计方法
IDS00-J 工具为主,人工为辅
IDS01-J 工具
IDS02-J N/A
IDS03-J 人工
IDS04-J 人工
IDS05-J 人工
IDS06-J 人工
IDS07-J 人工
IDS08,09,11 N/A
IDS012-J 人工
IDSother 中低 N/A

2.1 审计重点 IDS00-J 净化穿越受信边界的非受信数据

程序接受未经过验证的用户数据。问题严重,包括 SQL 注入攻击和 XML 注入攻击。发生可能性大,修复代价中等。

审计方法:

工具检测 SQL 注入

Fortify 可以检测 SQL injection(由 Juliet Test Case SWE89 验证支持)

人工审计 XML 注入有难度

Fortify 和 findbugs 都无法检测 XML injection。

整改方法:

SQL 命令行解析器和 XML 解析器都提供了自己的数据净化和验证的方法。当存在这样的方法的时候,应当优先考虑它们,因为自定义的方法会忽略一些特殊情况,会忽略解析器自身所隐含的复杂性。

  1. SQL 注入相关攻击

    用 PreparedStatement 代替 Statement, 通过使用 PreparedStatement 类的 set*() 方法,可以进行强制类型检查,会自动转义双引号内的输入数据,减少 SQL 注入漏洞。

    设计精巧的注入文本还是有可能规避 PreparedStatement,更进一步的整改方法,可以是根据数据库,代替使用$符号的地方。比如对于 DB2,可以使用'%'||'#param#'||'%'或者 CONCAT('%', #param#, '%') 避免。

    演示SQL注入例子,示例输入:

    userName:yuhuan' or '1=1 password:1(any word)

    防止SQL注入总体思路:

    1、检查变量数据类型和格式

    2、过滤特殊符号

    3、绑定变量,使用预编译语句

  2. XML 注入相关攻击

    1. 一个方法是,写程序时候使用白模板,Pattern.matches, 使用何种 pattern 和业务逻辑相关,比如年龄字段只能是数字且不能大于 150
    2. 另外一个通用方法是,写程序时候使用 Java 类库类 SchemaFactory 以及 setEntityResolver 方法,通过定义 schema 来规范并验证 xml 输入。比如姓名字段只能 (0,1] 代表无名或者一个名字, 而不允许多个名字。语法可参考:http://www.w3school.com.cn/schema/schema_intro.asp

    book.xml:

    <?xml version="1.0" encoding="UTF-8"?>
    <!--<!DOCTYPE bookstore SYSTEM "http://www.w3school.com.cn/dtd/book.dtd">-->
    <bookstore xmlns="http://www.w3school.com.cn"
               xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
               xsi:schemaLocation="http://www.w3school.com.cn book.xsd">
        <book id="1">
            <name>冰与火之歌</name>
            <author>乔治马丁</author>
            <year>2014</year>
            <price>
                89.00
                <!--</price><author>余欢</author><price>0.12-->
            </price>
        </book>
        <book id="2">
            <name>安徒生童话</name>
            <author>安徒生</author>
            <year>2004</year>
            <price>77.50</price>
        </book>
        <book id="3">
            <name>think think think</name>
            <author>aaa</author>
            <year>1997</year>
            <price>100.00</price>
        </book>
    </bookstore>
    

    book.xsd:

    <?xml version="1.0" encoding="UTF-8" ?>
    <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
            targetNamespace="http://www.w3school.com.cn"
            xmlns="http://www.w3school.com.cn"
            elementFormDefault="qualified">
     <xs:element name="bookstore">
         <xs:complexType>
             <xs:sequence>
                 <xs:element name="book" minOccurs="0" maxOccurs="unbounded">
                     <xs:complexType>
                         <xs:sequence>
                             <xs:element name="name" type="xs:string"/>
                             <xs:element name="author" type="xs:string"/>
                             <xs:element name="year">
                                 <xs:simpleType>
                                     <xs:restriction base="xs:integer">
                                         <xs:minInclusive value="0"/>
                                         <xs:maxInclusive value="3000"/>
                                     </xs:restriction>
                                 </xs:simpleType>
                             </xs:element>
                             <xs:element name="price">
                                 <xs:simpleType>
                                     <xs:restriction base="xs:decimal">
                                         <xs:pattern value="[\d]{0,16}\.[\d]{2}"/>
                                     </xs:restriction>
                                 </xs:simpleType>
                             </xs:element>
                         </xs:sequence>
    
                         <xs:attribute type="xs:integer" name="id"/>
    
                     </xs:complexType>
                 </xs:element>
             </xs:sequence>
         </xs:complexType>
     </xs:element>
    </xs:schema>
    
  3. XML外部实体攻击(XXE攻击)

    XXE原理

    既然XML可以从外部读取DTD文件,那我们就自然地想到了如果将路径换成另一个文件的路径,那么服务器在解析这个XML的时候就会把那个文件的内容赋值给SYSTEM前面的根元素中,只要我们在XML中让前面的根元素的内容显示出来,不就可以读取那个文件的内容了。这就造成了一个任意文件读取的漏洞。

    那如果我们指向的是一个内网主机的端口呢?是否会给出错误信息,我们是不是可以从错误信息上来判断内网主机这个端口是否开放,这就造成了一个内部端口被探测的问题。另外,一般来说,服务器解析XML有两种方式,一种是一次性将整个XML加载进内存中,进行解析;另一种是一部分一部分的、“流式”地加载、解析。如果我们递归地调用XML定义,一次性调用巨量的定义,那么服务器的内存就会被消耗完,造成了拒绝服务攻击。

2.2 审计重点 IDS01-J 先标准化,然后再检验字符串

问题严重,可以包庇 XSS 跨站攻击,使得代码无法识别跨站攻击使用的<script>字符。发生可能性高,重点审计。

因为不同版本 unicode 编码集的不同,所以同一个字符没有唯一的二进制表达,存在二义性。 比如尖括号<>,在一种 Unicode 版本中可能表达为“\uFE64”“\uFE65”, 在另外 Unicode 版本可能表达为另外形式。

通常用 NFKC 格式对任意编码的字符串进行标准化,消除二义性(当然还有 NFKD 标准化形式,总要选一种)。

审计方法:

工具审计,Fortify 和 findbugs 均可。

整改方法:

先 Normalize, 再 Validate。

比如:

String s =“\uFE64”+“script”+“uFE65”;

s = Normalizer.normalize(s, Form.NFKC);

pattern.matcher(s)

以上是针对服务器端的 string validation, 多说一句,对输出到客户端的字符串进行编码可以使系统更加安全。例如,把“<”编码为“<”,“"”编码为“"”等。

例如外部输入带有使服务器重启的命令:﹤;reboot;﹥,但因为这个字符本身是全角的,在系统检测时没有匹配到而导致外部攻击成功,这时可先使用Normalizer.normalize标准化,再检验。

当然,实际操作时完全可以只判断黑名单,只要输入中包含了"reboot"这种注入命令就无法通过校验。不过,就业务而言,外部输入中的错误应该尽早拦截,早发现早治疗,越到后面捅出的篓子可能也越大。就这点而言,使用归一化操作还是有一定必要的。

2.3 审计重点 IDS02-J 先标准化,然后再检验路径名

首先通过一个例子了解路径名的多样性。黑客可以改用包含 ../序列的参数来指定位于特定目录之外的文件,从而违反程序安全策略,引发路径遍历漏洞,攻击者可能可以向任意目录上传文件。

整改方法是采用 getCanonicalPath() 方法,并后续校验用 getCanonicalPath() 得到的路径名。

Java 一般路径 getPath(), 绝对路径 getAbsolutePath() 和规范路径 getCanonicalPath() 不同。

举例在 workspace 中新建 myTestPathPrj 工程,运行如下代码

public static void testPath() throws Exception{
    File file = new File("..\\src\\testPath.txt");
    System.out.println(file.getAbsolutePath());
    System.out.println(file.getCanonicalPath());
}

得到的结果形如:

E:\workspace\myTestPathPrj\..\src\ testPath.txt
E:\workspace\src\testPath.txt

有些操作系统,例如 Windows 和 Macintosh,File.getAbsolutePath() 也能解析符号链接,别名等。尽管如此, Java 语言不能保证所有平台都奏效,或者未来实现中也能这样。

规范路径名是绝对路径名,并且是惟一的。所以还是尽量使用规范路径。

规范路径名的准确定义与系统有关。如有必要,此方法首先将路径名转换成绝对路径名,这与调用 getAbsolutePath() 方法的效果一样。然后用与系统相关的方式将它映射到其惟一路径名。这通常涉及到从路径名中移除多余的名称(比如 "." 和 "..")、分析符号连接(对于 UNIX 平台),以及将驱动器名转换成标准大小写形式(对于 Microsoft Windows 平台)。

审计方法:

人工审计,难度大,Fortify 和 Findbugs 工具不支持。

首先文本查找 getPath, getAbsolutePath。

再排查程序的安全策略配置文件,搜索 permission Java.io.FilePermission 字样和 grant 字样,防止误报。换句话说,如果 IO 方案中已经做出防御。只为程序的绝对路径赋予读写权限,其他目录不赋予读写权限。那么目录系统还是安全的。

整改方法:

尽量使用 getCanonicalPath()。

或者使用安全管理器,或者使用安全配置策略文件。如何配置安全策略文件,和具体使用的 web server 相关。

  • getPath()

将此抽象路径名转换为一个路径名字符串。所得到的字符串使用默认名称分隔符来分隔名称序列中的名称。

返回此抽象路径名的字符串形式。

  • getAbsolutePath()

返回抽象路径名的绝对路径名字符串。

如果此抽象路径名已经是绝对路径名,则返回该路径名字符串,这与 getPath() 方法一样。如果此抽象路径名是空的抽象路径名,则返回当前用户目录的路径名字符串,该目录由系统属性 user.dir 指定。否则,使用与系统有关的方式分析此路径名。在 UNIX 系统上,通过根据当前用户目录分析某一相对路径名,可使该路径名成为绝对路径名。在 Microsoft Windows 系统上,通过由路径名指定的当前驱动器目录(如果有)来分析某一相对路径名,可使该路径名成为绝对路径名;否则,可以根据当前用户目录来分析它。

返回绝对路径名字符串,它与此抽象路径名表示相同的文件或目录的。

  • getCanonicalPath()

返回抽象路径名的规范路径名字符串。

规范路径名是绝对路径名,并且是惟一的。规范路径名的准确定义与系统有关。如有必要,此方法首先将路径名转换成绝对路径名,这与调用 getAbsolutePath() 方法的效果一样,然后用与系统相关的方式将它映射到其惟一路径名。这通常涉及到从路径名中移除多余的名称(比如 "." 和 "..")、分析符号连接(对于 UNIX 平台),以及将驱动器名转换成标准大小写形式(对于 Microsoft Windows 平台)。
表示现有文件或目录的每个路径名都有一个惟一的规范形式。表示非存在文件或目录的每个路径名也有一个惟一的规范形式。非存在文件或目录路径名的规范形式可能不同于创建文件或目录之后同一路径名的规范形式。同样,现有文件或目录路径名的规范形式可能不同于删除文件或目录之后同一路径名的规范形式。

【说明】一般情况下,二者得到的结果是没有差异的。getCanonicalPath函数主要是将路径进行了无歧义的处理,即将文件路径中的相对路径符号去掉了,这样的好处是可以防止一些注入攻击。

f1 relative path: .\src\test.txt
f2 relative path: D:\workspace\demo\src\test.txt
----------------------
f1 absolute path: D:\workspace\demo\.\src\test.txt
f2 absolute path: D:\workspace\demo\src\test.txt
----------------------
f1 canonical path: D:\workspace\demo\src\test.txt
f2 canonical path: D:\workspace\demo\src\test.txt

2.4 审计重点 IDS03-J 不 log 记录未净化的用户输入

问题严重,引发日志注入攻击,通过错误的日志误导系统维护工程师。

审计方法:

人工审计,工具默认不支持。

抽查文本形似:

logger.IDSver*uIDSname

整改方案:

先净化用户输入再记录。比如 pattern.match(“[A-Za-z0-9_]+”, uIDSname) 只是整改,减小日志注入攻击可能性。

2.5 审计重点 IDS04-J 限制上传文件的大小

审计方法:

人工方式,文本搜索 new FileOutputStream,上下文搜索 getSize。

工具 Fortify 和 Findbugs 无法检测。

整改方案:

用 ZipEntry.getSize() 方法得到文件大小,如果解压文件过大,抛出异常。

更进一步,JavaWeb 中的文件上传, 一般选择采用 apache 的开源工具 common-fileupload,因为直接使用 Servlet 解析其请求参数的原始方法比较麻烦。Fileupload 的库函数中包括形如,upload.setFileSizeMax() 和 upload.setSizeMax() 的校验函数,可上下文搜索这两个库函数,以策安全。

除了文件大小,特别注意文件上传的很多小细节问题,需要在 JavaWeb 程序中实现。否则会引起资源耗尽,拒绝服务攻击 DDoS 等。

  1. 上传文件推荐放在外界无法直接访问的目录下,比如放于 WEB-INF 目录
  2. 上传文件要有唯一的文件名,防止文件覆盖
  3. 上传文件不要扎堆放置在同一个目录下,可以根据文件名,依靠 hash 算法,新建目录,分别存放
  4. 上传文件的类型要限制,简单方法是用后缀名判断

在允许上传文件的网站和 web 应用中,通过客户端代码也可以验证上传的文件。但是攻击者使用抓包修改报文并重放的方式,可以绕过客户端验证。所以有必要在服务器端再验证。业界有一些推荐的解决方案。

2.6 审计重点 IDS05-J 使用合法的文件名和路径名

如果文件名和路径名中包含了特殊字符,就会有问题。比如以破折号开头的文件名,比如空格。

审计方法:

人工审计,工具 Fortify 不能发现。

先文本搜索 new File() 字样,再上下文搜索是否提供白名单支持,形如 pattern.match,更进一步排查 validation 是否足够力度。

整改方案:

推荐的一种安全文件名的匹配模式可以是 Pattern.compile(“[^A-Za-z0-9%&+,.:=_]”)。

2.7 审计重点 IDS06-J 从格式化字符串中排除用户输入

举例来说System.out.printf(“%s”+args[0])安全可行,但是直接System.out.printf(args[0])危险,用户可以在输入中用特殊字符串比如 %l$tm 诱骗系统打印出敏感信息。

审计方法:

工具 Fortify (Juliet TestCase CWE134_Uncontrolled_Format_String 支持)。

整改方案:

从格式化字符串中排除用户输入。

2.8 审计重点 IDS07-J 不用 Runtime.exec() 传递未净化数据

每一个Java应用都有一个唯一的Runtime类的实例,通过它可以提供一个应用和应用运行环境的接口。当前的Runtime对象可以通过Runtime.getRuntime()方法获得。

在通常情况下,它应当被命令直接调用,而不应该通过shell来调用。如果需要使用shell来调用它们可以在POSIX中使用StringTokenizer来分隔从命令行读入的字符串。在Windows平台中,在处理这些符号时,他们会被连接成一个单一的参数字符串。

因而,除非显式地调用命令行解释器,否则是不会产生命令行注入攻击的。然而,当参数中包含那些以空格、双引号或者其他以-/开头的用来表示分支的字符时,就可能发生参数注入攻击。

审计方法:

工具检查 Fortify 可检测(Juliet Test Case CWE78_OS_Command_Injection 支持)。

整改方案:

白名单 e.g. Pattern.matches(“[0-9A-Za-z@.]+”, dir),或者不使用 Runtime.exec, 直接用替代的功能函数。

2.9 审计重点 IDS08-J 净化数据后传递给正则表达式

在Java中,必须注意不能误用强大的正则表达式功能。攻击者也许会通过提供一个恶意输入对初始的正则表达式进行修改,比如使其不符合程序规定的正则表达的要求。这种攻击方式称为正则注入(regex injection),他可以影响程序控制流、导致信息泄露,并引起拒绝服务漏洞。

中等严重性问题,可以引发正则注入攻击,用户输入作为正则表达式来源。审计难度大。整改方案可以使用白名单。

2.10 审计重点 IDS09-J 如果没有指定适当 locale, 不要使用 locale 相关方法

问题严重性中等。

原书建议显式设置 Locale。

实际上考虑 I18N 问题,程序会首先依据程序体内 Locale,然后参考 JVM 和 OS 的 locale。 这些应该是灵活的,不建议显式的程序代码设置 Locale。

http://localhost/index?lang=zh_CN

http://localhost/index?lang=en_US

http://localhost/index?lang=zh_TW

实际示例:

http://exam.zte.com.cn/Eval/application/Questionnaire/Questionnaire.aspx?investigateNo=743314&modelNo=87894&User=JCDz3AgEjvbr4x0wtBywJQ%3d%3d&questionnaireNo=21240767&EnterMode=Fill&Lng=en-US

2.11 审计重点 IDS10-J 不拆分两种数据结构中的字符串

不严重问题

成熟的软件产品代码,读取字符字节都是 copy 的成熟用例,出问题可能性不大。

2.12 审计重点 IDS11-J 在验证前去掉非字符码点

严重问题。审计难度大。

掌握原则:

修改任何一个字符串,包括对非字符数据的移除或者替代,必须在对该字符串进行验证之前进行!

2.13 审计重点 IDS12-J 在不同字符编码之间无损转换字符串数据

不严重,整改方式灵活。

审计方法:

人工审计。工具不支持

人工文本搜索 new String(*, charset)。

整改方案:

使用 CharsetEncoder 和 CharsetDecoder 两个类来处理。

2.14 审计重点 IDS13-J 在文件或者网络 I/O 两端使用兼容的编码方式

问题不严重,人工审计,需要开发者配合理解代码逻辑和业务场景。

需要了解对于 web 应用程序,客户端和服务器端和编码有关的设置函数

  1. Java class 编译时可以指定编码
  2. JSP 编译: 在 JSP 文件开头设置,一般设为 charset=utf-8。
  3. JSP 输出:在 JSP 文件头可以指定,比如 response.setCharacterEncoding("GBK")。这指定文件输出到浏览器时使用的编码。
  4. META 设置:针对静态网页。因为静态网页无法使用上述两个 JSP 设置。JSP 设置优先级高于 META 设置,当它们共存时。
  5. FORM 设置:例如 URLEncoder.encode(key, "utf-8"))。浏览器专门针对表单,而不是网页使用特点 charset。

编码知识扩展:

为什么要编码?

1,计算机存储信息通常以byte为单位,占8个二进制位,所以一个字节能表示的状态只有255种

2,人类语言的符号太多,255位不够

3,所以必须把多个字节合起来表示一个人类语言符号

4,在怎么组合字节上出现了多种方法,这就是编码

需要注意:脱离具体的编码谈某个字符占几个字节是没有意义的

这就好比如43占几个字节是取决于用何种数据类型存储的,用byte占1个字节,用short通常占2个字节,用int通常占4个字节,用long则通常占8个字节;同理,编码决定了每个字符占几个字节。

  1. 同一个字符在不同的编码下可能占不同的字节:就以“”字为例,“”在 GBK 编码下占 2 字节,在 UTF-16 编码下也占 2 字节,在 UTF-8 编码下占 3 字节,在 UTF-32 编码下占 4 字节。
  2. 不同的字符在同一个编码下也可能占不同的字节:“”在 UTF-8 编码下占3字节,而“A”在 UTF-8 编码下占 1 字节。(因为 UTF-8 是变长编码)

JVM规范中明确说明了java的char类型使用的编码方案是UTF-16。

由于受启动参数及所在操作系统环境的影响,不带参数的 getBytes 方法通常是不建议使用的,最好是显式地指定参数以此获得稳定的预期行为。

String a = new String("字");
System.out.println(a.getBytes().length);
System.out.println(a.getBytes("GBK").length);

Unicode字符集

Unicode 是全球文字统一编码。它把世界上的各种文字的每一个字符指定唯一编码,实现跨语种、跨平台的应用。

Unicode 只是一个符号集,它只规定了每个符号的二进制数,却没有规定这个二进制数应该如何存储。比如,汉字‘严’的 Unicode 是十六进制数4E25,转换成二进制数足足有15位(100111000100101),也就是说,这个符号的表示至少需要2个字节。表示其他更大的符号,可能需要3个字节或者4个字节,甚至更多。

流行的Unicode 编码方案有两种UTF-16和UTF-8。其实还有一个UTF-32。

https://www.qqxiuzi.cn/zh/hanzi-unicode-bianma.php

UTF-8和Unicode区别

Unicode是一种字符集,而UTF-8是一种编码规则。一般在计算机内存中,统一使用Unicode编码,当需要保存到硬盘或者需要传输的时候,就转换为UTF-8编码。

章节总结

本章主要讲解的是要对输入的数据进行验证和净化处理,本质是程序内部不能信任一切外来的输入数据!

附录(部分示例代码)

CanonicalPathTest:

public class CanonicalPathTest {
    @Test
    public void testPath() throws IOException {
        File f1 = new File(".\\src\\test.txt");
        File f2 = new File("D:\\workspace\\demo\\src\\test.txt");
        System.out.println("f1 relative path: "+f1.getPath());
        System.out.println("f2 relative path: "+f2.getPath());
        System.out.println("----------------------");
        System.out.println("f1 absolute path: "+f1.getAbsolutePath());
        System.out.println("f2 absolute path: "+f2.getAbsolutePath());
        System.out.println("----------------------");
        System.out.println("f1 canonical path: "+f1.getCanonicalPath());
        System.out.println("f2 canonical path: "+f2.getCanonicalPath());
        System.out.println("----------------------");
    }
}

UnicodeTest:

public class UnicodeTest {

    @Test
    public void test(){
        String file = ".\\src\\test.txt";
        readByByte(file);
        System.out.println("---------------------------------------");
        readByChar(file);
    }


    @Test
    public void testEncoding() throws UnsupportedEncodingException {
        String a = new String("字");
        System.out.println(a.getBytes().length);
        System.out.println(a.getBytes("GBK").length);
    }


    /**
     * 读取文件
     * @param string
     */
    public static void readByByte(String string){
        File file = new File(string);    //1、建立连接
        InputStream is = null;
        try {
            is = new FileInputStream(file);    //2、选择流(此处为输入流)
//            //和上一句功能一样,BufferedInputStream是增强流,加上之后能提高输入效率,建议!
//            is = new BufferedInputStream(new FileInputStream(file));
            int len;
            byte[] car = new byte[1];
            while((len = is.read(car))!= -1) {    //3、操作:以每次car大小读取
                String ss = new String(car,0,len);    // 将byte类型的数组转化成字符串,方便下面输出
                System.out.println(ss);
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
            System.out.println("文件不存在!");
        } catch (IOException e) {
            e.printStackTrace();
            System.out.println("读取文件失败!");
        }finally {
            if (is != null) {    //若is还存在就需要释放,否则不需要释放
                try {
                    is.close();
                } catch (IOException e) {
                    e.printStackTrace();
                    System.out.println("关闭文件输入流失败");
                }
            }
        }
    }

    /**
     * 读取文件
     * @param string
     */
    public static void readByChar(String string){
        File file = new File(string);
        FileReader fr = null;
        try {
            fr = new FileReader(file);
            int line = 0;
            while((line = fr.read())!= -1) {
                System.out.println((char)line);
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
            System.out.println("文件不存在!");
        } catch (IOException e) {
            e.printStackTrace();
            System.out.println("读取文件失败!");
        }finally {
            if (fr != null) {    //若is还存在就需要释放,否则不需要释放
                try {
                    fr.close();
                } catch (IOException e) {
                    e.printStackTrace();
                    System.out.println("关闭文件输入流失败");
                }
            }
        }
    }
}

Xml校验:

XmlTest:

public class XmlTest extends DemoApplicationTests{
    @Autowired
    private XmlService xmlService;

    @Test
    public void readXml(){
        List<Book> books = new ArrayList<>();
        try {
//            books = xmlService.getBooks(ResourceUtils.getFile("classpath:xml/book.xml"));
            books = xmlService.getBooks(ResourceUtils.getFile("classpath:xml/book_error.xml"));
        } catch (IOException e) {
            e.printStackTrace();
        }
        if(!CollectionUtils.isEmpty(books)){
            books.forEach(System.out::println);
        }
    }

    @Test
    public void validateXml() throws FileNotFoundException {
        String xmlFileName1 = ResourceUtils.getURL("classpath:xml/book.xml").getPath();
        String xmlFileName2 = ResourceUtils.getURL("classpath:xml/book_error.xml").getPath();
        String xsdFileName = ResourceUtils.getURL("classpath:xml/book.xsd").getPath();
        String result1 = xmlService.validateXMLByXSD(xmlFileName1, xsdFileName);
        String result2 = xmlService.validateXMLByXSD(xmlFileName2, xsdFileName);
        System.out.println(result1);
        System.out.println(result2);
    }
}

XmlService:

public interface XmlService {
    List<Book> getBooks(File file);
    String validateXMLByXSD(String xmlFileName, String xsdFileName);
    String validateXMLByDTD(String xmlFileName, String dtdFileName);
}

XmlServiceImpl:

@Service
public class XmlServiceImpl implements XmlService {
    @Override
    public List<Book> getBooks(File file) {
        List<Book> bookList = new ArrayList<>();
        Book book;

        SAXReader reader = new SAXReader();
        try {
            Document document = reader.read(file);
            Element bookstore = document.getRootElement();
            Iterator storeit = bookstore.elementIterator();

            while(storeit.hasNext()){
                book = new Book();
                Element bookElement = (Element) storeit.next();
                //遍历bookElement的属性
                List<Attribute> attributes = bookElement.attributes();
                for(Attribute attribute : attributes){
                    if(attribute.getName().equals("id")){
                        String id = attribute.getValue();//System.out.println(id);
                        book.setId(Integer.parseInt(id));
                    }
                }

                Iterator bookit = bookElement.elementIterator();
                while(bookit.hasNext()){
                    Element child = (Element) bookit.next();
                    String nodeName = child.getName();
                    if(nodeName.equals("name")){
                        //System.out.println(child.getStringValue());
                        String name = child.getStringValue();
                        book.setName(name);
                    }else if(nodeName.equals("author")){
                        String author = child.getStringValue();
                        book.setAuthor(author);
                    }else if(nodeName.equals("year")){
                        String year = child.getStringValue();
                        book.setYear(Integer.parseInt(year));
                    }else if(nodeName.equals("price")){
                        String price = child.getStringValue();
                        book.setPrice(Double.parseDouble(price));
                    }
                }
                bookList.add(book);
                book = null;
            }
        } catch (DocumentException e) {
            e.printStackTrace();
        }
        return bookList;
    }

    @Override
    public String validateXMLByXSD(String xmlFileName, String xsdFileName) {
        String result = "";
        try {
            //创建默认的XML错误处理器
            XMLErrorHandler errorHandler = new XMLErrorHandler();
            //获取基于 SAX 的解析器的实例
            SAXParserFactory factory = SAXParserFactory.newInstance();
            //解析器在解析时验证 XML 内容。
            factory.setValidating(true);
            //指定由此代码生成的解析器将提供对 XML 名称空间的支持。
            factory.setNamespaceAware(true);
            //使用当前配置的工厂参数创建 SAXParser 的一个新实例。
            SAXParser parser = factory.newSAXParser();
            //创建一个读取工具
            SAXReader xmlReader = new SAXReader();
            //获取要校验xml文档实例
            Document xmlDocument = (Document) xmlReader.read(new File(xmlFileName));
            //设置 XMLReader 的基础实现中的特定属性。核心功能和属性列表可以在 [url]http://sax.sourceforge.net/?selected=get-set[/url] 中找到。
            parser.setProperty(
                    "http://java.sun.com/xml/jaxp/properties/schemaLanguage",
                    "http://www.w3.org/2001/XMLSchema");
            parser.setProperty(
                    "http://java.sun.com/xml/jaxp/properties/schemaSource",
                    "file:" + xsdFileName);
            //创建一个SAXValidator校验工具,并设置校验工具的属性
            SAXValidator validator = new SAXValidator(parser.getXMLReader());
            //设置校验工具的错误处理器,当发生错误时,可以从处理器对象中得到错误信息。
            validator.setErrorHandler(errorHandler);
            //校验
            validator.validate(xmlDocument);

            XMLWriter writer = new XMLWriter(OutputFormat.createPrettyPrint());
            //如果错误信息不为空,说明校验失败,打印错误信息
            if (errorHandler.getErrors().hasContent()) {
                System.out.println("XML文件通过XSD文件校验失败!");
                writer.write(errorHandler.getErrors());
                result = "XML文件通过XSD文件校验失败!";
            } else {
                System.out.println("Good! XML文件通过XSD文件校验成功!");
                result = "Good! XML文件通过XSD文件校验成功!";
            }
        } catch (Exception ex) {
            result = "XML文件通过XSD文件校验失败!原因:"+ex.getMessage();
            System.out.println("XML文件: " + xmlFileName + " 通过XSD文件:" + xsdFileName + "检验失败。/n原因: " + ex.getMessage());
            ex.printStackTrace();
        }
        return result;
    }

    @Override
    public String validateXMLByDTD(String xmlFileName, String dtdFileName) {
        //TODO
        return null;
    }
}

book.xml:

<?xml version="1.0" encoding="UTF-8"?>
<!--<!DOCTYPE bookstore SYSTEM "http://www.w3school.com.cn/dtd/book.dtd">-->
<bookstore xmlns="http://www.w3school.com.cn"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="http://www.w3school.com.cn book.xsd">
    <book id="1">
        <name>冰与火之歌</name>
        <author>乔治马丁</author>
        <year>2014</year>
        <price>
            89.00
            <!--</price><author>余欢</author><price>0.12-->
        </price>
    </book>
    <book id="2">
        <name>安徒生童话</name>
        <author>安徒生</author>
        <year>2004</year>
        <price>77.50</price>
    </book>
    <book id="3">
        <name>think think think</name>
        <author>aaa</author>
        <year>1997</year>
        <price>100.00</price>
    </book>
</bookstore>

book.xsd:

<?xml version="1.0" encoding="UTF-8" ?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
           targetNamespace="http://www.w3school.com.cn"
           xmlns="http://www.w3school.com.cn"
           elementFormDefault="qualified">
    <xs:element name="bookstore">
        <xs:complexType>
            <xs:sequence>
                <xs:element name="book" minOccurs="0" maxOccurs="unbounded">
                    <xs:complexType>
                        <xs:sequence>
                            <xs:element name="name" type="xs:string"/>
                            <xs:element name="author" type="xs:string"/>
                            <xs:element name="year">
                                <xs:simpleType>
                                    <xs:restriction base="xs:integer">
                                        <xs:minInclusive value="0"/>
                                        <xs:maxInclusive value="3000"/>
                                    </xs:restriction>
                                </xs:simpleType>
                            </xs:element>
                            <xs:element name="price">
                                <xs:simpleType>
                                    <xs:restriction base="xs:decimal">
                                        <xs:pattern value="[\d]{0,16}\.[\d]{2}"/>
                                    </xs:restriction>
                                </xs:simpleType>
                            </xs:element>
                        </xs:sequence>

                        <xs:attribute type="xs:integer" name="id"/>

                    </xs:complexType>
                </xs:element>
            </xs:sequence>
        </xs:complexType>
    </xs:element>
</xs:schema>

book.dtd:

<!ELEMENT bookstore (book)*>
<!ELEMENT book (name, author, year, price)>
        <!ATTLIST book id ID #IMPLIED>
<!ELEMENT name (#PCDATA)>
<!ELEMENT author (#PCDATA)>
<!ELEMENT year ()>
<!ELEMENT price (#PCDATA)>

ZipInputStreamTest:

public class ZipInputStreamTest {

    private static final int BUFFER = 1024;
    private static final long TOO_BIG = 0x6400000;

    @Test
    public void test(){
        String zipPath = "D:\\java soft\\ideaIC-2019.1.2.zip";
        String destPath = "C:\\Users\\10256880\\Desktop\\test\\";

        unZip(zipPath, destPath);
    }

    public void unZip(String zipPath, String destPath) {
        ZipInputStream zip;
        try {
            zip = new ZipInputStream(new FileInputStream(new File(zipPath)));
            ZipEntry zipEntry = null;
            while ((zipEntry = zip.getNextEntry()) != null) {
                System.out.println("Extracting: " + zipEntry);
                if(zipEntry.getSize() > TOO_BIG){
                    throw new IllegalStateException("file size too big!!!");
                }
                File file = new File(destPath + zipEntry.getName());
                if(zipEntry.isDirectory()){
                    file.mkdirs();
                }else {
                    int count;
                    byte data[] = new byte[BUFFER];
                    FileOutputStream fos = new FileOutputStream(file);
                    BufferedOutputStream dest = new BufferedOutputStream(fos, BUFFER);
                    while ((count = zip.read(data, 0, BUFFER)) != -1) {
                        dest.write(data, 0, count);
                    }
                    dest.flush();
                    dest.close();
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容