通过AccessController看Java安全模型

本文深入解析Java安全模型,包括访问控制体系结构、安全管理器、权限、策略文件及保护域概念。并通过示例代码展示了如何使用AccessController进行权限检查及doPrivileged方法实现临时权限提升。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

以下部分到虚线之间译自oracle官网:java security for access control

Java平台中的访问控制体系结构保护对敏感资源(例如本地文件)或敏感应用程序代码(例如,类中的方法)的访问。 所有访问控制决策都由一个安全管理器来调解,由java.lang.SecurityManager类表示。为了激活访问控制检查, SecurityManager必须安装到Java运行时。


Java applet和Java Web Start应用程序会在安装SecurityManager的情况下自动运行。 但是,通过java命令执行的本地应用程序默认情况下没有使用SecurityManager运行为了使用SecurityManager运行本地应用程序,应用程序本身必须通过setSecurityManager方法(在java.lang.System类中)以编程方式打开安全检查机关,或者必须在命令行中使用-Djava.security.manager参数调用。

Permissions

权限(permission)表示对系统资源的访问。 为了允许applet(或使用安全管理器运行的应用程序)访问资源,必须将相应的权限明确授予尝试访问的代码。

当Java代码被类加载器加载到Java运行时时,类加载器会自动将以下信息与该代码关联:

代码从哪里加载
谁签署了代码(如果有的话)
授予代码的默认权限


无论代码是通过不可信网络(例如,小应用程序)下载还是从文件系统(例如,本地应用程序)加载,该信息都与代码相关联。 加载代码的位置由URL表示,代码签名者由签名者的证书链表示,默认权限由java.security.Permission对象表示。

自动授予下载代码的默认权限包括将网络连接返回到主机的能力。 自动授予从本地文件系统加载的代码的默认权限包括从它所来自的目录以及该目录的子目录中读取文件的能力。

请注意,执行代码的用户身份在class加载时不可用。 如有必要,应用程序代码负责对最终用户进行身份验证(请参阅身份验证一节)。 一旦用户通过身份验证,应用程序就可以通过调用javax.security.auth.Subject类中的doAs方法将该用户与执行代码进行动态关联。

Security Policy  安全策略

类加载器为代码授予有限的一组默认权限。管理员可以通过安全策略灵活地管理其他代码权限。

Java SE在java.security.Policy类中封装了安全策略的概念。在任何给定时间,只有一个Policy对象安装在Java运行时中Policy对象的基本职责是确定代码是否有权访问受保护的资源(以从何处加载,谁签名以及谁执行它为特征)。 Policy对象如何做出这个决定取决于实现。例如,它可能会咨询包含授权数据的数据库,或者它可能会联系其他服务。

Java SE包含一个默认的策略实现,它从安全属性文件中配置的一个或多个ASCII(UTF-8)文件中读取授权数据。这些策略文件包含授予代码的确切权限集合:具体而言,授予从特定位置加载的代码的确切权限集,由特定实体签名并作为特定用户执行。每个文件中的策略条目必须符合文档化的专有语法,并且可以通过简单的文本编辑器或图形化策略工具实用程序进行组合。

Access Control Enforcement 访问控制执行
Java运行时跟踪程序执行时产生的Java调用序列。 当请求访问受保护资源时,默认情况下会对整个调用堆栈进行评估,以确定是否允许该访问请求。

如前所述,资源受到SecurityManager的保护。 JDK和应用程序中对安全敏感的代码通过如下代码保护对资源的访问:
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
   sm.checkPermission(perm);

}

Permission对象perm对应于请求的访问权限。 例如,如果尝试读取文件/ tmp / abc,则permission可以如下构造:
Permission perm = new java.io.FilePermission("/tmp/abc", "read");
SecurityManager的默认实现将其是否允许访问的决定委托给java.security.AccessController实现
AccessController遍历调用堆栈,并将所请求的权限(例如前面示例中的FilePermission)与所安装的安全策略传递给堆栈中的每个代码元素。 该策略根据管理员配置的权限确定请求的访问权限是否被授予。 如果未授予访问权限,AccessController将引发java.lang.SecurityException。


图1-4说明了访问控制执行。 在这个特定的例子中,调用堆栈中最初有两个元素,ClassA和ClassB。 ClassA调用ClassB中的方法,然后通过创建java.io.FileInputStream的实例来尝试访问文件/ tmp / abc。 FileInputStream构造函数创建一个FilePermission,perm,如上所示,然后将perm传递给SecurityManager类的checkPermission方法。 在这种特殊情况下,只需要检查ClassA和ClassB的权限,因为java.base模块中的所有类(包括FileInputStream,SecurityManager和AccessController)都会自动接收所有权限

Figure 1-4 Controlling Access to Resources


---------------------------------------------------------------------------------------------------------------------------

关于Permission对象的使用可看oracle官网:https://docs.oracle.com/javase/6/docs/technotes/guides/security/permissions.html

关于Policy File的使用可看oracle官网:https://docs.oracle.com/javase/6/docs/technotes/guides/security/PolicyFiles.html

关于doPrivileged API的使用可看oracle官网:https://docs.oracle.com/javase/6/docs/technotes/guides/security/doprivileged.html

------------------------------------------------------------------------------------------------------------------------------

为一种诞生于互联网兴起时代的语言,Java 从一开始就带有安全上的考虑,如何保证通过互联网下载到本地的 Java 程序是安全的,如何对 Java 程序访问本地资源权限进行有限授权,这些安全角度的考虑一开始就影响到 Java 语言的设计与实现。可以说 Java 在这些方面的探索与经验,对后来的一些语言与产品都带来了积极影响。

Java 中的安全模型

在 Java 中将执行程序分成本地和远程两种,本地代码默认视为可信任的,而远程代码则被看作是不受信的。对于授信的本地代码,可以访问一切本地资源。而对于非授信的远程代码在早期的 Java 实现中,安全依赖于沙箱 (Sandbox) 机制。沙箱机制就是将 Java 代码限定在虚拟机 (JVM) 特定的运行范围中,并且严格限制代码对本地系统的资源访问,通过这样的措施来保证对远程代码的有效隔离,防止对本地系统造成破坏。如图 1 所示,

图 1.JDK1.0 安全模型

图 1.JDK1.0 安全模型

但如此严格的安全机制也给程序的功能扩展带来障碍,比如当用户希望远程代码访问本地系统的文件时候,就无法实现。因此在后续的 Java1.1 版本中,针对安全机制做了改进,增加了安全策略,允许用户指定代码对本地资源的访问权限。如图 2 所示,

图 2.JDK1.1 安全模型

图 2.JDK1.1 安全模型

在 Java1.2 版本中,再次改进了安全机制,增加了代码签名。不论本地代码或是远程代码,都会按照用户的安全策略设定,由类加载器加载到虚拟机中权限不同的运行空间,来实现差异化的代码执行权限控制。如图 3 所示,

图 3.JDK1.2 安全模型
图 3.JDK1.2 安全模型

当前最新的安全机制实现,则引入了域 (Domain) 的概念。虚拟机会把所有代码加载到不同的系统域和应用域,系统域部分专门负责与关键资源进行交互,而各个应用域部分则通过系统域的部分代理来对各种需要的资源进行访问。虚拟机中不同的受保护域 (Protected Domain),对应不一样的权限 (Permission)。存在于不同域中的类文件就具有了当前域的全部权限,如图 4 所示:

图 4. 最新安全模型
图 4. 最新安全模型

以上提到的都是基本的 Java 安全模型概念,在应用开发中还有一些关于安全的复杂用法,其中最常用到的 API 就是 doPrivileged。doPrivileged 方法能够使一段受信任代码获得更大的权限,甚至比调用它的应用程序还要多,可做到临时访问更多的资源。有时候这是非常必要的,可以应付一些特殊的应用场景。例如,应用程序可能无法直接访问某些系统资源,但这样的应用程序必须得到这些资源才能够完成功能。针对这种情况,Java SDK 给域提供了 doPrivileged 方法,让程序突破当前域权限限制,临时扩大访问权限。下面内容会详细讲解一下安全相关的方法使用。


访问控制器

 类java.security.AccessController提供了一个默认的安全策略执行机制,它使用栈检查来决定潜在不安全的操作是否被允许。

这个访问控制器不能被实例化,它不是一个对象,而是集合在单个类中的多个静态方法。


     如果你安装了具体安全管理器,其实最终是由这个AccessController来决定一个潜在不安全的方法是否否被允许。

     每一个栈帧代表了由当前线程调用的某个方法,每一个方法是在某个类中定义的,每一个类又属于某个保护域,

每个保护域包含一些权限。因此,每个栈帧间接地和一些权限相关。

AccessController 用于访问控制操作和决定。再详细一点, AccessController主要用于以下三个目的: 

1.  根据当前有效的安全策略决定是否允许或拒绝对关键资源的访问。

2.  将代码标记为"特权",从而影响后续访问决定。

3.  获取当前调用上下文的“快照”,这样便可以相对于已保存的上下文作出其他上下文的访问控制决定。

 AccessController的最核心方法是它的静态方法checkPermission(),这个方法决定一个特定的操作能否被允许。

FilePermission perm = new FilePermission("/temp/testFile", "read");
  AccessController.checkPermission(perm);
在这个例子中, checkPermission将决定是否授予"读"权限给文件testFile。
如果访问请求被允许, checkPermission 会安静的返回,如果拒绝, 将会抛出一个AccessControlException。

[java]  view plain  copy
  1. for (int i = m; i > 0; i--) {  
  2.   
  3.      if (caller i's domain does not have the permission)  
  4.          throw AccessControlException  
  5.   
  6.      else if (caller i is marked as privileged) {  
  7.          if (a context was specified in the call to doPrivileged)  
  8.              context.checkPermission(permission)  
  9.          if (limited permissions were specified in the call to doPrivileged) {  
  10.              for (each limited permission) {  
  11.                  if (the limited permission implies the requested permission)  
  12.                      return;  
  13.              }  
  14.          } else  
  15.              return;  
  16.      }  
  17.  }  
  18.   
  19.  // Next, check the context inherited when the thread was created.  
  20.  // Whenever a new thread is created, the AccessControlContext at  
  21.  // that time is stored and associated with the new thread, as the  
  22.  // "inherited" context.  
  23.   
  24.  inheritedContext.checkPermission(permission);  


在某一个线程的调用栈中,当 AccessController 的 checkPermission 方法被最近的调用程序(例如 A 类中的方法)调用时,对于程序要求的所有访问权限,ACC 决定是否授权的基本算法如下:
1. 如果调用链中的某个调用程序没有所需的权限,将抛出 AccessControlException;
2. 若是满足以下情况即被授予权限:
a. 调用程序访问另一个有该权限域里程序的方法,并且此方法标记为有访问“特权”;
b. 调用程序所调用(直接或间接)的后续对象都有上述权限。

当然了,Java SDK 给域提供了 doPrivileged 方法,让程序突破当前域权限限制,临时扩大访问权限。

创建一个项目projectX:

[java]  view plain  copy
  1. package com.dusk;  
  2.   
  3. import java.io.File;  
  4. import java.io.IOException;  
  5. import java.security.AccessControlException;  
  6. import java.security.AccessController;  
  7. import java.security.PrivilegedAction;  
  8.   
  9. public class FileUtil {  
  10.     // 工程 A 执行文件的路径   
  11.     private final static String FOLDER_PATH = "C:\\Users\\dushangkui\\workspace\\projectX\\bin";   
  12.   
  13.     public static void makeFile(String fileName) {   
  14.         try {   
  15.             // 尝试在工程 A 执行文件的路径中创建一个新文件  
  16.             File fs = new File(FOLDER_PATH + "\\" + fileName);   
  17.             fs.createNewFile();   
  18.         } catch (AccessControlException e) {   
  19.             e.printStackTrace();   
  20.         } catch (IOException e) {   
  21.             e.printStackTrace();   
  22.         }   
  23.     }   
  24.   
  25.     public static void doPrivilegedAction(final String fileName) {   
  26.         // 用特权访问方式创建文件  
  27.         AccessController.doPrivileged(new PrivilegedAction<String>() {   
  28.             @Override   
  29.             public String run() {   
  30.                 makeFile(fileName);   
  31.                 return null;   
  32.             }   
  33.         });   
  34.     }   
  35. }  
创建另一个项目projectY

[java]  view plain  copy
  1. package com.dusk;  
  2.   
  3. import java.io.File;   
  4. import java.io.IOException;   
  5. import java.security.AccessControlException;   
  6.   
  7. import com.dusk.FileUtil;   
  8.   
  9. public class DemoDoPrivilege {   
  10.   
  11.    public static void main(String[] args) {   
  12.        System.out.println("***************************************");   
  13.        System.out.println("I will show AccessControl functionality...");   
  14.   
  15.        System.out.println("Preparation step : turn on system permission check...");   
  16.        // 打开系统安全权限检查开关  
  17.        System.setSecurityManager(new SecurityManager());   
  18.        System.out.println();   
  19.   
  20.        System.out.println("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~");   
  21.        System.out.println("Create a new file named temp1.txt via privileged action ...");   
  22.        // 用特权访问方式在工程 A 执行文件路径中创建 temp1.txt 文件  
  23.        FileUtil.doPrivilegedAction("temp1.txt");   
  24.        System.out.println("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~");   
  25.        System.out.println();   
  26.   
  27.        System.out.println("/////////////////////////////////////////");   
  28.        System.out.println("Create a new file named temp2.txt via File ...");   
  29.        try {   
  30.            // 用普通文件操作方式在工程 A 执行文件路径中创建 temp2.txt 文件  
  31.            File fs = new File(   
  32.                    "C:\\Users\\dushangkui\\workspace\\projectX\\temp2.txt");   
  33.            fs.createNewFile();   
  34.        } catch (IOException e) {   
  35.            e.printStackTrace();   
  36.        } catch (AccessControlException e1) {   
  37.            e1.printStackTrace();   
  38.        }   
  39.        System.out.println("/////////////////////////////////////////");   
  40.        System.out.println();   
  41.   
  42.        System.out.println("-----------------------------------------");   
  43.        System.out.println("create a new file named temp3.txt via FileUtil ...");   
  44.        // 直接调用普通接口方式在工程 A 执行文件路径中创建 temp3.txt 文件  
  45.        FileUtil.makeFile("temp3.txt");   
  46.        System.out.println("-----------------------------------------");   
  47.        System.out.println();   
  48.   
  49.        System.out.println("***************************************");   
  50.    }   
  51. }  

在projectY根目录下面创建策略文件MyPolicy.txt

[java]  view plain  copy
  1. // 授权工程 A 执行文件路径中文件在本目录中的写文件权限  
  2.  grant codebase "file:C:/Users/dushangkui/workspace/projectX/bin"  
  3.  {   
  4.   permission java.io.FilePermission   
  5.     "C:\\Users\\dushangkui\\workspace\\projectX\\bin\\*""write";   
  6.  };  
配置JVM运行参数 -Djava.security.policy=.\\MyPolicy.txt


Java默认不打开安全检查,如果不打开,本地程序拥有所有权限:

[java]  view plain  copy
  1. ***************************************  
  2. I will show AccessControl functionality...  
  3. Preparation step : turn on system permission check...  
  4.   
  5. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~  
  6. Create a new file named temp1.txt via privileged action ...  
  7. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~  
  8.   
  9. /////////////////////////////////////////  
  10. Create a new file named temp2.txt via File ...  
  11. /////////////////////////////////////////  
  12.   
  13. -----------------------------------------  
  14. create a new file named temp3.txt via FileUtil ...  
  15. -----------------------------------------  
  16.   
  17. ***************************************  

如果在projectY中打开系统安全权限检查开关  System.setSecurityManager(new SecurityManager());   则输出:

[java]  view plain  copy
  1. ***************************************  
  2. I will show AccessControl functionality...  
  3. Preparation step : turn on system permission check...  
  4.   
  5. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~  
  6. Create a new file named temp1.txt via privileged action ...  
  7. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~  
  8.   
  9. /////////////////////////////////////////  
  10. Create a new file named temp2.txt via File ...  
  11. java.security.AccessControlException: access denied ("java.io.FilePermission" "C:\Users\dushangkui\workspace\projectX\temp2.txt" "write")  
  12.     at java.security.AccessControlContext.checkPermission(Unknown Source)  
  13.     at java.security.AccessController.checkPermission(Unknown Source)  
  14.     at java.lang.SecurityManager.checkPermission(Unknown Source)  
  15.     at java.lang.SecurityManager.checkWrite(Unknown Source)  
  16.     at java.io.File.createNewFile(Unknown Source)  
  17.     at com.dusk.DemoDoPrivilege.main(DemoDoPrivilege.java:33)  
  18. /////////////////////////////////////////  
  19.   
  20. -----------------------------------------  
  21. create a new file named temp3.txt via FileUtil ...  
  22. java.security.AccessControlException: access denied ("java.io.FilePermission" "C:\Users\dushangkui\workspace\projectX\bin\temp3.txt" "write")  
  23.     at java.security.AccessControlContext.checkPermission(Unknown Source)  
  24.     at java.security.AccessController.checkPermission(Unknown Source)  
  25.     at java.lang.SecurityManager.checkPermission(Unknown Source)  
  26.     at java.lang.SecurityManager.checkWrite(Unknown Source)  
  27.     at java.io.File.createNewFile(Unknown Source)  
  28.     at com.dusk.FileUtil.makeFile(FileUtil.java:17)  
  29.     at com.dusk.DemoDoPrivilege.main(DemoDoPrivilege.java:45)  
  30. -----------------------------------------  
  31.   
  32. ***************************************  

参考: https://www.ibm.com/developerworks/cn/java/j-lo-javasecurity/


Spring源码--关于AccessController.doPrivileged

在Spring里发现一段代码,位置在, 
DefaultListableBeanFactory -> preInstantiateSingletons()方法里。 
如下:

if (System.getSecurityManager() != null && factory instanceof SmartFactoryBean) {
    isEagerInit = AccessController.doPrivileged(new PrivilegedAction<Boolean>() {
        public Boolean run() {
            return ((SmartFactoryBean<?>) factory).isEagerInit();
        }
    }, getAccessControlContext());
}

对于这个AccessController.doPrivileged,表示完全不懂是怎么回事。现在具体分析一下。

场景

当一个代码片段加载到现有的系统中,如果不对这个代码加以访问控制,那么它极有可能对当前系统安全造成破坏。比如这段代码会偷偷读取磁盘上的所有文件,然后保存到另外一台服务器上,造成机密信息泄露。 
最常见的就是applet程序,它会被限制在一个沙箱里运行。或者导入其他jar包到系统,而这个jar包中的程序也有可能是恶意的。 
所以,如果对这些代码进行权限控制是理所当然的。

安全管理器SecurityManager

作用是用于检查代码执行权限。其实日常的很多API都涉及到安全管理器,它的工作原理一般是: 
1. 请求Java API 
2. Java API使用安全管理器判断许可权限 
3. 通过则顺序执行,否则抛出一个Exception

经典的FileInputStream,会调用SecurityManager的checkRead方法,用于检查是否有读取某个文件的权限。

public FileInputStream(File file) throws FileNotFoundException {
    String name = (file != null ? file.getPath() : null);
    SecurityManager security = System.getSecurityManager();
    if (security != null) {
        security.checkRead(name);
    }
    if (name == null) {
        throw new NullPointerException();
    }
    if (file.isInvalid()) {
        throw new FileNotFoundException("Invalid file path");
    }
    fd = new FileDescriptor();
    fd.incrementAndGetUseCount();
    this.path = name;
    open(name);
}

SecurityManager里的方法列表如下,

checkAccept(String, int)
checkAccess(Thread)
checkAccess(ThreadGroup)
checkAwtEventQueueAccess()
checkConnect(String, int)
checkConnect(String, int, Object)
checkCreateClassLoader()
checkDelete(String)
checkExec(String)
checkExit(int)
checkLink(String)
checkListen(int)
checkMemberAccess(Class<?>, int)
checkMulticast(InetAddress)
checkMulticast(InetAddress, byte)
checkPackageAccess(String)
checkPackageDefinition(String)
checkPermission(Permission)
checkPermission(Permission, Object)
checkPrintJobAccess()
checkPropertiesAccess()
checkPropertyAccess(String)
checkRead(FileDescriptor)
checkRead(String)
checkRead(String, Object)
checkSecurityAccess(String)
checkSetFactory()
checkSystemClipboardAccess()
checkTopLevelWindow(Object)
checkWrite(FileDescriptor)
checkWrite(String)

都是check方法,分别囊括了文件的读写删除和执行、网络的连接和监听、线程的访问、以及其他包括打印机剪贴板等系统功能。而这些check代码也基本横叉到了所有的核心Java API上。

访问控制器AccessController

上面的SecurityManager都是基于AccessController实现的,还是继续上面的FileInputStream。 
security.checkRead()方法的细节,

public void checkRead(String file) {
    checkPermission(new FilePermission(file,
        SecurityConstants.FILE_READ_ACTION));
}

checkPermission()的细节,

public void checkPermission(Permission perm) {
    java.security.AccessController.checkPermission(perm);
}

可以看出,SecurityManager会调用AccessController里的方法。

概念


需要理解4个概念:代码源、权限、策略和保护域。


代码源CodeSource

CodeSource就是一个简单的类,用来声明从哪里加载类。


权限Permission

Permission类是AccessController处理的基本实体。Permission类本身是抽象的,它的一个实例代表一个具体的权限。权限有两个作用,一个是允许Java API完成对某些资源的访问。另一个是可以为自定义权限提供一个范本。权限包含了权限类型、权限名和一组权限操作。具体可以看看BasicPermission类的代码。典型的也可以参看FilePermission的实现。


策略Policy

策略是一组权限的总称,用于确定权限应该用于哪些代码源。话说回来,代码源标识了类的来源,权限声明了具体的限制。那么策略就是将二者联系起来,策略类Policy主要的方法就是getPermissions(CodeSource)和refresh()方法。Policy类在老版本中是abstract的,且这两个方法也是。在jdk1.8中已经不再有abstract方法。这两个方法也都有了默认实现。

在JVM中,任何情况下只能安装一个策略类的实例。安装策略类可以通过Policy.setPolicy()方法来进行,也可以通过java.security文件里的policy.provider=sun.security.provider.PolicyFile来进行。jdk1.6以后,Policy引入了PolicySpi,后续的扩展基于SPI进行。


保护域ProtectionDomain

保护域可以理解为代码源和相应权限的一个组合。表示指派给一个代码源的所有权限。看概念,感觉和策略很像,其实策略要比这个大一点,保护域是一个代码源的一组权限,而策略是所有的代码源对应的所有的权限的关系。

JVM中的每一个类都一定属于且仅属于一个保护域,这由ClassLoader在define class的时候决定。但不是每个ClassLoader都有相应的保护域,核心Java API的ClassLoader就没有指定保护域,可以理解为属于系统保护域。

使用方法


这是一个无法实例化的类——仅仅可以使用其static方法。

checkPermission

AccessController最重要的方法就是checkPermission()方法,作用是基于已经安装的Policy对象,能否得到某个权限。

doPrivileged

还有一个方法,就是doPrivileged()方法,获取特权,用于绕过权限检查。

参考

Java安全——安全管理器、访问控制器和类装载器 
Java 授权内幕


转载自:Spring源码--关于AccessController.doPrivileged


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值