java之jvm学习笔记六-十二(实践写自己的安全管理器)(jar包的代码认证和签名) (实践对jar包的代码签名) (策略文件)(策略和保护域) (访问控制器) (访问控制器的栈校验机制) (jvm基本结构)

时间:2022-03-20 03:03:53

java之jvm学习笔记六(实践写自己的安全管理器)

安全管理器SecurityManager里设计的内容实在是非常的庞大,它的核心方法就是checkPerssiom这个方法里又调用 AccessController的checkPerssiom方法,访问控制器AccessController的栈检查机制又遍历整个 PerssiomCollection来判断具体拥有什么权限一旦发现栈中一个权限不允许的时候抛出异常否则简单的返回,这个过程实际上比我的描述要复杂 得多,这里我只是简单的一句带过,因为这里涉及到很多比较后面的知识点。

下面来尝试一下写一个非常简单的demo,旨在让你有一个比较形象的思维,不会在概念上打转。

第一步,定义一个类继承自SecurityManger重写它的checkRead方(如果你有兴趣可以先跳到super.checkRead(file, context);看看,当然不看也没有关系,我们后面的章节会基于这个demo做扩展的时候也会讲到)。

  1. package com.yfq.test;
  2. public class MySecurityManager extends SecurityManager {
  3. @Override
  4. public void checkRead(String file) {
  5. //super.checkRead(file, context);
  6. if (file.endsWith("test"))
  7. throw new SecurityException("你没有读取的本文件的权限");
  8. }
  9. }

第二步,定义一个有main函数的public类来验证自己的安全管理器是不是器作用了。

  1. package com.yfq.test;
  2. import java.io.FileInputStream;
  3. import java.io.IOException;
  4. public class TestMySecurityManager {
  5. public static void main(String[] args) {
  6. System.setSecurityManager(new MySecurityManager());
  7. try {
  8. FileInputStream fis = new FileInputStream("test");
  9. System.out.println(fis.read());
  10. } catch (IOException e) {
  11. e.printStackTrace();
  12. }
  13. }
  14. }

第三步,运行代码查看控制台输出

Exception in thread "main" java.lang.SecurityException: 你没有读取的本文件的权限
 at com.yfq.test.MySecurityManager.checkRead(MySecurityManager.java:9)
 at java.io.FileInputStream.<init>(FileInputStream.java:100)
 at java.io.FileInputStream.<init>(FileInputStream.java:66)
 at com.yfq.test.TestMySecurityManager.main(TestMySecurityManager.java:10)

从上面的异常我们发现,安全管理器起作用了。读过笔记四的人应该会发现,这里我们用到
了一个笔记四提到的方法:System.setSecurityManager(new
MySecurityManager());这个是安装安全管理器的另外一种方法,笔记四中我们曾经用-Djava.security.manager安
装过默认的安全管理器,有印象不?

拓展内容

好了,我们的安全管理器是怎么被执行的呢?如果你有兴趣可以继续往下看一下,也可以跳过,这里只是简单的介绍一下,也是本人习惯的学习思路

直接跳到FileInputStream的构造函数里,下面贴出代码,简单阅读一下

  1. public FileInputStream(File file) throws FileNotFoundException {
  2. String name = (file != null ? file.getPath() : null);
  3. SecurityManager security = System.getSecurityManager();
  4. if (security != null) {
  5. security.checkRead(name);
  6. }
  7. if (name == null) {
  8. throw new NullPointerException();
  9. }
  10. fd = new FileDescriptor();
  11. open(name);
  12. }

发现没?它首先执行SecurityManager security = System.getSecurityManager();,然后再调用security的checkRead方法,就是这么简单。

如果你还有兴趣那么继续往下读,在使用java的File时,你是否用过setWritable(boolean,
boolean),让你可以指定创建文件的权限,学习了安全管理器之后你有没有有豁然开朗的感觉,它是怎么实现的,相信你已经猜到了,没有错就是安全管理
器设置权限啦。下面贴出它的代码,同时也引入一个新的概念Permission

  1. public boolean setWritable(boolean writable, boolean ownerOnly) {
  2. SecurityManager security = System.getSecurityManager();
  3. if (security != null) {
  4. security.checkWrite(path);
  5. }
  6. return fs.setPermission(this, FileSystem.ACCESS_WRITE, writable, ownerOnly);
  7. }

Permisson就是权限的意思,它仅仅取出安全管理器然后将文件的权限设置了一下而已,这个也是后面所有关于权限的一个不可或缺的类!

好了今天的文件安全管理器demo就到这里。意在浅显易懂!

java之jvm学习笔记七(jar包的代码认证和签名)

前言:

如果你循序渐进的看到这里,那么说明你的毅力提高了,jvm的很多东西都是比较抽像的,如果不找相对应的代码来辅助理解,其实很难有个比较形象的思维,前 面我努力的尝试通过将概念投射到代码的方式去讲解jvm的各个细节,希望你能够试着自己也去找到对应的代码段,然后试着读一读。一开始可能没有那么容易, 但是没有一件事情,一开始就是容易的。

代码认证与签名

终于到了这一节,这一节,其实相对于笔记二,笔记三和笔记四,是相对比较容易的,即使你对密码编码学一窍不通也不妨碍你学习,我们不会涉及到太多的实现,而主要从应用着手,旨在浅显易懂,触类旁通。在下一节中,我们会来尝试做一次签名,前提是你看完这一节

笔记3的时候我们曾经提到class文件的校验器,记得它分为几趟不,四趟,而jar包的代码签名认证和class检验的第一趟是有联系的。

class文件校验器的第一趟会对jar文件的结构,长度等进行校验,其中也包括对jar的签名和认证进行校验。

那么什么是jar包的签名和认证?

我们相关的class文件打包成了jar包之后,在传递这个jar的时候,如何防止jar不被他人暗中的修改呢?

方案一,可能你会想到对整个jar文件进行加密,这个思路是可行的,但是却显得比较笨拙,对每个jar文件都执行加密,需要的时候又要执行解密,不仅浪费时间,效率上也是不可取的。

方案二。对jar包的部分内容进行加密,这个思路好像效率高点,但是对哪一部分进行加密?如果没有加密的那一部分被修改了怎么确认?这又一个问题。

以上两种简单地解决方案虽然看起来简单但是实施起来都是有困难的,那么有没有好的方法?

有,在jar文件上hash摘要,什么是hash摘要,这里我不丢书包了,简单的说hash摘要就是有一个叫hash(String content)的哈希函数,当你传入内容的时候它都将返回一个独一无二个的128的hash数值,这样无论传入的内容多大,hash摘要的长度是固定 的。当然附加到jar文件的最后面时总体上并不会影响jar的结构和传输。

只要接收方也拥有这个hash函数,那么将jar的内容进行hash后的值再和附加在jar中的hash值做对比就可以知道jar的内容是否被修改过了, 看起来好像完美了,但是如果有意破坏的人把jar和hash都替换成具有破坏性ar文件以及由这个具有破坏性的jar文件进行hash运算的hash值, 那么前面做的事情也就都没有意义了,于是聪明的人类想到了对hash摘要运用私钥进行加密,这样只有加密方才能对hash值加密,而解密的那方运用公钥进行解密,而且它总是知道怎么解密的,我们把对hash摘要进行加密的过程称之为签名。这就是jar包签名的大致过程

                好吧,上面引述了那么多,无非是想描述下面图3-3的过程,如果你看到这个图完全明白,那前面那段废话就直接跳过吧!

              java之jvm学习笔记六-十二(实践写自己的安全管理器)(jar包的代码认证和签名) (实践对jar包的代码签名) (策略文件)(策略和保护域)  (访问控制器) (访问控制器的栈校验机制) (jvm基本结构)

接下来还有一个概念需要你理解----认证

                 先不管什么是认证,先来了解一下密码学的一点小知识

前面我说过,看这篇文章是不需要你有密码学的知识的,是的,我骗你,至少基本的概念还是要理解过的。如果你完全不懂,不要慌,我举个简单的例子来帮你简单的理解一下一两个基本的概念。

第一个概念对称加密,什么是对称加密?假设A想要说暗语,A想说5的时候就把5*3,然后把5*3的结果15告诉B,因为B知道A说暗语的规则,所以B就把15除以3,知道A要告诉自己5,这就是对称加密。

第二个概念非对称加密, 假设A要把一句话告诉B,A就把这句话放到一个有两 个完全不同的锁(lock1,lock2)的箱子里,然后锁上,A有lock1的钥匙,把箱子交给B,而B拥有lock2的钥匙,B通过打开lock2也 能看到箱子里的字条,这就是非对称加密。而A拥用的那把钥匙叫私要,B拥有的那把钥匙复制多份之后分给他们组员,就成了公钥。

没有那么可怕对吧!而在这里我应该负责任的告诉你,对于hash摘要的签名用的就是非对称加密!

回 到我们的主题,什么是认证,当我们队hash摘要用私钥进行加密,然后把公钥发给B和B组里的所有人的时候, 如果中间传递的环节被人偷天换日的将公钥换掉了,这个时候,jar文件的签名的真实性又受到了威胁,怎么保证传递公钥的时候,公钥的真实性,这就是我们提 到的认证,我们如果把公钥交给一个公正的认证机构,认证机构对你的公钥进行加密之后的序列号,我们就称为证书,需要公钥的人得带证书后向认证机构申请解 密,这样安全性就好很多了。

上面的一堆废话,其实也是为了描述下面这个图的整个过程,如果你一眼就看明白下面这个图,那就忽略上面的描述吧

java之jvm学习笔记六-十二(实践写自己的安全管理器)(jar包的代码认证和签名) (实践对jar包的代码签名) (策略文件)(策略和保护域)  (访问控制器) (访问控制器的栈校验机制) (jvm基本结构)

好吧,这一节的内容全是概念,概念只需要你看而不是要你背,在某个时候你会焕然大悟的,而这个时间应该会是在下一节java之jvm学习笔记八(实践对jar包进行签名)

java之jvm学习笔记八(实践对jar包的代码签名)

课程源码:http://download.csdn.net/detail/yfqnihao/4866500

这一节,以实践为主,在跟着我做相应的操作之前,我希望你已经能够理解笔记七所提到的概念,至少你应该对于笔记七的那个大图有所了解。

好了!对于习惯用ecplise的朋友今天不得不逼迫你把jdk的环境搭建出来!下面让我们动手来实践一下对jar进行签名吧!

第一步,首先配置jdk的环境变量,如果你的电脑已经配置了,那直接跳过这一步

  1. path=%JAVA_HOME%/bin
  2. JAVA_HOME=C:/Java/jdk1.6.0_01
  3. CLASSPATH=.;%JAVA_HOME%/lib/dt.jar;%JAVA_HOME%/lib/tools.jar

配置要这几个jdk的环境参数,好了,配完了,试着在cmd里跑一下java,javac,看看命令是否生效,如果配置成功执行第二步。

第二步,来写几个简单的类,简单的才是大家的。你完全可以直接copy我的代码,部分看不懂,忽略它,做实验而已,对那个jar文件签名不是签,这个例子的代码逻辑是后面才用到的,不用读

第一个类Doer

  1. package com.yfq.test;
  2. public abstract interface Doer {
  3. void doYourThing();
  4. }

第二个类

  1. package com.yfq.test.friend;
  2. import java.security.AccessController;
  3. import java.security.PrivilegedAction;
  4. import com.yfq.test.Doer;
  5. public class Friend implements Doer{
  6. private Doer next;
  7. private boolean direct;
  8. public Friend(Doer next,boolean direct){
  9. this.next=next;
  10. this.direct=direct;
  11. }
  12. @Override
  13. public void doYourThing() {
  14. System.out.println("Im a Friend");
  15. if (direct) {
  16. next.doYourThing();
  17. } else {
  18. AccessController.doPrivileged(new PrivilegedAction() {
  19. @Override
  20. public Object run() {
  21. next.doYourThing();
  22. return null;
  23. }
  24. });
  25. }
  26. }
  27. }

第三个类

  1. package com.yfq.test.stranger;
  2. import java.security.AccessController;
  3. import java.security.PrivilegedAction;
  4. import com.yfq.test.Doer;
  5. public class Stranger implements Doer {
  6. private Doer next;
  7. private boolean direct;
  8. public Stranger(Doer next, boolean direct) {
  9. this.next = next;
  10. this.direct = direct;
  11. }
  12. @Override
  13. public void doYourThing() {
  14. System.out.println("Im a Stranger");
  15. if (direct) {
  16. next.doYourThing();
  17. } else {
  18. AccessController.doPrivileged(new PrivilegedAction() {
  19. @Override
  20. public Object run() {
  21. next.doYourThing();
  22. return null;
  23. }
  24. });
  25. }
  26. }
  27. }

好了,编译一下,用强大的ecplise来编译,项目-右键-Build Project(工具是拿来用的,不要浪费这些强大的功能!)

第三步,打jar包,用ecplise就可以了就有导出jar包的功能,我还是那句老话,有工具不用,不是牛,是蠢。

步骤一,项目-右键-Export-java-JAR file-next

步骤二,展开目录清单-分别对com.yfq.tes.friend和
com.yfq.test.stranger打包(friend.jar,stranger.jar),放到哪里就随便你了,只要你记得就好,我这里假设
是放在d盘的根目录下

第四步,用java的keytool生成密钥对,用java的jarsigner做签名(记得笔记七我们说过对hash摘要的加密是非对称加密的吗?这里就需要两把不同的钥匙啦),一步步跟我来。

步骤一,cmd窗口,进入到存放friend.jar和stranger.jar的目录下,假设我的jar文件放在d盘下,直接输入盘符d:就可以了。

步骤二,在cmd窗口中输入keytool -genkey -keystore
ijvmkeys.keystore -keyalg RSA -validity 10000 -alias friend.keystore

生成第一个密钥对,这个密钥对的别名是
friend.keystore,采用的加密算法为RSA,密钥对的过期时间是10000天,密钥对存储的文件名ijvmkeys.keystore,而
查看ijvmkeys.keystore的密码和friend.keystore密钥对的查看密码我们设置为123456

java之jvm学习笔记六-十二(实践写自己的安全管理器)(jar包的代码认证和签名) (实践对jar包的代码签名) (策略文件)(策略和保护域)  (访问控制器) (访问控制器的栈校验机制) (jvm基本结构)

注意:这里在设置名字和姓氏的时候要特别的注意,不要随便的乱写,否则将导致后面的签名失败,一般我们写完网络域名的形式如:www.keycoding.com这样。

步骤三,在cmd窗口输入,keytool -genkey -keystore
ijvmkeys.keystore -keyalg RSA -validity 10000 -alias stranger.keystore

按照步骤2的截图,一步一步输入吧,这个步骤是生成别名为stranger.keystore的密钥对。

好了密钥对生成结束,看看你的jar文件目录下有没有多出一个文件ijvmkeys.keystore,是滴,这里生成了一个用于存放密钥对的文件。

步骤四,查看生成的密钥文件,在cmd窗口输入keytool -list -v -keystore ijvmkeys.keystore

java之jvm学习笔记六-十二(实践写自己的安全管理器)(jar包的代码认证和签名) (实践对jar包的代码签名) (策略文件)(策略和保护域)  (访问控制器) (访问控制器的栈校验机制) (jvm基本结构)

步骤五,对jar进行摘要并对hash摘要进行加密生成签名,放置到jar文件结构的尾部

在cmd窗口输入 
                                           jarsigner -verbose -keystore ijvmkeys.keystore friend.jar friend.keystore
                                           jarsigner -verbose -keystore ijvmkeys.keystore stranger.jar stranger.keystore

java之jvm学习笔记六-十二(实践写自己的安全管理器)(jar包的代码认证和签名) (实践对jar包的代码签名) (策略文件)(策略和保护域)  (访问控制器) (访问控制器的栈校验机制) (jvm基本结构)

步骤六,右键frend.jar和stranger.jar用rar解压器看看它们在META-INF目录下是否生成了两个附加的文件

java之jvm学习笔记六-十二(实践写自己的安全管理器)(jar包的代码认证和签名) (实践对jar包的代码签名) (策略文件)(策略和保护域)  (访问控制器) (访问控制器的栈校验机制) (jvm基本结构)

而关于这两个附加文件的用处,我这里也简单的说明一下,首先从名字上来讲他是八个字符,他默认取我们的密钥对的名字的前八个字符做名字而因为我们的
密钥对名字是friend.keystore所以生成的名字将点替换为下滑线。如果你想要自己指定名字在keytool后面加上-sigFile
XXXX这个参数

另外FRIEND_K.SF这个文件我们简单的展开

  1. Signature-Version: 1.0
  2. SHA1-Digest-Manifest-Main-Attributes: QHukAYw2MtCop4vlrhjJDDro1fQ=
  3. Created-By: 1.6.0_12 (Sun Microsystems Inc.)
  4. SHA1-Digest-Manifest: YePdyFc1+FVdY1PIcj6WVuTJAFE=
  5. Name: com/yfq/test/friend/Friend$1.class
  6. SHA1-Digest: mj79V3+YKsRAzxGHpyFGhOdY4dU=
  7. Name: com/yfq/test/friend/Friend.class
  8. SHA1-Digest: tqPfF2lz4Ol8eJ3tQ2IBvvtduj0=

它包含了签名的版本,签名者,还有被签名的类名,以及这个类的hash摘要,第四行是整个本文件的摘要,用于jar包的校验
FRIEND_K.DSA 文件,SF 文件被签名且签名被放入 .DSA 文件。.DSA 文件还包含来自密钥仓库的证书或证书链(被编码到其中),它们鉴别与用于签名的私钥对应的公钥。

步骤七,校验jar包在cmd中输入jarsigner -verify friend.jar和jarsigner -verify stranger.jar

java之jvm学习笔记六-十二(实践写自己的安全管理器)(jar包的代码认证和签名) (实践对jar包的代码签名) (策略文件)(策略和保护域)  (访问控制器) (访问控制器的栈校验机制) (jvm基本结构)

到这里jar签名的实验已经完毕!!!!!

查看上面步骤四截图,我们来验证一下在笔记七里说过的话。

1.我们说过hash摘要是一个128的值,对不对呢,看证书指纹那一行,md5:....

你数一数总共有几个十六进制数,32个,一个十六进制数用4个位可以表示完,那么总共是几位,32*4=128,但是后面还有一个sha1的,怎么
回事他貌似不止128位,是滴,散列函数多种多样,到底用那个散列函数,md5还是sha1这个就看你喜欢,而要使用哪个散列函数是可以指定
的,keytool的参数-keyalg "DSA",这个参数就是用来指定用什么散列算法的,默认的就是DSA,普通的128位散列数已经是安全的了。

2.在 笔记七中,记不记得最下面那个图,有一个认证机构会对解密签名(被加密的hash摘要)的公钥做认证(也就是加密公钥),并发布证书,我们这里没有认证机构,你有没有这个疑问?

keytool程序在生成密钥时,总是会生成一个自签名证书(自签名是指:如果附近没有认证机构,可以用私钥对公钥签名,生成一个自签名证书)

总结:

通过本章我们学习对一个jar进行签名,一个jar可以同时被多个机构或作者签名,看起来实验很复杂其实很简单。如果你还想了解更多关于jar包签名的知识,本人在这里推荐一篇文章(http://blog.csdn.net/yangxt/article/details/1796965),本人自己在学习jar包签名的时候也从这篇文章中收益匪浅,希望它对你有帮助。

java之jvm学习笔记九(策略文件)

什么是java的策略,什么又是策略文件。

今天我换一下笔记的方式,不是直接讲概念,而是先来做一个小例子,相信你做完这个例子之后再看我对例子的讲解,你对策略,策略文件,会豁然开朗的感觉。

例子很简单,简单的才是大家的,下面跟着我(你完全可以copy我的代码)。

第一步,定义一个简单类。

  1. package com.yfq.test;
  2. import java.io.FileWriter;
  3. import java.io.IOException;
  4. public class TestPolicy {
  5. public static void main(String[] args) {
  6. FileWriter writer;
  7. try {
  8. writer = new FileWriter("d:/testPolicy.txt");
  9. writer.write("hello1");
  10. writer.close();
  11. } catch (IOException e) {
  12. e.printStackTrace();
  13. }
  14. }
  15. }

第二步,定义一个简单的策略文件,我们放到工程的类路径下(src文件夹里),名字为myPolicy.txt

  1. grant codeBase "file:D:/workspace/TestPolicy/bin/*" {
  2. permission java.io.FilePermission "d:/testPolicy.txt", "read";
  3. };

我简单的来说一下这个文件的作用

第一行:grant codeBase "file:D:/workspace/TestPolicy/bin/*"意思是给D:/workspace/TestPolicy/bin/*给这个路径下的所有文件定义权限,星号是统配符,所有的意思

第二行:permission java.io.FilePermission "d:/testPolicy.txt", "read";意思是d:/testPolicy.txt这个文件 只分配读的权限。

第三步,运行,在cmd窗口输入(运行不起来,说明jdk的环境变量没有配置好,去配一下)
java -classpath D:/workspace/TestPolicy/bin -Djava.security.manager
-Djava.security.policy=D:/workspace/TestPolicy/src/myPolicy.txt
com.yfq.test.TestPolicy

这句话的意思,把当前的类路径指定为D:/workspace/TestPolicy/bin,启动默认的安全管理器(这里你应该也猜到了,策略必
须和安全管理器一起合作才能起作用),设置安全策略文件的位置(关于策略文件的安装是有多种方式的,这里我们是在windows下,如果你有兴趣可以自己
再多摸索)。

第四步,查看输出

java之jvm学习笔记六-十二(实践写自己的安全管理器)(jar包的代码认证和签名) (实践对jar包的代码签名) (策略文件)(策略和保护域)  (访问控制器) (访问控制器的栈校验机制) (jvm基本结构)

这里报出了异常,提示本应用对d:/testPolicy.txt这个文件没有写的权限。

修改一下上面的myPolicy.txt文件,如下

  1. grant codeBase "file:D:/workspace/TestPolicy/bin/*" {
  2. permission java.io.FilePermission "d:/testPolicy.txt", "read,write";
  3. };

再次运行,没有报错了。

好了实验成功,或许你会疑问,这个有鸟用啊,不要急,在下一节中,我们会详细的讲,现在我做一下简单的介绍,这个策略文件(本文中为
myPolicy.txt)在java中对应着一个类,叫java.security.Policy(策略),这是一个神奇的类,有了它,你可以定义自己
代码的权限,当然它还可以结合我们笔记四讲到的安全管理器。而你现在只需要记住一句话:

java对应用程序的访问控制策略是由抽象类java.security.Policy的一个子类的单例所表示,任何时候,每个应用程序实际上只有一个Policy对象,Policy对象对应着策略文件。类装载器利用这个Policy对象来帮助他们决定,在把一段代码导入虚拟机时应该给予什么权限。

如果你之前有稍微听过策略这个概念,希望看完本文有给你豁然开朗的感觉

java之jvm学习笔记十(策略和保护域)

前面一节,我们做了一个简单的实验,来说明什么是策略文件,在文章的最后,也顺带的讲了一下什么是策略,还有策略的作用。

为了引出另外一个很重要的概念ProtectionDomain(保护域),所以我们还是要先来回顾一下什么是策略

                        首先,什么是策略,今天的东西纯粹是比较概念的。当然,如果你读过笔记九,今天的东西,就真的是soso

策略与策略文件:

java对应用程序的访问控制策略是由抽象类java.security.Policy的一个子类的上面那段话告诉我们一个应用程序对应一个策略对象,一个策略对象对应一个策略文件。

                        那么策略文件,除了对我们笔记九中一个文件夹下的所有文件起限制作用外还能对什么主体起作用呢?先来看看下面的策略文件myPolicy.txt

  1. keystore "ijvmkeys";
  2. grant signedby "friend" {
  3. permission java.io.FilePermission "d:/testPolicy.txt", "read,write";
  4. };
  5. grant signedby "stranger" {
  6. permission java.io.FilePermission "d:/testPolicy.txt", "read,write";
  7. };
  8. grant codeBase "file:D:/workspace/TestPolicy/bin/*" {
  9. permission java.io.FilePermission "d:/testPolicy.txt", "read,write";
  10. };

简单的解读一下

第一行:keystore "ijvmkeys",这一行的意思,密钥对存放在当前目录一个叫ijvmkeys的文件里(记得笔记八做过的jar包签名实验吗)

第二行:grant signedby "friend",grant是授权的意思,这一行的意思是,给一个被“friend”的密钥对签名的文件授权

第三行:permission java.io.FilePermission "d:/testPolicy.txt", "read,write";这行的意思是对于d:/testPolicy.txt赋予读写的权限

倒数第三行:grant codeBase "file:D:/workspace/TestPolicy/bin/*" 这一句我们笔记九的时候见过,就是对D:/workspace/TestPolicy/bin/*下的所有文件赋予权限。

重点一:到这里我们应该可以知道,策略文件可以给一系列被签名的代码库(“friend”,‘stranger“都是代码库)授权,也可以给一个代码来源(一个具体的路径或者说url就是一个代码来源)授权。

重点二:策略文件不仅可以存储在文件中(后缀名是什么不重要),还可以存放在数据库里。

到了这里我们对策略有一个比较完整的概念了,但是你有没有这么一个疑问,前面我们总说,一个应用程序对应一个策略单例,一个策略单例对应一个策略文件,它到底怎么对应的?下面我们就来探究一下。

在探究之前,我们先引入一个新的概念叫保护域(ProtectionDomain),在笔记三的时候,我们提到过类装载器将class文件load内存的时候会将它放置到一个保护域中,是滴今天我就来说说什么是保护域。

              什么是保护域

当类装载器将类型装入Java虚拟机时,它们将为每个类型指派一个保护域。保护域定义了授予一段特定代码的所有权限。(一个保护域对应策略文件中的一个或多个Grant子句。)装载入Java虚拟机的每一个类型都属于一个且仅属于一个保护域。

类装载器知道它装载的所有类或接口的代码库和签名者。它利用这些信息来创建一个CodeSource对象。它将这个CodeSource对象传递个当前 Policy对象的getPermissions()方法,得到这个抽象类java.security.PermissionCollection的子类 实例。这个PermissinCollection包含了到所有Permission对象的引用(这些Permission对象由当前策略授予指定代码来 源)。利用它创建的CodeSource和它冲Policy对象得到的PermissionCollection,它可以实例化一个新的 ProtectDomain对象。它通过将合适的ProtectionDomain对象传递给defineClass()方法,来将这段代码放到一个保护 域中

如果你对上面这段话理解不了,看下面这个图

java之jvm学习笔记六-十二(实践写自己的安全管理器)(jar包的代码认证和签名) (实践对jar包的代码签名) (策略文件)(策略和保护域)  (访问控制器) (访问控制器的栈校验机制) (jvm基本结构)

好了看完上面的这整个过程之后你是否已经理解什么是保护域了。

下面我们再整理一下今天的内容,概念有点多,一个一个的来。

codeSource:代码源,这个是类装载器生成的java.security.CodeSource的一个 对象,classLoader通过读取class文件,jar包得知谁为这个类签过名(可以有多个签名者,关于签名请查看笔记七和八)而封装成一个签名者 数组赋给codeSource对象的signers成员,通过这个类的来源(可能来自一个本地的url或者一个网络的ur,对应了grant笔记九里 myPollicy里的"friend"或者file::....l)赋给codeSource的location成员,还有这个类的公钥证书赋给 codeSource的certs成员(通常一个jar是能够被多个团体或者机构担保的,也就是我们说的认证,在java1.2的默认安全管理器还有访问 控制体系结构都只能对证书起作用,而不能对赤裸的公钥起作用,而实际上,我们用keytool生成密钥对时,同时会生成一个自签名证书,所以 keytool生成的密钥对并不是赤裸的)。如果你有疑问,我们看一下jdk里的代码

  1. public class CodeSource implements java.io.Serializable {
  2. private static final long serialVersionUID = 4977541819976013951L;
  3. /**
  4. * The code location.
  5. *
  6. * @serial
  7. */
  8. private URL location;//本地代码库
  9. /*
  10. * The code signers.
  11. */
  12. private transient CodeSigner[] signers = null;//签名者
  13. /*
  14. * The code signers. Certificate chains are concatenated.
  15. */
  16. private transient java.security.cert.Certificate certs[] = null;//证书

Policy:策略,就是用来读取策略文件的一个单例对象,通过传入的CodeSource对象(由于
codeSource对象里包含了签名者和代码来源)所以他通过读取grant段,取出一个个的Perssiom然后返回一个
PerssiomCollection。这个类里有一个很重要的成员变量

  1. // Cache mapping  ProtectionDomain to PermissionCollection
  2. private WeakHashMap pdMapping;

这个成员为什么重要,我们来看一个方法

  1. private static void initPolicy (final Policy p) {
  1. ......
  1. if (policyDomain.getCodeSource() != null) {
  2. .......
  3. synchronized (p.pdMapping) {
  4. // cache of pd to permissions
  5. p.pdMapping.put(policyDomain, policyPerms);
  6. }
  7. }
  8. return;
  9. }

我们主要看关键代码。这个pdMapping就是把保护域对象当做key将权限集合当做value存在在了这个map里。所以我们说一个保护域对应多个策略文件的grant子句的permission。

ProtectionDomain:保护域,前面我们已经介绍过了,他就是用来容纳class文件,还有perssiom,codeSource的一个对象,如果你对此还有什么疑问,我们也看看它的代码,来验证一下我们的结论

  1. public class ProtectionDomain {
  2. /* CodeSource */
  3. private CodeSource codesource ;//代码源
  4. /* ClassLoader the protection domain was consed from */
  5. private ClassLoader classloader;//类装载器
  6. /* Principals running-as within this protection domain */
  7. private Principal[] principals;
  8. /* the rights this protection domain is granted */
  9. private PermissionCollection permissions;//权限集合

Permission:权限,这个对应了我们笔记九里的grant子句里的一个permission,它的结构也很简单,权限名和动作,就好像我们笔记九里的java.io.FilePermission是一个权限名

而动作则是read和write,在Permission中它对应一个字符串。

现在我们用一张图来把上面几个概念串联起来

java之jvm学习笔记六-十二(实践写自己的安全管理器)(jar包的代码认证和签名) (实践对jar包的代码签名) (策略文件)(策略和保护域)  (访问控制器) (访问控制器的栈校验机制) (jvm基本结构)

到这里我们已经有一条比较完整的思路了,从笔记四到这一节的笔记十,我们所要说的都只有一件事情,类装载器在装载类的时候(或者执行类)会调用安全
管理器,安全管理器,则通过判断策略来判断我们是不是允许加载这个类,或者执行某些操作,允许某个文件的读写啊之类的(这个在笔记九的时候我们已经做过实
验了)。那么你有没有这样的疑问,到底安全管理器是怎么样去调用策略的?这里我们不得不提出一个新的概念访问控制器AccessControl,如果你想
知道访问控制器是干什么的,做什么工作,怎么和安全管理进行合作,那么请你阅读下一节。

java之jvm学习笔记十一(访问控制器)

这一节,我们要学习的是访问控制器,在阅读本节之前,如果没有前面几节的基础,对你来说可能会比较困难!

本节实验源码下载:http://download.csdn.net/detail/yfqnihao/4863854

知识回顾:

我们先来回顾一下前几节的内容,在笔记三的时候我们学了类装载器,它主要的功能就是装载类,在装载的前后,class文件校验器会对class文件进行四趟的校验,而第一趟的校验会对文件的结构进行校验,对文件的结构完整性的校验时会校验class文件的hash摘要是否一致以确定文件没有中途被修改过,所以基于class文件校验我们又学习了jar的认证和签名,当class文件被装载到内存的时候,一个应用启动时,jvm会为该应用生成一个Policy的单例对象,它用于读取策略文件的grant信息,当类装载器装载一个类的时候,它根据jar包中的签名信息、证书、jar的url信息生成一个CodeSource对象,CodeSource对象向Policy对象索要一个PermissionCollecion权限集合,它是由各个grant子句中的permission语句的实例映射,再由CodeSource对象、PermissionCollecion权限集合、类加载器交由类加载器的defineClass方法组成了ProtectionDomain保护域。最后class字节码在内存中被放在了这个保护域中。

是的内容非常的多,概念也非常的多,所以如果你对前面的知识回顾一头雾水,建议还是倒回去把那些基础的概念再补一补。

回顾完目前为止的所有知识之后,我们需要解决两个问题

第一,什么是访问控制器。

第二,它是怎么样和安全管理器配合工作的。

我们先来简单的回答第一个问题,你可以听不明白,但是如果你耐性的往下看,在我回答第二个问题的时候,我们会做几个比较复杂的demo,而这些复杂的 demo,会在无形之中让你真正的认识到什么是访问控制器。在文章的最后如果篇幅够的话我们也会带大家来读一读jdk里的源码,看看他和安全管理是怎么配 合工作的。

那么什么是访问控制器?

类java.security.AccessControler提供了一个默认的安全策略执行机制,他使用栈检查机制来决定潜在的不安全操作是否被允许。这 个访问控制器不能够被实例化,它不是一个对象,而是集合在单个类的多个静态方法。AccesControler最核心的方法是 checkPermission,这个方法决定一个特定的操作是否被允许,他接收一个Perssmission的子类对象,当 AccessControler确定操作被允许,它将简单的返回,而如果操作被禁止,它将异常中止,并抛出一个 AcssessControlException,或者是它的子类。

-----------------------------------------------------------------------基础扎实的你完全可以忽略上面的内容----------------------------------------------------------------------------------

关于什么是访问控制器,听不明白,不要着急,下面我们先来做一个简单地demo,这个demo主要是为了后面我们来实现一个自己的 AceessControler做准备,是关于implies这个方法理解,这个方法可以说是串联起我们所有内容的核心。

  1. public static void main(String[] args) {
  2. Permission perOne = new FilePermission("d:/tmp/test.txt",SecurityConstants.FILE_READ_ACTION);
  3. Permission perAll = new FilePermission("d:/tmp/*",SecurityConstants.FILE_READ_ACTION);
  4. System.out.println(perOne.implies(perAll));
  5. System.out.println(perAll.implies(perOne));
  6. }

输出的结果为:

false

true

说明:implies方法就是用于判断一个权限的范围是不是包含了另外一个权限的范围, 在这个demo里,我们试着去判断对于perAll的权限是否包含perOne的权限还有perOne的权限是否包含perAll权限,很显 然,perAll权限是包含perOne的。而实际上AccessControler里有一个权限栈,它就是遍历栈帧中的 PermissionCollecion里的每个Permission然后调用里Permission的implies来判断是否包含某个权限的。

下面我们来做另外的一个demo,这个demo我们采取累加型的方法一点点的添加代码,以让你了解整个AccessControler和SecurityManager是怎么配合着工作的,这个demo稍微会复杂一点

步骤一:试着实现自己的安全管理器,实验是否成功,以下主要分三步来完成

第一步:实现一个自己的类MySecurityManager,它继承自SecurityManager,重写它的checkRead方法,我们直接让他抛出一个SecurityException异常。(copy吧少年,要的是你知识的储备,不是要你把代码背下来),

  1. package com.yfq.test;
  2. public class MySecurityManager extends SecurityManager {
  3. @Override
  4. public void checkRead(String file) {
  5. //super.checkRead(file, context);
  6. throw new SecurityException("你没有的权限");
  7. }
  8. }

第二步:
现一个简单的类,主要用来测试我们自己定义的安全管理器起作用了没有,我们这里借助了FileInputStream,因为
FileInputStream会调用安全管理器去校验权限(我们在笔记六已经详细的讲解过),所以用FileInputStream测试我们自己的安全
管理器非常的适合。

  1. package com.yfq.test;
  2. import java.io.FileInputStream;
  3. import java.io.IOException;
  4. import java.security.ProtectionDomain;
  5. public class TestMySecurityManager {
  6. public static void main(String[] args) {
  7. System.setSecurityManager(new MySecurityManager());
  8. try {
  9. FileInputStream fis = new FileInputStream("test");
  10. } catch (IOException e) {
  11. e.printStackTrace();
  12. }
  13. }
  14. }

现在简单的说明一下:

1.TestMySecurityManager的main函数第一行其实就是注册我们自己的安全管理器(还有一种安装安全管理器的方式,记得不,如果你忘记了请你看看笔记六)

2.FileInputStream fis = new
FileInputStream("test");这一行创建了一个FileInputStream对象,这个构造器内部会调用 public
FileInputStream(File
file);这个构造器,而这个构造会调用Ststem.getSercurityManager来取得当前的安全管理器security,然后调用它的
checkRead方法来校验权限。由于我们在第一行注册了自己的安全管理器,所以它将调用我们自己的安全管理器的checkRead来执行校验。

第三步:运行程序

  1. Exception in thread "main" java.lang.SecurityException: 你没有的权限
  2. at com.yfq.test.MySecurityManager.checkRead(MySecurityManager.java:8)
  3. at java.io.FileInputStream.<init>(FileInputStream.java:100)
  4. at java.io.FileInputStream.<init>(FileInputStream.java:66)
  5. at com.yfq.test.TestMySecurityManager.main(TestMySecurityManager.java:11)

好了,到这里说明我们自己的安全管理器安装上去了。上面的异常正好是我们期望见到的。

步骤二:我们来实现一个自己的类MyFileInputStream(当然这个不是真正意义的字节流包装类),它用于取代FileInputStream,它可以模拟FileInputStream是怎么去调用安全管理器,怎么去执行校验的。

                    第一步:编写MyFileInputStream(copy吧少年,不要自己狂敲)

  1. package com.yfq.test;
  2. import java.io.File;
  3. import java.io.FileNotFoundException;
  4. public class MyFileInputStream {
  5. public MyFileInputStream(String name) throws FileNotFoundException {
  6. this(name != null ? new File(name) : null);
  7. }
  8. public MyFileInputStream(File file) throws FileNotFoundException {
  9. String name = (file != null ? file.getPath() : null);
  10. SecurityManager security = System.getSecurityManager();
  11. if (security != null) {
  12. security.checkRead(name);
  13. }
  14. }
  15. }

简单的说一下逻辑,这个类MyFileInputStream(String name)的构造函数调用MyFileInputStream(File
file)这个构造函数,而MyFileInputStream(File
file)这个构造函数通过System.getSecurityManager();取出当前的SecurityManager,然后调用它的
checkRead方法。是滴,这个其实是FileInputStream源码里的逻辑,我只是把一些有妨碍我们理解的代码去掉了而已。

第二步,修改步骤一里的TestMySecurityManager里的main用自己的类替换FileInputStream函数如下

  1. package com.yfq.test;
  2. import java.io.IOException;
  3. public class TestMySecurityManager {
  4. public static void main(String[] args) {
  5. System.setSecurityManager(new MySecurityManager());
  6. try {
  7. MyFileInputStream fis = new MyFileInputStream("test");
  8. } catch (IOException e) {
  9. e.printStackTrace();
  10. }
  11. }
  12. }

 第三步,运行程序,好吧如果你用ecplise那么肯定报错,看看这个错误

java之jvm学习笔记六-十二(实践写自己的安全管理器)(jar包的代码认证和签名) (实践对jar包的代码签名) (策略文件)(策略和保护域)  (访问控制器) (访问控制器的栈校验机制) (jvm基本结构)

居然找不到我们的类,你郁闷没有,即使你跑到TestMySecurityManager.class的目录下,再运行还是这个问题。

我就不卖关子了,还是环境变量没有设置好。这里涉及到一些比较基础的问题,我简单的提一下,不然可能永远都讲不完了

我们知道配置jdk的环境的时候我们总是习惯设置三个变量

  1. path=%JAVA_HOME%/bin
  2. JAVA_HOME=C:/Java/jdk1.6.0_01
  3. CLASSPATH=.;%JAVA_HOME%/lib/dt.jar;%JAVA_HOME%/lib/tools.jar

这三个变量代表什么意思呢?

path,其实就是我们的java工具的目录,像我们编译java文件用到javac,还有运行class文件用到的java命令,包括我前面见到的密钥生成工具keytool和签名工具jarsigner。都是在这个path被配置的前提下才能正常运行的。

JAVA_HOME这个仅仅是一个变量名,你喜欢改成别的名字也可以,只是调用它的地方需要作出对应的修改

CLASSPATH:这个就是引起我们现在问题的地方,我们知道类加载器会加载类,但是它如何知道到哪里去加载类,这个路径就是告诉类加载器class文件放在了那个地方。

好了既然是这样的话,我们来设置一下CLASSPATH,

第四步,设置CLASSPATH.到com.yfq.test.TestMySecurityManager所在的编译目录

在cmd窗口我们输入java -classpath D:/workspace/MySecurityManager/bin com.yfq.test.TestMySecurityManager。

查看控制台输出  
java之jvm学习笔记六-十二(实践写自己的安全管理器)(jar包的代码认证和签名) (实践对jar包的代码签名) (策略文件)(策略和保护域)  (访问控制器) (访问控制器的栈校验机制) (jvm基本结构)

报错的提示变了,它提示MyFileInputStream这个类找不到,但是它命名和com.yfq.test.TestMySecurityManager在同个编译目录下,为什么?

好吧,这里我就不绕弯子了,我们再修改com.yfq.test.TestMySecurityManager,将设置自己的安全管理器的那行先简单地注释掉如下

  1. package com.yfq.test;
  2. import java.io.IOException;
  3. public class TestMySecurityManager {
  4. public static void main(String[] args) {
  5. //System.setSecurityManager(new MySecurityManager());
  6. try {
  7. MyFileInputStream fis = new MyFileInputStream("test");
  8. } catch (IOException e) {
  9. e.printStackTrace();
  10. }
  11. }
  12. }

编译之后再执行java -classpath D:/workspace/MySecurityManager/bin com.yfq.test.TestMySecurityManager,没有报错了。为什么会这样子???

重点:讲了这么一大篇幅,我无非要告诉你,在一般的情况下,同个线程中,我们用的是同一个类加载器去动态加载所需要的类文件,但
是,如果我们设置了SecurityManager的时候,情况就不一样了,当我们设置了安全管理器之后,当前类由于需要用到安全管理器来判断当前类是否
有加载类MyFileInputStream的权限,所以当前类会委托SecurityManager来加载MyFileInputStream,而对于
SecurityManger来说它就从CLASSPATH指定的路径加载我们的类,所以它没有找到我们的MyFileInputStream类

第五步,解决SecurityManager加载类,找不到类的问题。

解决方案太多了,第一种方法:直接修改系统的配置CLASSPATH将MyFileInputStream所在的类加到CLASSPATH中,但是这样太笨了。

第二种方法:直接使用set
classpath命令,我们执行这个命令set
classpath=.;D:/workspace/MySecurityManager/bin;%classpath%再执行

java com.yfq.test.TestMySecurityManager,问题解决。

第三种方法  : java -cp
"C:\Program Files\Java\jdk1.6.0_12\lib\tools.jar";"C:\Program
Files\Java\jdk1.6.0_12\lib\dt.jar";"D:/workspace/MySecurityManager/bin";.
com.yfq.test.TestMySecurityManager

第四种方法:java
-classpath  "C:\Program
Files\Java\jdk1.6.0_12\lib\tools.jar";"C:\Program
Files\Java\jdk1.6.0_12\lib\dt.jar";"D:/workspace/MySecurityManager/bin";.
com.yfq.test.TestMySecurityManager

第六步,将com.yfq.test.TestMySecurityManage中的System.setSecurityManager(new MySecurityManager());前的注释符号去掉再运行

java之jvm学习笔记六-十二(实践写自己的安全管理器)(jar包的代码认证和签名) (实践对jar包的代码签名) (策略文件)(策略和保护域)  (访问控制器) (访问控制器的栈校验机制) (jvm基本结构)

好了,终于完整的按照我们期望执行了。

小总结:上面的步骤一和步骤二,忽略整个调试的过程的话,其实思路很清晰了:

1.注册我们的安全管理器

2.实例化一个我们自己的类,这个类调用安全管理器的checkRead方法校验自己有没有相应的权限

3.MySecurityManager的checkRead方法由于只跑出一个异常,所以直接退出了程序。

这个过程其实就是我们的每个类调用安全管理器的过程,是一个比较简单的模拟,好好的玩味一下,然后开始我们的步骤三

到了这里,我们只是做了步骤一和步骤二,是不是一个很艰苦的过程?后面不难,真的不难,虽然我一直这么说,简单的才是大家的,但是难的才是自己的,哈哈哈。

  步骤三,实现我们的AccessControler(终于到这一步了,是不是很期待)

第一步,实现一个类
MyAccessControler,并实现一个叫checkPermission的静态方法。由于AccessControler是一个final类所
以我们无法想实现自己的MySecurityManager那样去继承它的父类,所以我们就自己定义一个类,我要你做的还是copy我的代
码。

  1. <p>package com.yfq.test;</p><p>import java.security.AccessControlException;
  2. import java.security.Permission;</p><p>
  3. public class MyAccessControler {
  4. public static void checkPermission(Permission perm)
  5. throws AccessControlException
  6. {
  7. throw new SecurityException("你没有的权限");
  8. }
  9. }
  10. </p>

第二步,修改MySecurityManage,重写父类SecurityManager的checkRead方法和checkPermission方法如下

  1. package com.yfq.test;
  2. import java.io.FilePermission;
  3. import java.security.Permission;
  4. import sun.security.util.SecurityConstants;
  5. public class MySecurityManager extends SecurityManager {
  6. @Override
  7. public void checkRead(String file) {
  8. checkPermission(new FilePermission(file,
  9. SecurityConstants.FILE_READ_ACTION));
  10. }
  11. @Override
  12. public void checkPermission(Permission perm) {
  13. MyAccessControler.checkPermission(perm);//调用我们自己的访问控制器
  14. }
  15. }

第三步:运行,在cmd控制台输出:java
-cp "C:\Program Files\Java\jdk1.6.0_12\lib\tools.jar";"C:\Program
Files\Java\jdk1.6.0_12\lib\dt.jar";"D:/workspace/MySecurityManager/bin";.
com.yfq.test.TestMySecurityManager
                               java之jvm学习笔记六-十二(实践写自己的安全管理器)(jar包的代码认证和签名) (实践对jar包的代码签名) (策略文件)(策略和保护域)  (访问控制器) (访问控制器的栈校验机制) (jvm基本结构)

恭喜你,哈,报错了,而且是一个很不常见的错误,类循环加载错误,你一定很好奇,怎么会循环加载错误,这个问题很多人定义自己的安全管理器的时候都遇到
过,但是它是怎么产生的?下面我们来改一行代码,再看它的错误信息,你就知道它是怎么产生的了,接着第四步。

第四步,修改上面的MySecurityManage类的checkPermission方法。如下

  1. package com.yfq.test;
  2. import java.io.FilePermission;
  3. import java.security.Permission;
  4. import sun.security.util.SecurityConstants;
  5. public class MySecurityManager extends SecurityManager {
  6. @Override
  7. public void checkRead(String file) {
  8. checkPermission(new FilePermission(file,
  9. SecurityConstants.FILE_READ_ACTION));
  10. }
  11. @Override
  12. public void checkPermission(Permission perm) {
  13. //MyAccessControler.checkPermission(perm);
  14. try {
  15. Class<?> clazz=this.getClass().getClassLoader().loadClass("com.yfq.test.MyAccessControler");
  16. } catch (ClassNotFoundException e) {
  17. e.printStackTrace();
  18. }
  19. }
  20. }

在次在cmd控制台输入运行命令:java -cp "C:\Program
Files\Java\jdk1.6.0_12\lib\tools.jar";"C:\Program
Files\Java\jdk1.6.0_12\lib\dt.jar";"D:/workspace/MySecurityManager/bin";.
com.yfq.test.TestMySecurityManager

java之jvm学习笔记六-十二(实践写自己的安全管理器)(jar包的代码认证和签名) (实践对jar包的代码签名) (策略文件)(策略和保护域)  (访问控制器) (访问控制器的栈校验机制) (jvm基本结构)

报错很长无止境,这里我们截取了重复的报错内容出来,看到Exception in thread "main" java.lang.*Error,栈溢出了,你仔细看报错

发现:at com.yfq.test.MySecurityManager.checkPermission(MySecurityManager.java:20)

at com.yfq.test.MySecurityManager.checkRead(MySecurityManager.java:11)

这两句话重复的在出现,一直重复,为什么???

解释:

记不记得前面那个MyFileInputStream的ClassNotFoundException的,它是怎么引起的,由于我们每new一个
MyFileInputStream的时候就要委托我们的SecurityManager调用checkPermission来校验当前线程是否有加载
MyFileInputStream这个类的权限,而SecurityManager的checkPermission方法里我们又调用了
Class<?>
clazz=this.getClass().getClassLoader().loadClass("com.yfq.test.MyAccessControler");,
类装载器装装载类的时候会判断该类有没有被装载的权限,这样当前的线程栈又需要委托当前的SecurityManager来校验我们当前的线程是否有装载
com.yfq.testMyAccessControler的权限,又需要再调用CheckPermission这样就没完没了了。

所以问题归到底还是SecurityManager的问题,它的
CheckPermission每次都会被调用来校验权限问题,一旦在CheckPermission中调用一些非核心API(默认为
SecurityConstants.ALL_PERMISSION)的方法时就需要被校验权限,一不小心就形成递归调用直到栈溢出。

现在又有一个新的疑问出来了,第三步中不是栈溢出啊,第四步讲一堆干嘛用啊,没错,好像是没什么用,但其实我们是在模拟这个过程,第四步之所以是栈溢出是因为:com.yfq.test.MyAccessControler它永远没有机会被load到内存,因为它一直递归的被校验,而第三步
不是,在第一装载的时候,由于我们的主线程,也就是TestMySecurityManager的main函数开启的线程它是由
sun.misc.Launcher$AppClassLoader这类装载的,第一次调用CheckPermission的时候,其实我们已经将
com.yfq.test.MyAccessControler装载入内存,而我们前面说过在装载之前它会委托SecurityManager来装载要应
用类,顺便校验执行权限,所以SecurityManager调用checkPermission的时候由于又被要求装载
MyAccessControler,所以SecurityManager用装载自己的parent来装载这个类,按照我们笔记三类装载器的体系结构,我
们知道,类的装载会采取双亲委托模式,照理来说这个错误是不应该发生的,是滴,你的想法是对滴,这貌似是jvm应该要为我们做的事情,但是由于在类执行链
接的时候MyAccessControler的调用触发了下一次checkPermission链接MyAccessControler所以它的链接关系
就变成了MyAccessControler<-->MyAccessControler这样就形成了双向的链接关系,即java.lang.ClassCircularityError,这个是jdk6.0的一个“bug”(我认为是bug)。

不信的话,我们来做个试验,复制下面的代码,跑一下

  1. package com.yfq.test;
  2. import java.security.Permission;
  3. public class Bug {
  4. public static class A {}
  5. public static void main(String[] args) throws Exception {
  6. System.out.println("Setting Security Manager");
  7. System.setSecurityManager(new SecurityManager() {
  8. public void checkPermission(Permission p) {
  9. new A();
  10. }
  11. });
  12. System.out.println("Post set.");
  13. }
  14. }

运行一下

java之jvm学习笔记六-十二(实践写自己的安全管理器)(jar包的代码认证和签名) (实践对jar包的代码签名) (策略文件)(策略和保护域)  (访问控制器) (访问控制器的栈校验机制) (jvm基本结构)

好吧,它就是个可恶的bug,每次new它都要来链接一次,这样就出现循环链接了,那么我们如何来解决这个bug呢?(如你把上面的式样程序 new A()改成new Bug()思考一下,为什么这个我们说的“bug”为什么会不见了)

第五步,再修改上面的MySecurityManage类的checkPermission方法。如下

  1. package com.yfq.test;
  2. import java.io.FilePermission;
  3. import java.security.Permission;
  4. import sun.security.util.SecurityConstants;
  5. public class MySecurityManager extends SecurityManager {
  6. private boolean isLoaded=true;
  7. @Override
  8. public void checkRead(String file) {
  9. checkPermission(new FilePermission(file,
  10. SecurityConstants.FILE_READ_ACTION));
  11. }
  12. @Override
  13. public void checkPermission(Permission perm) {
  14. //MyAccessControler.checkPermission(perm);
  15. if(isLoaded){
  16. isLoaded=false;
  17. System.out.println(MyAccessControler.class.getClassLoader());
  18. }
  19. }
  20. }

再次在cmd中输入:java -cp "C:\Program
Files\Java\jdk1.6.0_12\lib\tools.jar";"C:\Program
Files\Java\jdk1.6.0_12\lib\dt.jar";"D:/workspace/MySecurityManager/bin";.
com.yfq.test.TestMySecurityManager
java之jvm学习笔记六-十二(实践写自己的安全管理器)(jar包的代码认证和签名) (实践对jar包的代码签名) (策略文件)(策略和保护域)  (访问控制器) (访问控制器的栈校验机制) (jvm基本结构)

亲切的画面有木有!!!!!
                       第六步,对MyAccessControler中的checkPermission做简单的实现,copy我的代码

  1. package com.yfq.test;
  2. import java.io.FilePermission;
  3. import java.security.AccessControlException;
  4. import java.security.Permission;
  5. import sun.security.util.SecurityConstants;
  6. public class MyAccessControler {
  7. private MyAccessControler() {
  8. super();
  9. }
  10. public static void checkPermission(Permission perm)
  11. throws AccessControlException
  12. {
  13. Permission perAll = new FilePermission("d:/tmp/*",SecurityConstants.FILE_READ_ACTION);
  14. if(perAll.implies(perm)){
  15. System.out.println("你可以读取这个文件哦!");
  16. }else{
  17. throw new AccessControlException("你没有读取这个文件的权限");
  18. }
  19. }
  20. }

修改TestMySecurityManager中的main如下

  1. package com.yfq.test;
  2. import java.io.IOException;
  3. public class TestMySecurityManager {
  4. public static void main(String[] args) {
  5. System.setSecurityManager(new MySecurityManager());
  6. try {
  7. MyFileInputStream fis = new MyFileInputStream ("d:/tmp/test.txt");
  8. } catch (IOException e) {
  9. e.printStackTrace();
  10. }
  11. }
  12. }

运行:

java之jvm学习笔记六-十二(实践写自己的安全管理器)(jar包的代码认证和签名) (实践对jar包的代码签名) (策略文件)(策略和保护域)  (访问控制器) (访问控制器的栈校验机制) (jvm基本结构)

                      到这里,我们基本上已经走顺了安全管理器MySecurityManager和访问
控制器MyAccessControler是怎么配合工作的了,它大致是这么一个过程,需要权限控制的类会new一个Permission的子类对象(我
们的例子里采用MyFileInputStream)然后传递给我们的安全控制器里(我们的例子里自己定义了一个MySecurityManager)的
checkPermission方法,而这个方法什么也不干就是调用AccessControler的静态方法checkPermission,我们自己
的MyAccessControler里的checkPermission方法我们只是简单的调用了Permission的implies方法,而,其实
这也正是整个java虚拟机安全校验的总体脉络,但是这里我们还有一个疑问,AccessControler它到底是怎么和我们笔记九和笔记十的策略和策
略文件配合工作的呢???请看下一节,访问控制器的栈校验机制

java之jvm学习笔记十二(访问控制器的栈校验机制)

本节源码:http://download.csdn.net/detail/yfqnihao/4863854

这一节,我们会简单的描述一下jvm访问控制器的栈校验机制。

这节课,我们还是以实践为主,什么是栈校验机制,讲一百遍不如你自己实际的代码一下然后验证一下,下面我们下把环境搭起来。

第一步,配置系统环境。(copy吧,少年)

  1. path=%JAVA_HOME%/bin
  2. JAVA_HOME=C:/Java/jdk1.6.0_01
  3. CLASSPATH=.;%JAVA_HOME%/lib/dt.jar;%JAVA_HOME%/lib/tools.jar

第二步,配置一个策略文件的运行环境。

第一个类Doer:

  1. package com.yfq.test;
  2. public abstract interface Doer {
  3. void doYourThing();
  4. }

第二个类Friend:

  1. package com.yfq.test.friend;
  2. import java.security.AccessController;
  3. import java.security.PrivilegedAction;
  4. import com.yfq.test.Doer;
  5. public class Friend implements Doer{
  6. private Doer next;
  7. private boolean direct;
  8. public Friend(Doer next,boolean direct){
  9. this.next=next;
  10. this.direct=direct;
  11. }
  12. @Override
  13. public void doYourThing() {
  14. System.out.println("Im a Friend");
  15. if (direct) {
  16. next.doYourThing();
  17. } else {
  18. AccessController.doPrivileged(new PrivilegedAction() {
  19. @Override
  20. public Object run() {
  21. next.doYourThing();
  22. return null;
  23. }
  24. });
  25. }
  26. }
  27. }

第三个类Stranger:

  1. package com.yfq.test.stranger;
  2. import java.security.AccessController;
  3. import java.security.PrivilegedAction;
  4. import com.yfq.test.Doer;
  5. public class Stranger implements Doer {
  6. private Doer next;
  7. private boolean direct;
  8. public Stranger(Doer next, boolean direct) {
  9. this.next = next;
  10. this.direct = direct;
  11. }
  12. @Override
  13. public void doYourThing() {
  14. System.out.println("Im a Stranger");
  15. if (direct) {
  16. next.doYourThing();
  17. } else {
  18. AccessController.doPrivileged(new PrivilegedAction() {
  19. @Override
  20. public Object run() {
  21. next.doYourThing();
  22. return null;
  23. }
  24. });
  25. }
  26. }
  27. }

第四个类TextFileDisplayer:

  1. import java.io.CharArrayWriter;
  2. import java.io.FileNotFoundException;
  3. import java.io.FileReader;
  4. import java.io.IOException;
  5. import com.yfq.test.Doer;
  6. public class TextFileDisplayer implements Doer{
  7. String fileName;
  8. public TextFileDisplayer(String fileName){
  9. this.fileName=fileName;
  10. }
  11. @Override
  12. public void doYourThing() {
  13. try {
  14. FileReader fr = new FileReader(fileName);
  15. try {
  16. CharArrayWriter caw = new CharArrayWriter();
  17. int c;
  18. while((c=fr.read())!=-1){
  19. caw.write(c);
  20. }
  21. System.out.println(caw.toString());
  22. } catch (IOException e) {
  23. e.printStackTrace();
  24. }finally{
  25. if(fr!=null){
  26. try {
  27. fr.close();
  28. fr=null;
  29. } catch (IOException e) {
  30. e.printStackTrace();
  31. }
  32. }
  33. }
  34. } catch (FileNotFoundException e) {
  35. e.printStackTrace();
  36. }
  37. }
  38. }

第三步,参考笔记http://blog.csdn.net/yfqnihao/article/details/8267669,把Friend和Stranger打包并签名,并放到ecplise编译目录bin/jars下,把生成的密钥存储文件放在与bin同级的目录下。(你也可以先用我上传的源码里的jar包,不过还是建议你动手练一练)

第四步,配置策略文件

  1. keystore "ijvmkeys.keystore";
  2. grant signedby "friend.keystore" {
  3. permission java.io.FilePermission "d:/answer.txt", "read";
  4. permission java.io.FilePermission "d:/question.txt", "read";
  5. };
  6. grant signedby "stranger.keystore" {
  7. permission java.io.FilePermission "d:/question.txt", "read";
  8. };
  9. grant codeBase "file:D:/workspace/MyAccessControlerStack/bin/*" {
  10. permission java.io.FilePermission "d:/answer.txt", "read";
  11. permission java.io.FilePermission "d:/question.txt", "read";
  12. };

第五步,新建一个类,这个类里有个主函数,用于校验类Friend,Stranger,TextFileDisplayer对于question.txt的读取权限

  1. import com.yfq.test.friend.Friend;
  2. import com.yfq.test.stranger.Stranger;
  3. public class Example2 {
  4. public static void main(String[] args) {
  5. TextFileDisplayer tfd=new TextFileDisplayer("d:/question.txt");
  6. Friend friend = new Friend(tfd,true);
  7. Stranger stranger = new Stranger(tfd,true);
  8. stranger.doYourThing();
  9. }
  10. }

第六步运行,cmd窗口输入:
java -classpath .;jars/friend.jar;jars/stranger.jar
-Djava.security.manager
-Djava.security.policy=D:/workspace/MyAccessControlerStack/src/myPolicy.txt
Example2

java之jvm学习笔记六-十二(实践写自己的安全管理器)(jar包的代码认证和签名) (实践对jar包的代码签名) (策略文件)(策略和保护域)  (访问控制器) (访问控制器的栈校验机制) (jvm基本结构)

说明:

从这里,我们并不能很直观的发现访问控制器的栈校验机制,看Example2的main函数,我们知道当stranger执行doYourThing的时候,会经过这么一个过程,

Example2--------->被ApplClassLoader装入到ProtectionDomian_Example2中---------------->执行main函数

TextFilDisplayer------->ApplClassLoader判断当前的线程有没有装载类TextFilDisplayer的权限---------->装载到ProtectionDomian_TextFileDisplayer中

Friend------->ApplClassLoader判断当前的线程有没有装载类TextFilDisplayer的权限-------->装载到ProtectionDomian_Friendr中()

Stranger------->ApplClassLoader判断当前的线程有没有装载类TextFilDisplayer的权限----------->装载到ProtectionDomian_Stranger中

Stranger的实例对象stranger执行doYourThing方法---->直接调用Friend的实力引用执行
doYourThing方法----->Friend的实例引用直接调用TextFileDisplayer的doYourThing方法

输出question.txt的文本内容。

这个过程中,AccessControler到底是在什么时候执行的,怎么执行的呢,来看下面这个图

java之jvm学习笔记六-十二(实践写自己的安全管理器)(jar包的代码认证和签名) (实践对jar包的代码签名) (策略文件)(策略和保护域)  (访问控制器) (访问控制器的栈校验机制) (jvm基本结构)

上面的这个图是一个AccessControlerContext,也就是访问控制器上
下文,它大概了描述了,各个函数被调用的时候的保护域的压栈过程,直到栈顶结束压栈之后,它会按照先进后出的规则,AccessControler调用自
己的checkPermission方法,检验每一层的权限(上面的保护域数组中,名为BOOTSTRAP保护域是系统保护域,它的权限是
SecurityConstants.ALL_PERMISSION,这就意味着他什么都能够做)。AccessControler的保护域数组成员则会
调用自己的implies方法,ProtectionDomain的implies方法会先查看是否有配置了策略文件,如果有的话就将当前保护域传递给
Policy这个单例,由他从配置文件中取出PermissionCollection然后再调用每个Permission检验它的implies方法,
如果没有设定特定的配置文件,则直接调用当前保护域中的PermissionCollecion成员的implies,再由它调用Permission的
implies方法。

由于Examples2所读取的是question.txt文本,又由于我们的策略文件中,让Friend,Stranger,TextFileDisplayer都拥有它的读取权限,所以顺利的执行了。

第七步:为了验证我们的猜想是正确的,我们现在修改Example2如下

  1. import com.yfq.test.friend.Friend;
  2. import com.yfq.test.stranger.Stranger;
  3. public class Example {
  4. public static void main(String[] args) {
  5. TextFileDisplayer tfd=new TextFileDisplayer("d:/answer.txt");
  6. Friend friend = new Friend(tfd,true);
  7. Stranger stranger = new Stranger(tfd,true);
  8. stranger.doYourThing();
  9. }
  10. }

这里我们仅仅是将question.txt换成了answer.txt,而关于这个文件我们知道Stranger是没有读取的权限的,下面我们来运行它看看

第八步,cmd窗口输入
java -classpath .;jars/friend.jar;jars/stranger.jar
-Djava.security.manager
-Djava.security.policy=D:/workspace/MyAccessControlerStack/src/myPolicy.txt
Example
     java之jvm学习笔记六-十二(实践写自己的安全管理器)(jar包的代码认证和签名) (实践对jar包的代码签名) (策略文件)(策略和保护域)  (访问控制器) (访问控制器的栈校验机制) (jvm基本结构)

我们再来看AccessControlerContext的图

java之jvm学习笔记六-十二(实践写自己的安全管理器)(jar包的代码认证和签名) (实践对jar包的代码签名) (策略文件)(策略和保护域)  (访问控制器) (访问控制器的栈校验机制) (jvm基本结构)

前面的安全检查都通过了,但是到了STRANGER保护域的时候,由于Stranger'没有读取answer.txt的权限,所以implies方法抛出了一个AccessControlException。

那么AccessControler的栈校验机制能够带来什么好处呢??

答案很显然,就好像我们第七步一样,我们试图让一个没有权限的类来调用一个具有高级权限的类别,以达到“破坏”的目的,由于栈校验机制的存在,让我
们的这种幻想变得不容易实现,但是不容易实现并不代表不能够实现,下面我们将来学习一个方法,这个方法叫doPrivileged(),这个方法可以帮助
我们达到第七步的目的。

第九步,修改我们上面的Example类如下

  1. import com.yfq.test.friend.Friend;
  2. import com.yfq.test.stranger.Stranger;
  3. public class Example3 {
  4. public static void main(String[] args) {
  5. TextFileDisplayer tfd=new TextFileDisplayer("d:/answer.txt");
  6. Friend friend = new Friend(tfd,false);
  7. Stranger stranger = new Stranger(friend,true);
  8. stranger.doYourThing();
  9. }
  10. }

我们只是将friend的初始化参数做了稍微的调整,new Friend(tfd,true)改为了new
Friend(tfd,false);这个调整使得friend的doYourThing方法不是直接的执行next.doYourThing()而是通
过给AccessController.doPrivileged()方法传入一个匿名内部类并重写它的run方法,在run方法里调用了
next.doYourThing()。

第十步,然后我们在cmd窗口
输入:java -classpath .;jars/friend.jar;jars/stranger.jar
-Djava.security.manager
-Djava.security.policy=D:/workspace/MyAccessControlerStack/src/myPolicy.txt
Example3

java之jvm学习笔记六-十二(实践写自己的安全管理器)(jar包的代码认证和签名) (实践对jar包的代码签名) (策略文件)(策略和保护域)  (访问控制器) (访问控制器的栈校验机制) (jvm基本结构)
查看输出:既然成功由没有权限查看answer.txt的Strange完成了查看answer.txt的操作。这是怎么回事??我们再来看刚才表示AccessControlContext的图

java之jvm学习笔记六-十二(实践写自己的安全管理器)(jar包的代码认证和签名) (实践对jar包的代码签名) (策略文件)(策略和保护域)  (访问控制器) (访问控制器的栈校验机制) (jvm基本结构)

由于我们在Friend中安装了doPrivileged(),所以doPrivileged()这个方法被压入栈而且是在Stranger的前
面,doPrivileged()执行的时候会调用我的匿名内部类Friend$1并执行它的run方法,而run方法里执行完
next.doYourThing之后,AccessControlContext将继续执行判断到doPrivileged(),它发现这是一个
BootStrap的调用,那么AccessControlContext会继续执行另外一个判断,判断是谁安装了这个doPrivileged()方
法,所以执行到了Freind的doYourThing(),判定它有打开answer.txt的权限,那么最后才直接把run方法的return返回出
去。

就是通过这样的方式,使得我们没有权限的Stranger能够“越权"操作。

但是越权还是有条件的,如第九步,我们执行”越权“方法run的方法栈帧是嵌套在Friend的doYourThing的线程栈帧中的,由于Friend有读取answer.txt的权限,这才使得run方法有了”越狱“的机会。

第十一步,我们修改一下Example3来验证一下自己的观点

  1. import com.yfq.test.friend.Friend;
  2. import com.yfq.test.stranger.Stranger;
  3. public class Example4 {
  4. public static void main(String[] args) {
  5. TextFileDisplayer tfd=new TextFileDisplayer("d:/answer.txt");
  6. Stranger stranger = new Stranger(tfd,false);
  7. Friend friend = new Friend(stranger,true);
  8. stranger.doYourThing();
  9. }
  10. }

第十二步,cmd窗口输入
java -classpath .;jars/friend.jar;jars/stranger.jar
-Djava.security.manager
-Djava.security.policy=D:/workspace/MyAccessControlerStack/src/myPolicy.txt
Example4

java之jvm学习笔记六-十二(实践写自己的安全管理器)(jar包的代码认证和签名) (实践对jar包的代码签名) (策略文件)(策略和保护域)  (访问控制器) (访问控制器的栈校验机制) (jvm基本结构)

报出异常了,这个异常就是由于stranger$1这个内部类的方法栈帧是嵌套在
stranger的doYourThing的方法栈帧中,而stranger的保护域规定了stranger这个类的对象是没有权限读取
answer.txt这个文件,所以run这个方法也就没办法”越狱“。

总结:

这一节,我们学习了访问控制器校验保护域权限的过程,它采取的是栈的校验机制(先进后出),而它的每个方法调用总是线程栈
帧相关的,如果我们必须要”越狱“,那预约的条件要求调用doPrivileged()方法的栈帧的至少要有执行越狱操作的权限。