JavaSecurity和JAAS——Java标准安全体系概述(上)

 

前言:java标准安全体系分为两大部分,一个是在JDK1.0引入并在JDK2进行了重构的代表着以代码为中心的授权体系。此体系下,关注的重点在于“这段代码能访问哪些系统资源”;另一个是在JDK1.3以扩展的形式引入,并在JDK1.4作为核心集成进来的以用户为中心的认证与授权体系JAAS。此时,关注的重点变成了“运行这段代码的用户的访问权限是什么”。其中JAAS是在java security基础上对组件进行了增强和扩展。

 

接下来,将分别介绍两种体系的整体概念及使用样例,并对其中的核心算法及技术要点进行分析讨论。

 

第一部分 java security architecture 以代码为中心的授权体系

1.1 使用样例

下面先看一个java security访问控制的简单示例。说明:本例使用maven管理jar包及开发环境中项目之间的依赖,若没有使用maven,则需要自行管理项目之间的依赖。例如:在普通java project环境中可以将项目X打成jar包然后引入到项目Y中。

 

1.1.1 创建项目sampleProjectX

创建maven项目sampleProjectX,在pom.xml中定义其groupId、artifactId、version如下:

 

security.jaas.test
prox
0.0.1-SNAPSHOT


然后创建package sample.projectx,并在其中添加类FileUtil:

 

public class FileUtil {  
    // 工程 X 执行文件的路径   
    private final static String FOLDER_PATH = "/home/wangd/data";   
  
    public static void makeFile(String fileName) {
        try {   
            // 尝试在工程 X 执行文件的路径中创建一个新文件  
            File fs = new File(FOLDER_PATH + "/" + fileName);   
            fs.createNewFile();   
            System.out.println("create "+fileName+" done!");
        } catch (AccessControlException e) {   
            e.printStackTrace();   
        } catch (IOException e) {   
            e.printStackTrace();   
        }   
    }   
  
    public static void doPrivilegedAction(final String fileName) {
        // 用特权访问方式创建文件  
        AccessController.doPrivileged(new PrivilegedAction() {   
            @Override   
            public String run() {   
                makeFile(fileName);   
                return null;   
            }   
        });   
    }   
}

1.1.2 创建项目sampleProjectY

创建maven项目sampleProjectY,在pom.xml中添加对sampleProjectX的依赖:

 


     security.jaas.test
      prox
      0.0.1-SNAPSHOT

 

并创建测试类AccessControlTest:

 

public class AccessControlTest {
	public static void main(String[] args) throws Exception{
        // 打开系统安全权限检查开关  
	    System.setSecurityManager(new SecurityManager());  
	    System.out.println("*****************install a SecurityManager**********************");   
		
	    System.out.println("Create a new file named temp1.txt via privileged action ..."); 
	    FileUtil.doPrivilegedAction("temp1.txt");    //调用projectX中的FileUtils类的doPrivilegedAction方法来创建一个文件
	    System.out.println("create a new file named temp2.txt via FileUtil ...");   
	    FileUtil.makeFile("temp2.txt");		//调用projectX中的FileUtils类的makeFile方法来创建一个类
	    
	    System.out.println("Create a new file named temp3.txt via File ...");   
	    try {   
	        // 用普通文件操作方式在工程 A 执行文件路径中创建 temp3.txt 文件  
	        File fs = new File("/home/wangd/data/temp3.txt");   
	        fs.createNewFile();   
	        System.out.println("create temp3.txt done!");
	    } catch (IOException e) {   
	        e.printStackTrace();   
	    } catch (AccessControlException e1) {   
	        e1.printStackTrace();   
	    }   
	}
}

其中System.setSecurityManager(new SecurityManager())代表注册启动一个安全管理器,另外也可以通过命令行参数来启动一个安全管理者,例如:

 

java -Djava.security.manager SomeApp

1.1.3 配置安全规则文件(policy file)

如果完成上述两步以后直接执行AccessControlTest结果如下:

 

*****************install a SecurityManager**********************
Create a new file named temp1.txt via privileged action ...
java.security.AccessControlException: access denied ("java.io.FilePermission" "/home/wangd/data/temp1.txt" "write")
	at java.security.AccessControlContext.checkPermission(AccessControlContext.java:472)
	at java.security.AccessController.checkPermission(AccessController.java:884)
	at java.lang.SecurityManager.checkPermission(SecurityManager.java:549)
	at java.lang.SecurityManager.checkWrite(SecurityManager.java:979)
	at java.io.File.createNewFile(File.java:1008)
	at sample.projectx.FileUtil.makeFile(FileUtil.java:22)
	at sample.projectx.FileUtil$1.run(FileUtil.java:37)
	at sample.projectx.FileUtil$1.run(FileUtil.java:1)
	at java.security.AccessController.doPrivileged(Native Method)
	at sample.projectx.FileUtil.doPrivilegedAction(FileUtil.java:34)
	at sampleProjectY.AccessControlTest.main(AccessControlTest.java:16)
create a new file named temp2.txt via FileUtil ...
java.security.AccessControlException: access denied ("java.io.FilePermission" "/home/wangd/data/temp2.txt" "write")
	at java.security.AccessControlContext.checkPermission(AccessControlContext.java:472)
	at java.security.AccessController.checkPermission(AccessController.java:884)
	at java.lang.SecurityManager.checkPermission(SecurityManager.java:549)
	at java.lang.SecurityManager.checkWrite(SecurityManager.java:979)
	at java.io.File.createNewFile(File.java:1008)
	at sample.projectx.FileUtil.makeFile(FileUtil.java:22)
	at sampleProjectY.AccessControlTest.main(AccessControlTest.java:19)
Create a new file named temp3.txt via File ...
java.security.AccessControlException: access denied ("java.io.FilePermission" "/home/wangd/data/temp3.txt" "write")
	at java.security.AccessControlContext.checkPermission(AccessControlContext.java:472)
	at java.security.AccessController.checkPermission(AccessController.java:884)
	at java.lang.SecurityManager.checkPermission(SecurityManager.java:549)
	at java.lang.SecurityManager.checkWrite(SecurityManager.java:979)
	at java.io.File.createNewFile(File.java:1008)
	at sampleProjectY.AccessControlTest.main(AccessControlTest.java:25)

 

从上可得,三种创建文件方式统统抛出权限控制异常,理由都是access denied ("java.io.FilePermission" "/home/wangd/data/tempX.txt" "write"),说明没有对目标文件的写权限。

在java security体系中,通过配置规则文件(policy file),可以给某代码库(codebase)赋予相应的一组权限。
规则文件的位置在安全参数文件中定义,或者启动时通过命令行参数指定。安全参数文件的默认位置为:

 

{java.home}/lib/security/java.security  (Solaris)  
{java.home}\lib/security\java.security  (Windows)


规则文件在其中的policy.url.n属性下配置。例如默认配置为:

 

policy.url.1=file:${java.home}/lib/security/java.policy  
policy.url.2=file:${user.home}/.java.policy


第一条一般不要修改,那是java的默认安全配置。可将第二条修改为我们自己定义的policy file:

 

policy.url.2=file:/mypath/mypolicy.txt


可以同时指定很多规则文件,参数名按数字序增加就可以了,所有指定的规则文件都会被加载。
另外也可以在启动时通过命令行参数指定

 

-Djava.security.policy=pURL  //将pURL指定的规则文件添加到policy.url.1--policy.url.n所指定的所有规则文件之后
-Djava.security.policy==pURL   //用pURL指定的规则文件替换所有的规则文件

本例中,我们将安全参数文档中的policy.url.2的值设置为我们自己配置的规则文档,/mypath/mypolicy.txt,并在mypolicy.txt配置我们想要赋予各个代码源的访问许可。规则文件的具体格式请参考《Java Security Architecture--Java安全体系技术文档翻译(三)》。

 

1.1.4 修改policy file的配置,查看不同的结果

首先把mypolicy.txt的内容修改为:

 

grant codebase "file:/myworkspace/sampleProjectX/target/classes"
 {   
  permission java.io.FilePermission "/home/wangd/data/*", "write,read";
 };
grant codebase "file:/myworkspace/sampleProjectY/target/classes"
 {   
  permission java.io.FilePermission "/home/wangd/data/*", "write,read";
 };


也就是说,同时赋予了sampleProjectX和sampleProjectY的代码库对于通配符为"/home/wangd/data/*"的所有文件的读和写许可。然后再执行AccessControlTest,结果如下:

 

*****************install a SecurityManager**********************
Create a new file named temp1.txt via privileged action ...
create temp1.txt done!
create a new file named temp2.txt via FileUtil ...
create temp2.txt done!
Create a new file named temp3.txt via File ...
create temp3.txt done!

不出所料的,所有方式都创建成功了。因为无论是sampleProjectX还是sampleProjectY中的代码都拥有足够的访问权限。

其次把mypolicy.txt的内容修改为:

 

grant codebase "file:/myworkspace/sampleProjectY/target/classes"
 {   
   permission java.io.FilePermission "/home/wangd/data/*", "write,read";
 };

此时,赋予了sampleProjectY的代码库对于通配符为"/home/wangd/data/*"的所有文件的读和写许可。然后再执行AccessControlTest,结果如下:

 

*****************install a SecurityManager**********************
Create a new file named temp1.txt via privileged action ...
java.security.AccessControlException: access denied ("java.io.FilePermission" "/home/wangd/data/temp1.txt" "write")
	at java.security.AccessControlContext.checkPermission(AccessControlContext.java:472)
	at java.security.AccessController.checkPermission(AccessController.java:884)
	at java.lang.SecurityManager.checkPermission(SecurityManager.java:549)
	at java.lang.SecurityManager.checkWrite(SecurityManager.java:979)
	at java.io.File.createNewFile(File.java:1008)
	at sample.projectx.FileUtil.makeFile(FileUtil.java:22)
	at sample.projectx.FileUtil$1.run(FileUtil.java:37)
	at sample.projectx.FileUtil$1.run(FileUtil.java:1)
	at java.security.AccessController.doPrivileged(Native Method)
	at sample.projectx.FileUtil.doPrivilegedAction(FileUtil.java:34)
	at sampleProjectY.AccessControlTest.main(AccessControlTest.java:16)
create a new file named temp2.txt via FileUtil ...
java.security.AccessControlException: access denied ("java.io.FilePermission" "/home/wangd/data/temp2.txt" "write")
	at java.security.AccessControlContext.checkPermission(AccessControlContext.java:472)
	at java.security.AccessController.checkPermission(AccessController.java:884)
	at java.lang.SecurityManager.checkPermission(SecurityManager.java:549)
	at java.lang.SecurityManager.checkWrite(SecurityManager.java:979)
	at java.io.File.createNewFile(File.java:1008)
	at sample.projectx.FileUtil.makeFile(FileUtil.java:22)
	at sampleProjectY.AccessControlTest.main(AccessControlTest.java:19)
Create a new file named temp3.txt via File ...
create temp3.txt done!

我们可以看到,只有在sampleProjectY中直接创建文件的方式成功了,其他调用sampleProjectX中的类的方式都没有权限。由此可知道,如果调用AccessControll.doPrivileged(action)的代码本身并不拥有action中要求的许可权限,那么该操作依旧会被拒绝并抛出访问控制异常。具体doPrivileged的算法将在下面详细说明。

最后将mypolicy.txt的内容编辑为:

 

grant codebase "file:/myworkspace/sampleProjectX/target/classes"
 {   
  permission java.io.FilePermission "/home/wangd/data/*", "write,read";
 };

 此时,赋予了sampleProjectX的代码基对于通配符为"/home/wangd/data/*"的所有文件的读和写许可。然后再执行AccessControlTest,结果如下:

 

*****************install a SecurityManager**********************
Create a new file named temp1.txt via privileged action ...
create temp1.txt done!
create a new file named temp2.txt via FileUtil ...
java.security.AccessControlException: access denied ("java.io.FilePermission" "/home/wangd/data/temp2.txt" "write")
	at java.security.AccessControlContext.checkPermission(AccessControlContext.java:472)
	at java.security.AccessController.checkPermission(AccessController.java:884)
	at java.lang.SecurityManager.checkPermission(SecurityManager.java:549)
	at java.lang.SecurityManager.checkWrite(SecurityManager.java:979)
	at java.io.File.createNewFile(File.java:1008)
	at sample.projectx.FileUtil.makeFile(FileUtil.java:17)
	at sampleProjectY.AccessControlTest.main(AccessControlTest.java:19)
Create a new file named temp3.txt via File ...
java.security.AccessControlException: access denied ("java.io.FilePermission" "/home/wangd/data/temp3.txt" "write")
	at java.security.AccessControlContext.checkPermission(AccessControlContext.java:472)
	at java.security.AccessController.checkPermission(AccessController.java:884)
	at java.lang.SecurityManager.checkPermission(SecurityManager.java:549)
	at java.lang.SecurityManager.checkWrite(SecurityManager.java:979)
	at java.io.File.createNewFile(File.java:1008)
	at sampleProjectY.AccessControlTest.main(AccessControlTest.java:25)


我们可以看到,第一种通过privileged action方式创建文件成功了,第二第三种方式仍然没有成功。第三种方式被拒绝很好理解,sampleProjectX并没有对相应文件的访问权利因此会报权限拒绝的异常。第一种和第二种的执行结果一个允许一个拒绝的本质不同就在于第一个是通过调用doPrivileged方法调用的,关于doPrivileged带来的改变,我们将在下面访问控制部分详细讲解。
 此时,一个使用java security进行访问控制的例子就完成了:我们启动了一个中央的访问控制管理器SecurityManager,它负责对所有涉及到敏感资源(也就是在执行实际操作之前会调用SecurityManager.checkPermission)的操作进行访问权限的校验。然后我们配置安全规则文件,指定了哪些代码可以有哪些访问权限。然后编写了两个来自不同位置的代码,其中一个调用另外一个执行了对敏感资源(文件系统)的访问,最终根据访问控制算法,得到了允许执行还是拒绝执行的反馈结果。当然实际情况可能远远比这个复杂,但是这个例子也足以说明问题了。
 接下来我们就上面例子涉及到的相关概念及核心算法做一下介绍,涉及到许可(Permission)、规则配置(Policy)、保护域(ProtectionDomain)、访问控制算法、doPrivilege方法对于权限校验的影响等等内容。下文将一一讲解。

 

1.2 java security模型演化历史

Java平台一开始提供的安全模型作为沙盒模型(sandbox model)而广为人知,该模型旨在提供一个对于从网络上获取的非信任代码的非常严格的校验环境。sandbox模型的实质就是:本地代码是被信任的,对于系统资源(例如文件系统)有所有的访问权限;同时下载的远程代码(applet)是不被信任的,只能访问沙盒内部允许的有限资源。
JDK1.1引进了“签名程序”的概念。在这个版本下,如果代码片段的签名密钥被收到它的终端系统识别为可信任,那么该签名的代码就会和本地代码一样受信任。签名代码,连同它们的签名,使用JAR格式被传递。在JDK1.1中,未签名的远程代码(applet)仍然在沙盒中运行。
在JDK2中提出了新的安全体系,也可以称为基于保护域的安全体系。基于如下几点被提出:

 

 

  • 粒度划分良好的访问控制
  • 可轻松配置的安全策略
  • 易于扩展的访问控制体系
  • 将安全检查扩展到所有的Java程序,不只是applets

在该模型中,所有的代码都会被划分进某一个保护域(ProtectionDomain),然后基于该保护域授予不同的许可(Permission)。

 

 

1.3 核心概念

 

1.3.1 许可(Permission)

许可类代表了对系统资源的访问权限。java.security.Permission是一个抽象类,可通过继承它以实现特定类型的访问权限。例如:perm = new java.io.FilePermission("/tmp/abc", "read")就是一个文件类型的访问许可。系统内置的许可类型可详见文档:《Java Security Architecture--Java安全体系技术文档翻译(二)》。
关于Permission的一个非常重要的方法就是implies,每一个具体的实现都应该非常认真小心的实现它。其含义是“隐含”:也就是如果a implies b 那么说明a定义的权限“隐含”了b定义的权限,也就是a的权限范围大于等于b。这对于访问控制算法的实现是至关重要的。
另外两个概念是PermissionCollection和Permissions,它们都表示了权限的聚合概念。其中,PermissionCollection是代表了相同类型的一组访问权限,而Permissions是PermissionCollection的集合,代表了不同类型的多组访问权限。聚合权限类一样实现了implies方法,使得执行聚合权限的“隐含”操作跟执行普通权限是一致的。

 

1.3.2 规则(Policy)

一个Java应用的系统安全规则指定了对于不同来源的代码应该赋予哪些许可,由一个Policy对象代表。更确切的说,它是由一个实现了Policy抽象方法的子类来代表。
Policy实现类是由安全属性文件中的属性policy.provider配置,默认是sun.security.provider.PolicyFile实现。是全局唯一的并且默认不会初始化,在第一次调用Policy.getPolicyNoCheck时初始化。policy.provider=sun.security.provider.PolicyFile可将其修改为自定义的Policy实现类。(javasecurity文档强烈说明,并没这个必要)

 

1.3.3 保护域

一个基本概念和系统安全的重要组成部分就是保护域(protection domain)。一个域从概念上包括了一组类,这些类的实例都被赋予了相同的一组许可(permission)。保护域是由当前使用的规则(policy)所决定的。Java应用环境保持了一个从代码(类和实例)到他们的保护域再到他们的许可(permission)这样一个路径。
Java2安全体系根据保护域赋予访问许可,而不是向单个的一段运行代码赋予这种许可。这样的设计是为了保持灵活性和扩展性。例如被JAAS强化以后的保护域还与当事人身份(Principals)关联,代表代码由什么身份运行,此部分将在JAAS中详细讨论。

 

1.3.4 当前调用的访问控制上下文

有些时候一个本应该在某特定访问控制上下文中执行的安全检查,实际上需要在另外一个不同的访问控制上下文中完成。例如,当一个线程向另外一个线程发送了一个事件,第二个线程在处理这个请求事件的时候没有正确的访问控制上下文来完成访问控制,如果这个服务需要请求访问受控资源的话。
为了解决该问题,为AccessController提供了getContext方法并提供了AccessControlContext类。getContext方法获取一个当前调用的访问控制上下文的“快照”,将其放在一个AccessControlContext对象中,并返回该对象。一个调用的样例如下:

 

AccessControlContext acc = AccessController.getContext();


该上下文快照抓取了足够的访问控制相关信息,所以一个访问控制决定可以在另外一个上下文中通过检查该上下文快照来做出。例如,一个线程可以发送事件给另外一个线程,同时也提供这个上下文快照信息。AccessControlContext本身有一个checkPermission方法,该方法基于它自己包含的安全上下文来做出访问控制决定,而不是当前执行线程的安全上下文。这样一来,如有必要,第二个线程就可以通过调用下面的代码来执行一个合适的安全检查:

 

acc.checkPermission(permission);

上面的方法调用等价于在第一个线程中执行安全检查,即使它是在第二个线程中执行的。
其本质上是代表了当前调用方法栈中涉及的所有类对应的安全域的集合。(进行了优化,例如去掉重复的,系统域以及调用doprivileged方法的类之前的域)

 

1.3.5 整体概念

  1. SecureClassLoader及其子类在加载类时,创建相应的ProtectionDomain并在内部维护。并将创建的类与该ProtectionDomain相关联。该ProtectionDomain被称为应用程序保护域,与系统保护域相区别。系统保护域只有一个,应用程序保护域基于不同的codeSource和不同的ClassLoader可以有很多。
  2. 当线程在执行的过程中,实际的执行流表现为方法调用栈,其中的每个方法都属于某个类,而类又对应着某个ProtectionDomain,因此一个执行线程在碰到需要执行checkPermission的敏感操作时很可能已经穿越了很多个安全域。
  3. ProtectionDomain中保存的Permissions是在创建在ProtectionDomain时传入的一组静态许可,而系统运行时真正赋予该ProtectionDomain的许可是参考了当前生效的Policy以后获得的动态许可。
  4. Java应用环境保持了一个从代码(类和实例)到他们的保护域再到他们的许可(permission)这样一个路径。
  5. 当任何代码想要访问一个敏感资源时,访问控制算法会首先获得当前调用瞬间当前线程的执行堆栈上的所有对应类的保护域(会执行优化及被doPrivileged方法改变,下面详述),然后逐个查看各保护域是否拥有访问该资源的许可。如果有,访问控制算法会默默退出;如果任何一个域没有相应许可,那么访问控制算法会抛出异常。换句话说,要取所有这些保护域拥有许可的交集,并看该交集中是否拥有需要的许可。
  6. doPrivilege方法允许一段受信任的代码暂时拥有比调用它的应用更多的资源访问许可。也就是说,假如当前ProtectionDomain中的某方法PTest.testDoPrivileged调用了doPrivilege来执行对系统资源的访问操作,并且该ProtectionDomain及其随后调用涉及到的ProtectionDomain都拥有相应的许可。那么任何域中的代码都可以调用PTest.testDoPrivileged来访问系统资源。

1.4 访问控制

1.4.1 SecurityManager与AccessController

SecurityManager代表了一个中央访问控制点的概念,而AccessController实现了一个详细的带有如doPrivileged这样的特殊规范的访问控制算法。
当系统注册启动了一个SecurityManager以后,对系统所有敏感资源的访问都会由SecurityManager来进行访问许可检查。在SecurityManager内部调用了AccessController实现的访问控制算法。AccessController将一个javasecurity团队认为最有限制力和在大部分场景下能减轻开发者编写大量安全代码负担的访问控制算法内置进去。

 

1.4.2 当前调用访问控制上下文的抓取算法

当调用AccessController.getContext()时,系统开始对当前调用的访问控制上下文进行抓取,首先通过

 

AccessControlContext acc = getStackAccessControlContext();

来调用一个native方法获得执行线程当前调用栈对应的访问控制上下文快照(简称上下文)。然后对该上下文快照执行实例方法optimize:

 

acc.optimize()

在方法中将本调用上下文与调用doPrivilege方法时候传递的特权上下文(privileged context)或者从父线程继承过来的上下文(inherited context)合并(特权上下文与继承上下文是互斥的关系,也就是要么是特权上下文要么是继承上下文),相关代码摘录如下:(与JAAS相关的代码暂时略去)

 

AccessControlContext optimize() {  
    	.......................  
	if (isPrivileged) {  
            acc = privilegedContext;  
            if (acc != null) {  
                /* 
                 * If the context is from a limited scope doPrivileged() then 
                 * copy the permissions and parent fields out of the wrapper 
                 * context that was created to hold them. 
                 */  
                if (acc.isWrapped) {  
                    permissions = acc.permissions;  
                    parent = acc.parent;  
                }  
            }  
        } else {  
            acc = AccessController.getInheritedAccessControlContext();  
            if (acc != null) {  
                /* 
                 * If the inherited context is constrained by a limited scope 
                 * doPrivileged() then set it as our parent so we will process 
                 * the non-domain-related state. 
                 */  
                if (acc.isLimited) {  
                    parent = acc;  
                }  
            }  
        }  
        ..........  
        ProtectionDomain[] assigned = (skipAssigned) ? null : acc.context;  
        ..........  
        pd = combine(context, assigned);  
        ..........  
        }  

 

上述代码中首先根据本AccessControlContext是否标记特权flag,来决定使用privilegedContext还是调用native方法AccessController.getInheritedAccessControlContext()获得的继承的访问控制上下文。然后将其合并进当前调用堆栈的上下文,并返回。关于需要将当前线程调用栈上下文与特权上下文或继承上下文合并的必要性请参考Java Security Architecture--Java安全体系技术文档翻译(四),其中4.3和4.4两节。

1.4.3 访问控制上下文快照的优化

 

在内部,AccessController在确定这个访问控制上下文时进行一些优化,以使访问检查循环尽可能地快。这些优化包括:

 

  • 返回的ProtectionDomain只到达(并包括)通过调用AccessController的doPrivileged特别标记的第一个堆栈帧。因为从前面对于doPrivileged的讨论可知,该堆栈帧之前的堆栈帧无需进行访问控制校验。
  • 返回的ProtectionDomains不包括系统域。系统域定义为具有所有权限,所以不需要检查是否“隐含”了所需要的权限(它总是隐含的)。
  • 返回的ProtectionDomain都是惟一的(即如果多个堆栈帧对应于同一个ProtectionDomain,那么只会返回一个ProtectionDomain)。

1.4.4 访问控制检查核心算法

假设当前线程跨越了m个调用者,按照caller1到caller2到callerm的顺序。然后callm调用了checkPermission方法。则技术文档上给出的AccessController.checkPermission方法用来决定访问请求是被允许还是拒绝的算法如下:

 

	i = m;  
    	while (i > 0) {  
            if (caller i's domain does not have the permission)  
                throw AccessControlException  
            else if (caller i is marked as privileged) {  
                if (a context was specified in the call to doPrivileged)  
                    context.checkPermission(permission);  
                return;  
           }  
            i = i - 1;  
    	};  
	// Next, check the context inherited when  
    	    // the thread was created. Whenever a new thread is created, the  
    	    // AccessControlContext at that time is  
    	    // stored and associated with the new thread, as the "inherited"  
    	    // context.  
    	inheritedContext.checkPermission(permission);

 


在实际的实现中(JDK1.8),在AccessController.checkPermission被调用时,首先执行了与AccessController.getContext()相同的行为:实时获得当前调用堆栈的上下文快照,并通过该快照的optimize方法将自身与privileged context或inherited context合并进来,并执行了一系列的优化。所以最终只需要执行优化合并后的AccessControlContext的checkPermission方法。在其中对涉及到的每一个ProtectionDomain进行许可检查。

 

1.5 执行线程的inheritedAccessControlContext

当一个线程创建了一个新线程,一个新的栈也被创建。如果在创建新线程时当前的访问控制上下文不被保存,那么当在新线程中调用AccessController.checkPermission时,安全决定将仅仅根据新线程的访问控制上下文做出,不会考虑父线程的访问控制上下文。
本质上这个干净栈的问题并不是一个安全问题,但是它会使得编写安全代码,尤其是系统代码,更容易产生难易察觉的错误。例如,一个非专家的开发者可能会想当然的假定一个子线程(例如:一个没有涉及到不信任代码的)会从父线程(例如:一个涉及到不安全代码的)继承相同的访问控制上下文。而这将会导致不希望的安全漏洞,比如可以从新创建的线程中访问受控的资源,然后将该资源传递给不受信任的代码。
因此,当一个新线程被创建,我们实际上会确保(通过线程创建以及其他代码)在该子线程创建时自动的继承了父线程的访问控制上下文,以此来保证子线程中随后的checkPermission会考虑到继承的父线程访问控制上下文。
在Thread代码的init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc)方法中通过如下语句:

 

 

this.inheritedAccessControlContext =
                acc != null ? acc : AccessController.getContext();

设置继承的安全上下文,并且对外没有公开的API可以修改这个继承特性。
也可以说,逻辑上的线程访问控制上下文包括了父线程访问控制上下文和当前调用堆栈的访问控制上下文。

 

1.6 类加载器

 

1.6.1 类加载过程及ProtectionDomain与ClassLoader及Class的关系

java安全体系中,从AppClassLoader开始,应用程序代码的类绝大部分是由SecureClassLoader及其子类加载的(AppClassLoader是URLClassLoader的子类,而URLClassLoader是SecureClassLoader的子类)。当一个类被SecureClassLoader及其子类加载,类加载器会创建/从缓存中取得一个protectionDomain(视该保护域是否已被创建),并将该类关联到这个保护域上。
一个类与其ProtectionDomain之间的映射是在类第一次装载时设置的,并在类被垃圾收集之前不会改变。

 

1.6.2 URLClassLoader类加载器中的acc属性

URLClassLoader中的acc属性是在该URLClassLoader被创建时,从执行创建的方法所在的调用堆栈中抓取的访问控制上下文快照。以后该类加载器每次加载类或执行其他敏感操作时都会额外检查该上下文快照(使用带上下文参数的AccessController.doPrivileged方法)。
这么做的目的是为了防止新创建的加载器获得比创建它的类所对应的加载器更大的许可权限。因为这样会导致难以发现的安全漏洞。其考虑跟子线程继承父线程的安全上下文是一致的。

 

1.7 总结

以代码为中心的授权体系大家可能考虑较少,如果不选用JAAS来做用户认证和授权的话,应该很少会开启SecurityManager。但是作为Java内置的安全体系,其中的一些概念已经深深根植于java语言的方方面面,例如安全域、上下文快照等等概念会隐藏在每一次的创建类加载器、类加载、创建线程等动作之中,对我们的程序带来一些影响,例如带来我们没有意识到的间接强引用,影响GC的行为。关于这方面的一个例子,请参考我写的另一篇文章《使用ReferenceQueue实现对ClassLoader垃圾回收过程的观察、以及由此引发的ClassLoader内存泄露的场景及排查过程》。

 

 

 

接下来这部分将介绍JAAS的示例与概念,并剖析如何在java security的基础之上进行增强来实现以用户为中心的认证与授权体系的。

 

 

 

 

 

 

 

下一篇:JavaSecurity和JAAS——Java标准安全体系样例及概述(下)

你可能感兴趣的:(技术研究)