20220424 Java核心技术 卷2 高级特性 9
安全
当 Java 技术刚刚问世时,令人激动的并不是因为它是一种设计完美的编程语言,而是因为它能够安全地运行通过因特网传播的各种 applet 。这就意味着, Java 与其他的语言和系统有所不同,在那些语言和系统中安全是在事后才想到要去实现的,或者是对破坏的一种应对措施,而对 Java 来说,安全机制是一个不可分割的组成部分
Java 技术提供了以下 种确保安全的机制:
- 语言设计特性(对数组的边界进行检查,元不受检查的类型转换,无指针算法等)
- 访问控制机制,用于控制代码能够执行的操作(比如文件访问,网络访问等)
- 代码签名,利用该特性,代码的作者就能够用标准的加密算法来认证 Java 代码。这样,该代码的使用者就能够准确地知道谁创建了该代码,以及代码被标识后是否被修改过
类加载器,它可以在将类加载到虚拟机中的时候检查类的完整性。
为了获得最大的安全性,无论是加载类的默认机制,还是自定义的类加载器,都需要与负责控制代码运行的 安全管理器类 协同工作。
java.security
包提供的加密算法,用来进行代码的标识和用户身份认证
类加载器
Java 编译器会为虚拟机转换源指令。虚拟机代码存储在以 .class
为扩展名的类文件中,每个类文件都包含某个类或者接口的定义和实现代码。这些类文件必须由一个程序进行解释,该程序能够将虚拟机的指令集翻译成目标机器的机器语言。
类加载过程
虚拟机只加载程序执行时所需要的类文件
假设程序从 MyProgram.class
开始运行,下面是虚拟机执行的步骤:
- 虚拟机有一个用于加载类文件的机制,例如,从磁盘上读取文件或者请求 Web 上的文件;它使用该机制来加载 MyProgram 类文件中的内容
- 如果 MyProgram 类拥有类型为另一个类的域,或者是拥有超类,那么这些类文件也会被加载 (加载某个类所依赖的所有类的过程称为类的 解析
- 接着,虚拟机执行时
MyProgram
中的main
方法(它是静态的,无需创建类的实例) - 如果
main
方法或者main
调用的方法要用到更多的类,那么接下来就会加载这些类
类加载机制并非只使用单个的类加载器。每个 Java 程序至少拥有三个类加载器:
- 引导类加载器
- 扩展类加载器
- 系统类加载器(有时也称为应用类加载器)
引导类加载器负责加载系统类(通常从 JAR 文件 rt.jar 中进行加载) 。它是虚拟机不可分割的一部分,而且通常是用 C 语言来实现的。引导类加载器没有对应的 ClassLoader
对象
ClassLoader classLoader = String.class.getClassLoader();
System.out.println(classLoader); // null
扩展类加载器用于从 jre/lib/ext
目录加载 “标准的扩展” 。可以将 JAR 文件放入该目录,这样即使没有任何类路径,扩展类加载器也可以找到其中的各个类
系统类加载器用于加载应用类。它在由 CLASSPATH
环境变量或者 -classpath
命令行选项设置的类路径中的目录里或者是 JAR/ZIP 文件里查找这些类
在 Oracle 的 Java 语言实现中,扩展类加载器和系统类加载器都是用 Java 来实现的。它们都是 URLClassLoader
类的实例
警告: 如果将 JAR 文件放入 jre/lib/ext
目录中,并且在它的类中有一个类需要调用系统类或者扩展类,那么就会遇到麻烦,因为扩展类加载器并不使用类路径。在使用扩展目录来解决类文件的冲突之前,要牢记这种情况
注意:除了所有已经提到的位直,还可以从 jre/lib/endorsed
目录中加载类。这种机制只能用于将某个标准的 Java 类库替换为更新的版本(例如那些支持 XML 和 CORBA 的类库)。参考
类加载器的层次结构
类加载器有一种父/子关系。除了引导类加载器外,每个类加载器都有一个父类加载器。根据规定,类加载器会为它的父类加载器提供一个机会,以便加载任何给定的类,并且只有在其父类加载器加载失败时,它才会加载该给定类。例如,当要求系统类加载器加载一个系统类(比如, java.util.Arraylist
)时,它首先要求扩展类加载器进行加载,该扩展类加载器则首先要求引导类加载器进行加载。引导类加载器会找到并加载 rt.jar
中的这个类,而无须其他类加载器做更多的搜索
某些程序具有插件架构,其中代码的某些部分是作为可选的插件打包的。如果插件被打包为 JAR 文件,那就可以直接用 URLClassLoader
类的实例去加载这些类
URL url = new URL("file:///path/to/plugin.jar");
URLClassLoader pluginLoader = new URLClassLoader(new URL[]{url});
Class<?> cl = pluginLoader.loadClass("mypackage.MyClass");
因为在 URLClassloader
构造器中没有指定父类加载器,因此 pluginLoader
的父亲就是系统类加载器
大多数时候,你不必操心类加载的层次结构。通常,类是由于其他的类需要它而被加载的,而这个过程对你是透明的
偶尔,你也会需要干涉和指定类加载器。考虑下面的例子:
- 你的应用的代码包含一个助手方法,它要调用
Class.forName(classNameString)
- 这个方法是从一个插件类中被调用的
- 而
classNameString
指定的正是一个包含在这个插件的 JAR 中的类
插件的作者会很合理地期望这个类应该被加载。但是,助手方法的类是由系统类加载器加载的,这正是 Class.forName
所使用的类加载器。而对于它来说,在插件 JAR 中的类是不可视的,这种现象称为 类加载器倒置
要解决这个问题,助手方法需要使用恰当的类加载器,它可以要求类加载器作为其一个参数传递给它。或者,它可以要求将恰当的类加载器设置成为当前线程的上下文类加载器,这种策略在许多框架中都得到了应用(例如 JAXP 和 JNDI 框架)
每个线程都有一个对类加载器的引用,称为上下文类加载器。主线程的上下文类加载器是系统类加载器。当新线程创建时,它的上下文类加载器会被设置成为创建该线程的上下文类加载器。因此,如果你不做任何特殊的操作,那么所有线程就都会将它们的上下文类加载器设置为系统类加载器
通过下面的调用将线程的上下文类加载器设置成为任何类加载器:
Thread t = Thread.currentThread();
t.setContextClassLoader(loader);
然后助手方法可以获取这个上下文类加载器:
Thread t = Thread.currentThread();
ClassLoader loader = t.getContextClassLoader();
Class cl = loader.loadClass(className);
当上下文类加载器设置为插件类加载器时,问题依旧存在。应用设计者必须作出决策:通常,当调用由不同的类加载器加载的插件类的方法时,进行上下文类加载器的设置是一种好的思路;或者,让助手方法的调用者设置上下文类加载器
提示:如果你编写了一个按名字来加载类的方法,那么让调用者在传递显式的类加载器和使用上下文类加载器之间进行选择就是一种好的做法。不要直接使用该方法所属的类的类加载器
将类加载器作为命名空间
在同一个虚拟机中,可以有两个类,它们的类名和包名都是相同的。类是由它的全名和类加载器来确定的。这项技术在加载来自多处的代码时很有用
两个类加载器分别加载具有相同名字的两个类:
编写你自己的类加载器
我们可以编写自己的用于特殊目的的类加载器,这使得我们可以在向虚拟机传递字节码之前执行定制的检查
如果要编写自己的类加载器,只需要继承 ClassLoader
类,然后覆盖下面这个方法
protected Class<?> findClass(String name)
ClassLoader
超类的 loadClass
方法用于将类的加载操作委托给其父类加载器去进行,只有当该类尚未加载并且父类加载器也无法加载该类时,才调用 findClass
方法。
如果要实现该方法,必须做到以下几点:
- 为来自本地文件系统或者其他来源的类加载其字节码
- 调用
ClassLoader
超类的defineClass
方法,向虚拟机提供字节码
/**
* This class loader loads encrypted class files.
*/
class CryptoClassLoader extends ClassLoader {
private int key;
/**
* Constructs a crypto class loader.
*
* @param k the decryption key
*/
public CryptoClassLoader(int k) {
key = k;
}
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] classBytes = null;
classBytes = loadClassBytes(name);
Class<?> cl = defineClass(name, classBytes, 0, classBytes.length);
if (cl == null) {
throw new ClassNotFoundException(name);
}
return cl;
} catch (IOException e) {
throw new ClassNotFoundException(name);
}
}
/**
* Loads and decrypt the class file bytes.
*
* @param name the class name
* @return an array with the class file bytes
*/
private byte[] loadClassBytes(String name) throws IOException {
String cname = name.replace('.', '/') + ".caesar";
byte[] bytes = Files.readAllBytes(Paths.get(cname));
for (int i = 0; i < bytes.length; i++) {
bytes[i] = (byte) (bytes[i] - key);
}
return bytes;
}
}
java.lang.Class<T> 方法名称 |
方法声明 | 描述 |
---|---|---|
getClassLoader |
public ClassLoader getClassLoader() |
获取加载该类的类加载器 |
java.lang.ClassLoader 方法名称 |
方法声明 | 描述 |
---|---|---|
getParent |
public final ClassLoader getParent() |
返回父类加载器,如果父类加载器是引导类加载器,则返回 null |
getSystemClassLoader |
public static ClassLoader getSystemClassLoader() |
获取系统类加载器,即用于加载第一个应用类的类加载器 |
findClass |
protected Class<?> findClass(String name) throws ClassNotFoundException |
类加载器应该覆盖该方法,以查找类的字节码,并通过调用 defineClass 方法将字节码传给虚拟机。在类的名字中,使用 . 作为包名分隔符,并且不使用 .class 后缀 |
defineClass |
protected final Class<?> defineClass(String name, byte[] b, int off, int len) throws ClassFormatError |
将一个新的类添加到虚拟机中,其字节码在给定的数据范围中 |
java.net.URLClassLoader 方法名称 |
方法声明 | 描述 |
---|---|---|
构造器 | public URLClassLoader(URL[] urls) public URLClassLoader(URL[] urls, ClassLoader parent) |
构建一个类加载器,它可以从给定的 URL 处加载类。如果 URL 以 / 结尾,那么它表示的一个目录,否则,它表示的是一个 JAR 文件 |
java.lang.Thread 方法名称 |
方法声明 | 描述 |
---|---|---|
getContextClassLoader |
public ClassLoader getContextClassLoader() |
获取类加载器,该线程的创建者将其指定为执行该线程时最适合使用的类加载器 |
setContextClassLoader |
public void setContextClassLoader(ClassLoader cl) |
为该线程中的代码设置一个类加载器,以获取要加载的类。如果在启动一个线程时没有显式地设置上下文类加载器,则使用父线程的上下文类加载器 |
字节码校验
当类加载器将新加载的 Java 平台类的字节码传递给虚拟机时,这些字节码首先要接受 校验器( verifier )的校验。校验器负责检查那些指令无法执行的明显有破坏性的操作。除了系统类外,所有的类都要被校验
下面是校验器执行的一些检查:
- 变量要在使用之前进行初始化
- 方法调用与对象引用类型之间要匹配
- 访问私有数据和方法的规则没有被违反
- 对本地变量的访问都落在运行时堆栈内
- 运行时堆栈没有溢出
如果以上这些检查中任何一条没有通过, 那么该类就被认为遭到了破坏,并且不予加载
如果校验器接受了一个程序,那么该程序就确实是安全的。然而,也有许多程序尽管是安全的,但却被校验器拒绝了 (在强制用哑元值来初始化一个变量时,你就会碰到这个问题,因为编译器无法了解这个变量是否可以被正确地初始化)
这种严格的校验是出于安全上的考虑,有一些偶然性的错误
为什么要有一个专门的校验器来检查这些特性呢?实际上,用 Java 语言编译器生成的类文件总是可以通过校验。然而,类文件中使用的字节码格式是有很好的文档记录的,对于具有汇编程序设计经验并且拥有十六进制编辑器的人来说,要手工地创建一个对 Java 虚拟机来说,由合法的但是不安全的指令构成的类文件,是一件非常容易的事情。再次提醒你,要记住,校验器总是在防范被故意篡改的类文件,而不仅仅只是检查编译器产生的类文件
安全管理器与访问权限
一旦某个类被加载到虚拟机中,并由检验器检查过之后, Java 平台的第二种安全机制就会启动,这个机制就是 安全管理器
权限检查
安全管理器是一个负责控制具体操作是否允许执行的类。安全管理器负责检查的操作包括以下内容:
- 创建一个新的类加载器
- 退出虚拟机
- 使用反射访问另一个类的成员
- 访问本地文件
- 打开 socket 连接
- 启动打印作业
- 访问系统剪贴板
- 访问 AWT 事件队列
- 打开一个顶层窗口
整个 Java 类库中还有许多其他类似的检查。
在运行 Java 应用程序时,默认的设置是不安装安全管理器的,这样所有的操作都是允许的。
以 java.lang.Runtime#exit
为例:
public void exit(int status) {
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkExit(status);
}
Shutdown.exit(status);
}
如果安全管理器同意了退出请求,那么 checkExit
便直接返回并继续处理下面正常的操作。但是,如果安全管理器不同意退出请求,那么 checkExit
方法就会抛出一个 SecurityException
异常
显然,安全策略的完整性依赖于谨慎的编码 标准类库中系统服务的提供者,在试图继续任何敏感的操作之前,都必须与安全管理器进行协商
Java 平台的安全管理器,不仅允许系统管理员,而且允许程序员对各个安全访问权限实施细致的控制
注意:实现并安装自己的安全管理器是可行的,但是你不应该进行这种尝试,除非你是计算机安全方面的专家。配直标准的安全管理器更加安全
Java 平台安全性
从 Java SE 1.2 开始, Java 平台拥有了更灵活的安全机制,它的 安全策略 建立了 代码来源 和 访问权限 集之间的映射关系
一个安全策略:
代码来源( code source )是由一个 代码位置 和一个 证书集 指定的。代码位置指定了代码的来源
权限( permission )是指由安全管理器负责检查的任何属性。 Java 平台支持许多访问权限类,每个类都封装了特定权限的详细信息
例如,FilePermission
类的实例表示:允许在 /tmp
目录下读取和写入任何文件
FilePermission p = new FilePermission("./tmp/*", "read,write");
更为重要的是, Policy
类的默认实现可从访问权限文件中读取权限。在权限文件中,同样的读权限表示为:
permission java.io.FilePermission "./tmp/*", "read,write";
Java SE 1.2 中提供的权限类的层次结构。JDK 的后续版本添加了更多的权限类
每个类都有一个 保护域 ,它是一个用于封装类的代码来源和权限集合的对象 Security Manager 类需要检查某个权限时,它要查看当前位于调用堆榜上的所有方法的类,然后它要获得所有类的保护域,并且询问每个保护域,其权限集合是否允许执行当前正在被检查的操作。如果所有的域都同意,那么检查得以通过。否则,就会抛出一个 SecurityException
异常
ProtectionDomain protectionDomain = String.class.getProtectionDomain();
System.out.println(protectionDomain);
/*
ProtectionDomain null
null
<no principals>
java.security.Permissions@4b67cf4d (
("java.security.AllPermission" "<all permissions>" "<all actions>")
)
*/
安全策略文件
策略管理器要读取相应的策略文件,这些文件包含了将代码来源映射为权限的指令
可以将策略文件安装在标准位置上 默认情况下,有两个位置可以安装策略文件:
- Java 平台主目录的
java.policy
文件 - 用户主目录的
.java.policy
文件 (注意文件名前面的圆点)
可以在 java.security
配置文件中修改这些文件的位置,默认位置设定为:
policy.url.1=file:${java.home}/lib/security/java.policy
policy.url.2=file:${user.home}/.java.policy
用户认证
Java API 提供了一个名为 Java 认证和授权服务的框架,它将平台提供的认证与权限管理集成起来。
JAAS 框架
Java 认证和授权服务(JAAS, Java Authentication and Authorization Service )包含两部分:“认证” ( Authentication
)部分主要负责确定程序使用者的身份,而 “授权” ( Authorization
)将各个用户映射到相应的权限
JAAS 是一个可插拔的 API ,可以将 Java 应用程序与实现认证的特定技术分离开来。除此之外, JAAS 还支持 UNIX 登录、 NT 登录、 Kerberos 认证和基于证书的认证
一旦用户通过认证,就可以为其附加一组权限
System.setSecurityManager(new SecurityManager());
try {
LoginContext context = new LoginContext("Login1");
context.login();
System.out.println("Authentication successful.");
Subject subject = context.getSubject();
System.out.println("subject=" + subject);
PrivilegedAction<String> action = new SysPropAction("user.home");
String result = Subject.doAsPrivileged(subject, action, null);
System.out.println(result);
context.logout();
} catch (LoginException e) {
e.printStackTrace();
}
这里, subject
是指已经被认证的个体
数字签名
消息摘要
消息摘要( message digest )是数据块的数字指纹。例如,所谓的 SHA1(安全散列算法制)可将任何数据块,无论其数据有多长,都压缩为 160 位( 20 字节)的序列。与真实的指纹一样,人们希望任何两条消息都不会有相同的 SHA1 指纹。当然,这是不可能的,因为只存在 2^160 SHA1 指纹,所以肯定会有某些消息具有相同的指纹
消息摘要具有两个基本属性:
- 如果数据的 1 位或者几位改变了,那么消息摘要也将改变
- 拥有给定消息的伪造者不能创建与原消息具有相同摘要的假消息
人们已经设计出大量的算法,用于计算这些消息摘要,其中最著名的两种算法是 SHA1 和 MD5 。SHA1 是由美国国家标准和技术学会开发的加密散列算法, MD5 是由麻省理工学院的 Ronald Rivest 发明的算法。这两种算法都使用了独特巧妙的方法对消息中的各个位进行
扰乱。但是,人们在这两种算法中发现了某些微妙的规律性,因此美国国家标准和技术学会建议切换到更强的加密算法上,例如 SHA-256 、 SHA-384 、SHA-512
Java 编程语言已经实现了 MD5, SHA-1, SHA-256, SHA-384 和 SHA-512 。MessageDigest
类是用于创建封装了指纹算法的对象的 “工厂”,它的静态方法 getInstance
返回继承了 MessageDigest
类的某个类的对象。这意味着 MessageDigest
类能够承担下面的双重职责:
- 作为一个工厂类
- 作为所有消息摘要算法的超类
// 获取一个能够计算 SHA 指纹的对象的方法
// 如果要获取计算 MD5 的对象,请使用字符串 "MD5" 作为 getInstance 的参数
MessageDigest alg = MessageDigest.getInstance("SHA-1");
// 在获取 MessageDigest 对象之后,可以通过反复调用 update 方法,将信息中的所有字节提供给该对象
InputStream in = ... ;
int ch;
while ((ch = in.read()) != -1) {
alg.update((byte) ch);
}
如果这些字节存放在一个数组中,那就可以一次完成整个数组的更新:
byte[] bytes = ... ;
alg.update (bytes);
当完成上述操作后,调用 digest
方法。该方法按照指纹算法的要求补齐输入,并且进行相应的计算,然后以字节数组的形式返回消息摘要
byte[] hash = alg.digest();
程序计算了一个消息摘要,既可以用 SHA ,也可以使用 MD5 来计算:
// 在线加密网站:http://www.metools.info/code/c26.html
String path = "D:\\Develop\\workspace\\study\\study-corejava\\src\\main\\java\\v2ch09\\hash\\input.txt";
String algname = "MD5"; // MD5 、 SHA-1
MessageDigest alg = MessageDigest.getInstance(algname);
byte[] input = Files.readAllBytes(Paths.get(path));
byte[] hash = alg.digest(input);
String d = "";
for (int i = 0; i < hash.length; i++) {
int v = hash[i] & 0xFF;
if (v < 16) {
d += "0";
}
d += Integer.toString(v, 16).toUpperCase() + " "; // 这里加了空格
}
System.out.println(d);
java.security.MessageDigest 方法名称 |
方法声明 | 描述 |
---|---|---|
getInstance |
public static MessageDigest getInstance(String algorithm) throws NoSuchAlgorithmException |
返回实现指定算法的 MessageDigest 对象 如果没有提供该算法,则抛出 NoSuchAlgorithmException 异常 |
update |
public void update(byte input) public void update(byte[] input) public void update(byte[] input, int offset, int len) |
使用指定的字节来更新摘要 |
digest |
public byte[] digest() |
完成散列计算,返回计算所得的摘要,并复位算法对象 |
reset |
public void reset() |
重置摘要 |
消息签名
公共密钥加密技术是基于 公共密钥 和 私有密钥 这两个基本概念的。它的设计思想是你可以将公共密钥告诉世界上的任何人,但是,只有自己才持有私有密钥,重要的是你要保护你的私有密钥,不将它泄漏给其他任何人
数字签名算法( DSA )
在现实中,几乎不可能用一个密钥去推算出另一个密钥
大多数密码学者认为,拥有 2000 位或者更多位“模数”的密钥目前是完全安全的,可以抵御任何攻击 DSA 被认为具有类似的安全性
假设 Alice 想要给 Bob 发送一个消息, Bob 想知道该消息是否来自 Alice ,而不是冒名顶替者。 Alice 写好了消息,并且用她的私有密钥对该消息摘要 签名 。Bob 得到了她的公共密钥的拷贝,然后 Bob 用公共密钥对该签名进行 校验 。如果通过了校验,则 Bob 可以确认以下两个事实:
- 原始消息没有被篡改过
- 该消息是由 Alice 签名的,她是私有密钥的持有者,该私有密钥就是与 Bob 用于校验的公共密钥相匹配的密钥
校验签名
JDK 配有一个 keytool
程序,该程序是一个命令行工具,用于生成和管理一组证书
警告:绝对不妥将你并不完全信任的证书导入到密钥库中 一旦证书添加到密钥库中,使用密钥库的任何程序都会认为这些证书可以用来对签名进行校验
jarsigner
工具负责对 JAR 文件进行签名和校验
认证问题
假设你从一个声称代表某著名软件公司的陌生人那里获得了一个消息,他要求你运行消息附带的程序 这个陌生人甚至将他的公共密钥的拷贝发送给你,以便让你校验他是否是该消息的作者。你检查后会发现该签名是有效的,这就证明该消息是用匹配的私有密钥签名的,并且没有遭到破坏
此时你要小心:你仍然不清楚谁写的这条消息。任何人都可以生成一对公共密钥和私有密钥,再用私有密钥对消息进行签名,然后把签名好的消息和公共密钥发送给你。这种确定发送者身份的问题称为 “认证问题”
解决这个认证问题的通常做法是比较简单的。假设陌生人和你有一个你们俩都值得信赖的 共同熟人 。假设陌生人亲自约见了该熟人,将包含公共密钥的磁盘交给了他。后来,你的熟人与你见面,向你担保他与该陌生人见了面,并且该陌生人确实在那家著名的软件公司工作,然后将磁盘交给你。这样一来,你的熟人就证明了陌生人身份的真实性。
事实上,你的熟人并不需要与你见面 取而代之的是,他可以将他的私有签名应用于陌生人的公共密钥文件之上即可。当你拿到公共密钥文件之后,就可以检验你的熟人的签名是否真实,由于你信任他,因此你确信他在添加他的签名之前,确实核实了陌生人的身份
然而,你们之间可能没有共同的熟人。有些信任模型假设你们之间总是存在一个 “信任链”一一即一个共同熟人的链路一一这样你就可以信任该链中的每个成员。当然,实际情况并不总是这样。你可能信任你的熟人 Alice ,而且你知道 Alice 信任 Bob ,但是你不了解 Bob ,因此你没有把握究竟是不是该信任他。其他的信任模型则假设有一个我们大家都信任的慈善大佬,在扮演这个角色的公司中,最有名的是 VeriSign 公司
然而,对于实际被认证的对象,你应该抱有一个符合实际的期望:在认证公共密钥时,VeriSign 公司的 CEO 也不会亲自去会见每个人或者公司代表。直接在 Web 页面上填一份表格,并支付少量的费用,就可以获得一个“第一类( class 1 )” ID ,包含在证书中的密钥将被
发送到指定的邮件地址。因此,你有理由相信该电子邮件是真实的,但是密钥申请人也可能填入任意名字和机构。还有其他对身份信息的检验更加严格的 ID 类别。例如,如果是“第三类( class 3 )” ID , VeriSign 将要求密钥申请人必须进行身份公证,公证机构将要核实企业申请者的财务信用资质。其他认证机构将采用不同的认证程序。因此,当你收到一条经过认证的消息时,重要的是你应该明白它实际上认证了什么
证书签名
证书授权( CA )
假设 Alice 想要给同事 Cindy 发送一条经过签名的消息,但是 Cindy 并不希望因为要校验许多签名指纹而受到困扰。因此,假设有一个 Cindy 信任的实体来校验这些签名。在这个例子中, Cindy 信任 ACME 软件公司的信息资源部
这个部门负责证书授权( CA )的运作 ACME 的每个人在其密钥库中都有 CA 的公共密钥,这是由一个专门负责详细核查密钥指纹的系统管理员安装的。CA 对 ACME 雇员的密钥进行签名,当他们在安装彼此的密钥时,密钥库将隐含地信任这些密钥,因为它们是由可信任的密钥签名的
密钥库要进行校验,以确定该密钥是由密钥库中已有的受信任的根密钥签过名的。Cindy 就不必对证书的指纹进行校验了
一旦 Cindy 添加了根证书和经常给她发送文档的人的证书后,她就再也不用担心密钥库了。
加密
到现在为止,我们已经介绍了一种在 Java 安全 API 中实现的重要密码技术, 即通过数字签名进行的认证。安全性的第二个重要方面是加密 当信息通过认证之后,该信息本身是直白可见的。数字签名只不过负责检验信息有没有被篡改过。相比之下,信息被加密后,是不可见的,只能用匹配的密钥进行解密
对称密码
Java 密码扩展 包含了一个 Cipher
类,该类是所有加密算法的超类。通过调用下面的 getInstance
方法可以获得一个密码对象:
Cipher cipher = Cipher.getInstance(algorithName);
Cipher cipher = Cipher.getInstance(algorithName, providerName);
JDK 中是由名为 “ SunJCE ” 的提供商提供密码的,如果没有指定其他提供商,则会默认为该提供商。如果要使用特定的算法,而对该算法 Oracle 公司没有提供支持,那么也可以指定其他的提供商
算法名称是一个字符串,比如 “ AES ” 或者 “ DES / CBC / PKCS5Padding ”
DES ,即数据加密标准,是一个密钥长度为 56 位的古老的分组密码。DES 加密算法在现在看来已经是过时了,因为可以用穷举法将它破译,更好的选择是采用它的后续版本,即 高级加密标准(AES)
密钥生成
为了加密,我们需要生成密钥。每个密码都有不同的用于密钥的格式,我们需要确保密钥的生成是随机的。这需要遵循下面的步骤:
- 为加密算法获取
KeyGenerator
- 用随机源来初始化密钥发生器。如果密码块的长度是可变的,还需要指定期望的密码块长度
- 调用
generateKey
方法
如何生成 AES 密钥:
KeyGenerator keygen = KeyGenerator.getInstance("AES");
SecureRandom random = new SecureRandom();
keygen.init(random);
Key key = keygen.generateKey();
或者,可以从一组固定的原生数据(也许是由口令或者随机击键产生的)中生成一个密钥,这时可以使用如下的 SecretKeyFactory
byte[] keyData = ...; // 16 bytes far AES
SecretKey key = new SecretKeySpec(keyData, "AES");
如果要生成密钥,必须使用“真正的随机”数。在 Random
类中的常规的随机数发生器,是根据当前的日期和时间来产生随机数的,因此它不够随机。SecureRandom
类产生的随机数,远比由 Random
类产生的那些数字安全得多