Java核心技术卷II(第8版) – 读书笔记 – 第9章

1、安全机制是Java不可分割的一部分,主要从以下方面:

(1)语言设计特性(越界、类型、无指针等)
(2)访问控制(文件访问、网络访问)
(3)代码签名(用加密算法表明作者身份,代码是否被修改过)

2、类加载器将在加载时检查类是否完整,它与“安全管理器”协同工作。

3、Java编译器将.java文件编译成字节码.class文件。后者必须由解析器解释后才能执行。每个Java程序至少有三个类加载器:

引导类加载器:是JVM虚拟机的一部分,通常用C实现,他们没有ClassLoader,如String类。

扩展类加载器:位于jre/lib/ext中的jar包。这样即使没有设置ClassPath,也可以找到对应的类。

系统类加载器:设置在classpath中的类,也可以jvm --classpath中加入路径。

4、加载的时候是有顺序的,当使用系统加载器时,首先要使用扩展加载类,例如java.util.ArrayList,会首先要求扩展类加载器,失败才会到系统类加载器。

有时候我们需要的类已经打包在jar中,可以用URLClassLoader:

URL url = new URL("file:///path/to/xx.jar");
URLClassLoader ld = new URLClassLoader(new URL[]{url});
Class<?> cls = ld.loadClass("xxpack.xxClass");

5、由于上述的顺序,如果在扩展加载类中含某类A,比如MySQL的驱动放在ext下了,我们Class.forname的时候,就会无视我们提供在classpath下的jar包,所以我们可以强制更改类加载器:

Thread t = Thread.currentThread();
t.setContextClassLoader(xxloader);

6、可以编写自己的类加载器:继承自ClassLoader,并覆盖方法findClass(String name)。这是有意义的:我们可以自己将.class进行加密,然后在ClassLoader中解密,这种思路可以用在付费软件上!

书上用了简单的凯撒密码加密class文件、在loadClass之前解密.class文件:

7、要编写自己的类加载器,只需要继承ClassLoader,并覆盖findClass(String className)方法。

比如我们要在加载之前实现解密,那么需要:
(1)读取本地class文件,并解密,得到bytes[](cbs)。
(2)调用defineClass()方法,尝试构造Class类:
Class<?> cls = defineClass(name, cbs, 0, cbs.length);

8、当类加载器将新加载的字节码传递给虚拟机之前,它们将被校验(verify)。

校验将负责检查那些明显错误或者具有破坏性的指令,例如:
(1)未初始化就使用的变量
(2)调用与引用类型不匹配
(3)访问私有数据
(4)堆栈溢出

9、际上,上述校验所做的工作,和编译器没有本质区别,但为什么还要这么做呢?

实际上,用Java自带编译器生成的.class文件肯定可以通过上述校验。但是在互联网这个开放环境内,你得class很可能被别人恶意修改,甚至植入后门。
如果你实在不想校验可以如下:

java -noverify XXX

10、一旦通过了校验(verify),就会启动第二道安全机制:安全管理器。它负责控制某个操作是否允许执行,主要包括但不限于:

(1)创建一个新的类加载器
(2)退出虚拟机
(3)使用反射访问另一个类成员
(4)访问本地文件
(5)打开socket连接
(6)启动打印作业
(7)访问系统剪贴板
(8)访问AWT事件队列
(9)打开一个顶层窗口

11、默认的Java程序是不安装(额外)安全管理器的。配置自己指定的安全管理器(以及策略)时要非常小心,因为他可能影响程序的正常运行。安全管理器不是凌驾于系统权限之上的。它是在系统权限和程序员执行的程序之间,加了一道屏障。

12、允许在/tmp下读写文件:

FilePermission p = new FilePermission("/tmp/*", "read,write");

此外,也可以配置策略文件(policy):

permission java.io.FilePermission "/tmp/*", "read/write";

13、当安全管理器(SecurityManager)类需要检查某个权限时,它要查看当前位于调用堆栈上的所有方法的类,然后遍历上述所有保护类,让它们判断是否允许通行。如果所有都同意,检查通过,否则,抛出SecurityException异常。

14、策略文件可以放在两个位置下:

(1)JRE主目录的java.policy下
(2)~/.java.policy,windows不知道。。

如果程序要指定自己的策略文件,可以:

System.setProperty("java.security.policy", "MyApp.policy")

或者

java -Djava.security.policy=xx.policy MyApp

上述情况是追加,即把xx.policy追加到其他策略上,我们还可以覆盖默认策略,用两个等号==即可:

java -Djava.security.policy==xx.policy MyApp

15、一个策略文件包含若干的grant项,一个项的格式如下:

grant codesource {
permission 1;
permission 2;
};

codesource可以是:

grant codeBase "www.xx.com/classes/xx.jar"

或者

grant codeBase "file:/path/to/xx.jar"

permission部分是:

permission className targetName, actionList;

className需要写全,如: java.io.SocketPermission

目标target,具体可以见JDK文档或者书,以Socket的为例

16、Socket由主机和端口范围组成

主机形式有:
hostname或者ip address 具体某个机器
localhost/空 本机
*.domainsuffex 域名下所有机器
* 所有主机

端口形式:
:n 单一端口
:n- 大于n的端口
:-n 小于n的端口
:n1-n2 位于n1到n2之间端口

权限目标:accept, connect, listen, resolve等

一个例子:
permission java.net.SocketPermission "*.hostname.com:8000-8999", "connect";

17、权限定制,我们当然也可以自己定义权限类,需要:

(1)拓展自Permission类。
并提供带有两个String参数的构造函数:
(1)第一个参数是target,第二个是action
(2)implies(Permission other)决定是否依赖于其他权限

18、Java认证和授权服务(JAAS)是1.4及以上版本提供的服务,主要用于确定程序使用者的身份,并授权给用户权限。

JAAS是可插拔的设计API,可以与上面提到的SecurityManager结合使用。

19、Java内置了消息摘要算法:MD5和SHA-1

MessageDigist alg = MessageDigist.getInstance("MD5"); // or getInstance("SHA-1")
alg.update(byteXXX); // 可以更新若干次
byte[] hash = alg.digist(); //最终的Hash结果

如果要复用,重头算,可以alg.reset()

20、Java内置了一些加密算法,可以用Cipher:

Cipher cipher = Cipher.getInstance(algoName); // algoName可以是AES/DES/CBC/PKCS5Padding

int mode = ...;
Key key = ...;
cipher.init(mode, key);

mode常用的有Cipher.DECRYPT_MODE和Cipher.ENCRYPT_MODE

然后update加密

cipher.update(xxxbytes); //更新
cipher.doFinal(); //最后完成,如果需要padding可能要调用其他doFinal()方法

一般这些padding都是需要按照块来操作的:
cipher.getBlockSize()获取每一块的大小,

21、像AES这种算法,如果Random随机生成密钥,会非常危险,我们可以用KeyGenerator完成。

一个完整的生成AES密钥、AES加密的例子如下:

import javax.crypto.*;
import java.security.SecureRandom;

public class AESTest {
    public static void main(String [] args) {
        // Data
        String data = new String("I'm string to be encrypt");
        byte [] bd = data.getBytes();
        try {
            // Generate key
            KeyGenerator keygen = KeyGenerator.getInstance("AES");
            SecureRandom random = new SecureRandom();
            keygen.init(random);
            SecretKey key = keygen.generateKey();
            // Cipher encrypt
            Cipher c1 = Cipher.getInstance("AES");
            c1.init(Cipher.ENCRYPT_MODE, key);
            //c1.update(bd);
            byte [] crypt_data = c1.doFinal(bd);
            // Cipher decrypt
            Cipher c2 = Cipher.getInstance("AES");
            c2.init(Cipher.DECRYPT_MODE, key);
            //c2.update(crypt_data);
            String dstr = new String(c2.doFinal(crypt_data));
            System.out.println(dstr);
        } catch(Exception e) {
            e.printStackTrace();
        }   
    }   
}

22、上面这种还会比较复杂,特别是需要Padding的时候很麻烦。

JDK提供了CipherInputStream和CipherOutputStream,用于对数据进行加密,它会透明地调用update()和final(),自动处理块的问题,非常方便。
下面是一个例子,由于流可以和其他各种包装起来用,瞬间就很简单了:

import javax.crypto.*;
import java.io.*;
import java.util.*;
import java.security.SecureRandom;

public class CipherStreamTest {
	public static void main(String [] args) {
		// Filename 
		String file = "./crypt.dat";
		CipherOutputStream cout = null;
		CipherInputStream cin = null;
		try {
			// Generate key
			KeyGenerator keygen = KeyGenerator.getInstance("AES");
			SecureRandom random = new SecureRandom();
			keygen.init(random);
			SecretKey key = keygen.generateKey();
			// Cipher encrypt init
			Cipher c1 = Cipher.getInstance("AES");
			c1.init(Cipher.ENCRYPT_MODE, key);
			// CipherOutputStream
			cout = new CipherOutputStream(new
					FileOutputStream(file), c1);
			cout.write("我是被AES加密的!\r\n".getBytes());
			cout.write("你能看懂么?".getBytes());
			cout.flush();
			cout.close();
			// Cipher decrypt init
			Cipher c2 = Cipher.getInstance("AES");
			c2.init(Cipher.DECRYPT_MODE, key);
			// CipherInputStream
			cin = new CipherInputStream(new
					FileInputStream(file), c2);
			Scanner scan = new Scanner(cin);
			while(scan.hasNext()) {
				String str = scan.next();
				System.out.println(str);
			}
			scan.close();
			cin.close();
		} catch(Exception e) {
			e.printStackTrace();
		}
	}
}

23、Java也内置了非对称密码(公钥) 算法。但是一般来说,我们不会用RSA加密大段文本,而是用RSA的公钥加密对称密钥(如一个AES密钥),然后用户持有私钥,先解密出AES密码,再解密正文。

如下是用RSA包裹AES密钥的方法

KeyPairGenerator pairgen = KeyPairGenerator.getInstance("RSA");
SecureRandom random = new SecureRandom();
pairgen.initialize(KEYSIZE, random); //KEYSIZE
KeyPair kp = pairgen.generateKeyPair();
Key publicKey = kp.getPublic();
Key privateKey = kp.getPrivate();

24、下面是从RSA加密中恢复AES密钥:

Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.WRAP_MODE, publicKey);
byte [] wrapped_data = cipher.wrap(data); //加密

解密类似,只不过用的是unwrap()方法:

Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.UNWRAP_MODE, privateKey);
byte [] wrapped_data = cipher.unwrap(data, "AES", Cipher.SECRET_KEY); //解密

本章结束。

Leave a Reply

Your email address will not be published. Required fields are marked *