Java(1.8)高级特性-安全
这里的安全包括了Java虚拟机如何保障自己的安全,不被恶意程序所侵害和Java提供的加密方法。
虚拟机的安全
Java虚拟机通过多种方法保障运行时的安全,这里将介绍虚拟机类加载时的安全保障和运行类方法时的权限查看。
类加载验证
虚拟机在从一个文件加载类信息时需要经过加载、连接、初始化、使用、卸载这几个步骤。其中连接这一步中包括了验证的部分,在这一部分将验证文件、元数据、字节码、符号引用,以保证加载进来的类不会对虚拟机造成威胁,详细内容在介绍JVM的这篇记录中。
安全管理器与访问权限
安全管理器是负责检查具体操作是否允许执行的类,在运行Java程序是默认不启用该管理器的,可以通过在main
函数中调用System.setSecurityManager(new SecurityManager())
或在启动虚拟机时使用java -Djava.security.manager
来启用安全管理器。
Java中的安全策略将代码源和权限集进行了映射,下面是一个策略文件的例子,其中代码源为http://www.horstmann.com/classes
,权限为对/tmp
目录下的所有文件拥有读写的权限。这些代码源和权限集合被保存在每个类的保护域中。当安全管理器要检查某个权限时,会查看当前位于调用堆栈上的所有方法的类,获取类的保护域,检查是否有权限执行当前的操作。
grant codeBase "http://www.horstmann.com/classes"
{
permission java.io.FilePermission "/tmp/*", "read,write";
};
-
代码源
代码源由代码位置和证书集指定。代码位置指定了代码的来源,如HTTP URL,本地文件等。证书集的目的是由某一方来保证代码没有被篡改过。 -
权限类
安全_权限类.png
Java提供了多个权限类,每个类都封装了特定的权限信息。权限信息不仅可以通过类进行初始化,如FilePermission p=new FilePermission("/tmp/*","write,read");
,还可以存储在文件中,权限在文件中这样表示,permission java.io.FilePermission "/tmp/*", "write,read"
。 -
安全策略文件
上面给出的例子就是一个安全策略文件,安全策略文件可以安装在标准位置上,默认情况下为Java平台主目录的java.policy
文件和用户主目录的.java.policy
文件。如果不想要覆盖标准文件,那么也可以通过在main
函数中调用System.setProperty("java.security.policy","thePolicy")
或在启动虚拟机时使用java -Djava.security.policy=thePolicy classname
来指定相应的策略文件。
一个文件具有以下的格式,其中代码源包含一个代码基和值得新来的用户特征与证书签名者的名字(如果不要求签名,则可以省略)。grant codesource { permission1; permission2; }
代码基格式为:
codebase "url"
,其中如果url
以/
结尾,则表示一个目录,否则为一个文件。url
中的分隔符总是/
,与操作系统无关。
权限格式为:permission className targetName,actionList;
。className
是权限类的全称类名(java.io.FilePermission)。targetName
是与权限相关的值,如文件名或目录名或socket权限中的主机和端口。actionList
是与权限相关的操作,如read、write,不同的权限类提供了不同的操作方法。
targetName
可以有多种形式:- 文件
file
(文件)、directory/
(目录)、directory/*
(目录下的所有文件)、*
(当前目录的所有文件)、directory/-
(目录和子目录中的所有文件)、-
(当前目录和子目录中的所有文件)、<<ALL FILES>>
(所有文件)。
用${}
可以访问系统属性,用${/}
可以表示分隔符以满足不同平台的适用性。
例子:permission java.io.FilePermission "C:${/}myapp${/}-" ,"read,write"
。 - Socket
hostname
或IPaddress
(单个主机)、localhost
或空字符串(本地主机)、*.domainSuffix
(以给定后缀结尾的域中所有主机)、*
(所有主机)。
端口表示::n
(单个端口)、:n-
(大于等于n的端口)、:-n
、:n1-n2
。
例子:permission java.net.SocketPermission "*.hostmann.com:8000-9999","connect"
。
- 文件
-
自定义权限类
实现自定义的权限类需要实现以下功能:- 带有两个String参数的构造器,分别是目标和操作列表
- String getActions()
- boolean equals(Object other)
- int hashCode()
- boolean implies(Permission other)
其中最后一个方法比较重要,权限有一个
排序
,该方法表示了该权限是否隐含了另一个权限。比如对文件/tmp/-
的write
权限蕴含了对文件/tmp/README
的write
权限,更确切的说**如果p1的目标文件包含p2的目标文件,p1的操作集包括p2的操作集,那么p1就隐含了p2
、
用户认证
数据安全
这一部分涉及了如何保障一个数据在互联网中传输的安全性。
数字签名
假设你想要向某人(对方)发送一个数据,如何能够保证对方接收到的数据就是你发送的那一个呢?如果不进行任何安全保障,数据在网络中传输就可能会被第三方修改或劫持。下面就将一步步保障数据的安全性。
原方法
:你直接发送数据给对方。
消息摘要
消息摘要是数据块的指纹,得到一个消息摘要的算法有SHA1、SHA256、SHA384、MD5等。
这些算法均保证了以下几点:
- 就算数据只修改了1位,得到的消息摘要也会有较大的不同。
- 无法伪造与原消息的摘要相同的假消息。
获得了一个数据的消息摘要后,就可以与原数据一通发送给对方,对方收到数据后也使用相同的算法获得数据的消息摘要,并于你发送的摘要进行对比,便可以得知数据是否被修改过。
更优方法
:你生成一个数据的消息摘要,与数据一同发送给对方。对方接收到后,根据数据生成一个消息摘要并与你发送的消息摘要进行对比。如果相同,则说明数据未被修改。
消息签名
上述方法存在一个漏洞,如果第三人劫持了你发送的数据和消息摘要,生成了一个伪造的数据和伪造的摘要发送给对方,这时候虽然对方接收到的数据不是你发送的数据,但是也能够通过上述的验证方法。为了解决这个问题,就需要对摘要进行签名,保证摘要是由你生成的,也就是接下来所说的消息签名。
消息签名所用的方法就是对消息摘要通过你独有的方式进行加密,加密后的消息摘要只能通过与加密对应的方法进行解密。这样,对方通过解密之后,再进行对比,就能知道这个数据是否是由你发送、是否被修改过。如果无法通过对应的方法解密或解密后的摘要与接收到的数据的摘要不同,就说明数据被修改过了。
非对称加密
技术是现在普遍使用的加密方法。非对称加密
需要两个钥,分别是公钥和私钥,其中公钥任何人都能够获取到,而私钥个人保管不公开。任何通过公钥进行加密的数据可以通过私钥进行解密,任何通过私钥进行加密的数据可以通过公钥进行解密。
更更优方法
:现在你向对方发送信息摘要时,通过自己的私钥加密消息摘要,然后与数据和你的公钥(通常会被加工成一个证书,包含了你的一些相关信息)一同发送给对方。对方接收到数据和消息摘要后,用你的公钥对消息摘要进行解密,然后自己通过数据生成消息摘要并与你发送的消息摘要进行对比。如果相同,则说明数据未被修改。
认证问题
在上述的方法中,你将你的证书(公钥)发送给了对方,对方通过你的证书进行解密码。这里存在一个安全问题,就是如果第三方又劫持了你的数据、消息摘要和证书。他假造了数据和用他的私钥进行加密的消息摘要,并把他自己的证书发送了给对方。这时候对方用得到的“你的证书”进行验证,不会出现任何错误。
为了解决上述问题,就需要保证对方获得的证书的的确确就是你的证书。为了达到这一点,我们引入中间人,也就是一个权威。该权威保存了你的证书,当对方接收到你的证书后和你保存在权威那的证书进行对比,相同则没有问题。
最终方法
:你将数据、消息摘要和证书发送给对方,对方从权威那获得你的证书并与接受到的证书进行比较,相同则说明没有被修改过。对方解密后就能得到你发送的数据。
加密
介绍完一些安全方面的概念,终于要介绍Java所提供的安全相关的类和用法了。
对称密码
对称密码使用同一个钥进行加密和解密。
Java中提供了Cipher
类,该类是所有加密算法的超类,通过下述方法就可以获得一个实例,其中algorithName
为加密算法的名字,prividerName
是算法的提供者。在获得实例并调用init()
初始化后就能够进行加密和解密。
Cipher cipher=new Cipher(algorithName);
Cipher cipher=new Cipher(algorithName,providerName);
//模式有Cipher.ENCRYPT_MODE、DECRYPT_MODE、WRAP_MODE、UNWRAP_MODE
int mode=..;
Key key=..;
cipher.init(mode,key);
int blockSize=cipher.getBlockSize();
byte[] inBytes=new byte[blockSize];
.....//read bytes
int outputSize=cipher.getOutputSize(blockSize);
byte[] outBytes=new byte[outputSize];
//调用update进行加密
int outLength=cipher.update(inBytes,0,outputSize,outBytes);
...//write bytes
//在update调用完后还需要调用doFinal
//如果数据没写完,则调用
outBytes=cipher.doFinal(inBytes,0,inLength);
//写完了,则调用
outBytes=cipher.doFinal();
在这个例子中,在调用init()
后使用update()
来对输入的数据进行加密。在调用完update()
后调用doFinal()
的目的是对文件最后的块进行填充。以DES密码来说,它的数据块大小都为8字节,如果最后一个数据块少于8字节则需要进行填充。填充的方法是以缺少的字节数的数字进行填充,比如当最后的数据块只有7个字节,缺少1个字节,那么就用01
进行填充,如果缺少2个字节,就用02
进行填充,如果不缺字节,就用08
进行填充,生成08 08 08 08 08 08 08 08
。这样做的好处是,当接收到文件并解密后,查看最后一字节的数字为几就删掉几个字节,这样得到的文件就和原文件一样了。
Cipher
类还提供了密码流的方法,可以对流数据自动加密或解密,只需要创建CipherOutputStream
或CipherInputStream
即可。
Cipher cipher= ...;
cipher.init(mode,key);
CipherOutputStream out=new CipherOutputStream(new FileOutputStream(outputFileName),cipher);
//调用write方法就会自动进行加密
out.write(bytes,0,inLength);
CipherInputStream in=new CipherInputStream(new FileInputStream(inputFileName),cipher);
//同样,调用read方法就会自动进行解密
in.read();
公共密钥密码
公共密钥密码也就是之前提到的非对称加密技术使用的密码,分为公钥和私钥。目前最常见的公共密钥算法是RSA
算法,使用该算法需要一个公钥和私钥,可以使用Key-PairGenerator
来获得。生成key
后就可以用来初始化Cipher
类。
KeyPairGenerator pairgen=KeyPairGenerator.getInstance("RSA");
SecureRandom random=new SecureRandom();
pairgen.initialize(KEYSIZE,random);
KeyPair keyPair=pairgen.generateKeyPair();
Key publicKey=keyPair.getPublic();
Key privateKey=keyPair.getPrivate();
生成密钥
生成随机的密钥有三个步骤:
⑴ 为加密算法获取KeyGenerator
。
⑵ 用随机数来初始化密钥产生器。
⑶ 调用generateKey
的方法。
为了保证密钥的安全性,需要保证随机数足够"随机"。普通的Random
类是基于当前的日期和时间来产生随机数的,如果攻击者知道发布密钥的日期,就能够使用枚举法破解。在上述的代码中使用了SecureRandom
类,该类产生的随机数安全很多,该类在产生随机数时需要一个种子,一般是用户的随机输入。