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); //解密
本章结束。