Java沙箱机制的实现——安全管理器、访问控制器

一、Java沙箱机制

Java沙箱(sandbox)是Java安全模型的核心,那如何理解沙箱呢?

我们知道如果默认不作任何配置,我们所写的程序是可以直接访问机器上的任意资源的,例如操作文件、网络请求等。而当我们把程序运行在服务器时,如果不对程序加以限制,那么将大大增加系统的安全隐患。更甭提我们的程序中有部分代码并不是我们自己实现的,而是网络上的轮子。为了解决这种隐患,便有了沙箱机制。沙箱机制其实就是一个可以用来限制你程序运行的环境。

Java提供了沙箱机制,我们一般所说的打开沙箱,也是加-Djava.security.manager选项。并且Java沙箱还可以由程序员自我定制,它主要包括了以下几个部分:

  • 类加载器(class loader):所有的Java类都是通过类加载器加载的,可以自定义类加载器来设置加载类的权限。
  • 安全管理器(security manager):是核心API和操作系统之间的主要接口。实现权限管理,比存取控制器优先级高。
  • 访问控制器(access controller):访问控制器可以控制核心API对操作系统的存取权限,而这个控制的策略设定,可以由用户指定。

二、SecurityManager安全管理器

1. 什么是安全管理器

安全管理器是Java沙箱的基础组件。所谓安全管理器,是指在代码运行的过程中,禁止代码访问一些比较私密的信息或执行一些不安全代码。例如读取存储私密信息的文件夹,监听端口,甚至反射,修改正在运行的代码中的各种信息。

使用安全管理器之后,你就可以安全地运行一些来历不明的软件,如果这个软件做出一些你并不希望的操作,系统就会禁止这个操作并抛出一个异常。

简单举例,当我们应用程序中运行到System.exit()这行代码时,代码会首先通过 Java安全管理器去检查它是否有调用该函数的权利,没有则会抛出异常。

// =====:Client.class
System.exit();

// =====:System.class
public static void exit(int status) {
    Runtime.getRuntime().exit(status);
}

// =====:Runtime.class
public void exit(int status) {
    SecurityManager security = System.getSecurityManager();
    if (security != null) {
    // =====:检查是否有执行exit的权利
        security.checkExit(status);
    }
    Shutdown.exit(status);
}

2. 认识SecurityManager安全管理器

上文中代码设计到一个类SecurityManager,这是实现Java安全机制中十分关键的一个类。根据 JavaDoc 中的介绍我们可以了解到:

SecurityManager是一个被用于应用程序实现安全策略的类。它允许应用程序在执行不安全或敏感操作之前确定该操作是什么,以及判断当前的安全上下文是否允许该操作被执行。为了实现所提到的这种安全机制,SercurityManager还包含了许多以check开头的检查方法,这些检查方法通常在被执行敏感操作前被调用。这种检查方法的调用通常如下所示:

SecurityManager security = System.getSecurityManager();
if (security != null) {
    security.checkXXX(argument,  . . . );
}

通过上面了解,我们可以采用 SecurityManager机制来完善我们应用程序的安全性,安全管理器SecurityManager 是安全的实施者,可对此类进行扩展,它提供了加在应用程序上的安全措施,通过配置安全策略文件达到对网络、本地文件和程序其他部分的访问限制的效果。

流程

3. 启用安全管理器

一般而言,Java程序启动时并不会自动启动安全管理器,可以通过以下两种方法启动安全管理器:

  • 隐式启用,启动默认的安全管理器最简单的方式就是:直接在启动命令中添加 -Djava.security.manager 参数即可。
  • 显示启用,实例化一个 java.lang.SecurityManager 或继承它的子类的对象,然后通过 System.setSecurityManager() 来设置并启动一个安全管理器。

3.1 显示启用安全管理

public class SecurityManagerDemo {

    public static void main(String[] args) throws IOException {
    // =====:实例化一个继承SecurityManager类,并将其设置为应用程序的安全管理器
        System.setSecurityManager(new SecurityManagerImpl());

        FileReader fr = new FileReader(new File("D:\\test.txt"));
        BufferedReader reader = new BufferedReader(fr);
        String s;
        while ((s = reader.readLine()) != null) {
            System.out.println(s);
        }
    }
}

// =====:继承SecurityManager,自定义一个安全管理器
class SecurityManagerImpl extends SecurityManager {

    public void CheckRead(String file) {
        throw new SecurityException();
    }
}

上面代码的输出结果如下:

输出结果

这里可能会产生一点疑惑,checkRead()函数是在哪里被调用到的?其实我们阅读源代码就可以发现,在FileInputStream中有这样一段代码

public FileInputStream(File file) throws FileNotFoundException {
    String name = (file != null ? file.getPath() : null);
    SecurityManager security = System.getSecurityManager();
    if (security != null) {
        // =====:在这里调用checkRead()函数查看权限
        security.checkRead(name);
    }
    // =====:继续执行代码逻辑....
}

也就是说这个方法最终是被横叉在我们所调用的Java的API中,所谓显示启用的前提也是Java本身帮我们在关键代码中插入了许多类似的检查。

3.2 隐式启用安全管理

所谓隐式启用就是启用系统默认的安全管理器,一般推荐使用这种方式,因为这种方式可以不用去改动代码。

可在启动 JVM 时设定-Djava.security.manager选项,还需要同时指定安全策略文件。如果在应用中启用了Java安全管理器,却没有指定安全策略文件,那么Java安全管理器将使用默认的安全策略,默认是由位于目录$JAVA_HOME/jre/lib/security中的 java.policy 定义的。关于安全策略的内容下面再详细介绍。

4. SecurityManager中的安全检查方法

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

// =====:①文件访问相关的安全检查方法
checkRead(FileDescriptor)
checkRead(String)
checkRead(String, Object)

checkWrite(FileDescriptor)
checkWrite(String)

checkDelete(String)

// =====:②网络访问相关的安全检查方法
checkAccept(String, int)

checkConnect(String, int)
checkConnect(String, int, Object)

checkListen(int)

checkMulticast(InetAddress)
checkMulticast(InetAddress, byte)

checkSetFactory()

// =====:③线程访问相关的安全检查方法
checkAccess(Thread)
checkAccess(ThreadGroup)

// =====:④系统资源相关的安全检查方法
checkAwtEventQueueAccess()

checkPrintJobAccess()

checkTopLevelWindow(Object)

checkSystemClipboardAccess()

// =====:⑤保护虚拟机的安全检查方法
checkCreateClassLoader()

checkExec(String)

checkExit(int)

checkLink(String)

checkPermission(Permission)
checkPermission(Permission, Object)

checkPropertiesAccess()
checkPropertyAccess(String)

// =====:⑥保护安全机制本身的安全检查方法
checkMemberAccess(Class<?>, int)

checkSecurityAccess(String)

checkPackageAccess(String)
checkPackageDefinition(String)

而 Check方法最内层的实现,其实利用了后面将要提到的访问控制器。

三、AccessController访问控制器

从上文我们了解到Java API的安全策略主要由 SecurityManager 提供。而 SecurityManager 中所有 check方法的实现,都是基于 AccessController的。

1. 组成

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

1.1 代码源CodeSource

就是一个简单的类,用来声明从哪里加载类,对其进行封装。

1.2 权限Permission

Permission本身是一个抽象类,但它的一个实例代表了一个具体的权限。它是AccessController处理的基本实体。

1.3 策略Policy

策略是一组权限的总称,用于确定权限应该用于哪些代码源。代码源标识了类的来源,权限声明了具体的限制。那么策略就是将二者联合起来。Java使用了Policy对安全策略进行封装,我们可以通过java.policy文件去声明一个安全策略,-Djava.security.policy指定安全策略的地址。如果我们启动了安全管理器但没有指定一个安全策略,那么系统就用使用Java默认的安全策略。

Java在$JAVA_HOME/lib/security/java.policy提供默认的策略文件如下:

// =====:授权基于路径在"file:${{java.ext.dirs}}/*"的class和jar包,所有权限。
grant codeBase "file:${{java.ext.dirs}}/*" {
        permission java.security.AllPermission;
};

// =====:定义了所有JAVA程序都拥有的权限,包括停止线程、启动Socket 服务器、读取部分系统属性
grant {
        permission java.lang.RuntimePermission "stopThread";
        permission java.net.SocketPermission "localhost:0", "listen";
        permission java.net.SocketPermission "localhost:1099", "listen";
        permission java.util.PropertyPermission "java.version", "read";
        permission java.util.PropertyPermission "java.vendor", "read";
        
        // 省略...... 其他细粒度的授权
};

1.4 保护域ProtectionDomain

保护域可以理解为代码源和相应权限的一个组合。表示指派给一个代码源的所有权限。看概念,感觉和策略很像,其实策略要比这个大一点,保护域是一个代码源的一组权限,而策略是所有的代码源对应的所有的权限的关系。保护域就在于声明了比如由代码A可以做权限B这样的事情。

JVM中的每一个类都一定属于且仅属于一个保护域,这由ClassLoader在define class的时候决定。同时代码源所在保护域包含的权限集规定了一些权限,这个类就拥有这些权限。

2. 访问控制器的操作

AccessController类无法实例化,它仅向外部提供了静态方法,其中最重要的就是checkPermission(Permission p),该方法基于当前安装的Policy对象,判定当前的保护域是否拥有指定权限。安全管理器SecurityManager提供的一系列check***的方法,最后基本都是通过AccessController.checkPermission(Permission p)完成。

接下来我们使用代码来验证访问控制器的存在与使用:

public class SecurityManagerDemo {

    public static void main(String[] args) throws IOException {
        FilePermission permission = new FilePermission("D:\\test.txt", "read");
        try {
            AccessController.checkPermission(permission);
            FileReader fr = new FileReader(new File("D:\\test.txt"));
            BufferedReader reader = new BufferedReader(fr);
            String s;
            while ((s = reader.readLine()) != null) {
                System.out.println(s);
            }
        } catch (AccessControlException e) {
            System.out.println("没有访问的权限");
        }
    }
}

接下来我们修改java启动参数,加入-Djava.security.manager-Djava.security.policy=D:\\custom.policy两个参数,同时在D盘创建一个custom.policy安全配置文件。启动程序,程序输出:

输出结果

这里可以表明,沙箱机制已经启动了,同时由于我们没有在custom.policy文件中配置相应的读取权限,所以程序抛出了异常。

接下来,我们在custom.policy文件中声明对D盘文件的读取权限:

grant {
    permission java.io.FilePermission "D:\\*", "read";
};

再启动程序,程序运行如下:

输出结果

如上例。我们通过自定义policy文件修改了默认沙箱的安全策略,再通过启动参数开启沙箱模式。这样就可以构造我们自己想要的沙箱效果了。

3. doPrivilege

AccessController中还有一个比较使用的功能是doPrivilege(授权)假设一个保护域 A 有读文件的权限,另一个保护域 B 没有。那么通过 AccessController.doPrivileged 方法,可以将该权限临时授予B保护域的类。而这种授权是单向的。也就是说,它可以为调用它的代码授权,但是不能为它调用的代码授权。

import java.io.FilePermission;  
import java.security.AccessController;  
import java.security.Permission;  
import java.security.PrivilegedAction;  
  
public class Client  
{  
    public void doCheck() {  
        AccessController.doPrivileged( new  PrivilegedAction()  {  
            public Object run()  {  
                check();  
                return   null ;  
            }  
        });  
    }  
  
    private void check()  {  
        Permission perm = new FilePermission( "/1.txt" ,  "read" );  
        AccessController.checkPermission(perm);  
        System.out.println("TestService has permission");  
    }  
}  

把这个类打包成client.jar 放到/home/h/client/下
然后建立个 my.policy 文件,文件内容是:

grant codeBase  "file:/home/h/client/*"   {  
     permission java.io.FilePermission  "/1.txt","read";  
 }; 

配置文件的意思是 /home/h/client/ 下面的jar包或class类 可以读取/1.txt.

现在我们再创建一个项目:创建一个类来调用前面的Client

public class Server  
{  
    public static void main(String[] args)  
    {  
        Client c = new Client();  
        c.doCheck();  
    }  
}

运行这个Server类.注意这里要用上之前的my.policy文件
在vm参数中写上这样的:

-Djava.security.manager   
-Djava.security.policy=/home/h/my.policy  

运行,结果是:TestService has permission

在配置文件my.policy中我们没有允许server去读取/1.txt,但是现在却可以正常访问.这个就是 AccessController.doPrivileged的作用.

参考

猜你喜欢

转载自blog.csdn.net/Allen_Adolph/article/details/106479545