美文网首页程序员
Java 安全模型

Java 安全模型

作者: 河北漂 | 来源:发表于2018-01-25 16:22 被阅读19次

    Java程序在执行中会操作系统的一系列资源,如CPU,内存,文件系统,网络等等,这样会给程序运行的环境本身带来一些安全隐患,Java为了防止一些非法操作的发生,定制了安全模型——Java的沙箱(sandbox)。

    1.什么是沙箱

    沙箱机制就是将 Java 代码限定在虚拟机 (JVM) 特定的运行范围中,并且严格限制代码对本地系统的资源访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏。

    Sandbox.png

    2.沙箱的使用

    2.1沙箱的开启

    沙箱默认是不启动的,如果想使用的话,需要在启动JVM时设定参数:

    -Djava.security.manager

    或者在程序启动入口Main方法处增加以下代码:

    System.setSecurityManager(new SecurityManager());

    以上两种方式是等价的。

    接下来验证沙箱的启动效果,执行代码,比如读取文件:

    FileReader fileReader = new FileReader(new File("unExist.txt"));

    会得到异常:

    java.security.AccessControlException: access denied ("java.io.FilePermission" "unExist.txt" "read")

    这说明沙箱生效,因为当前执行环境,没有读取文件系统的权限。代码想去读取,就报异常了。

    2.2沙箱的配置

    那么沙箱究竟有哪些权限,是在哪里配置查询的呢?
    JDK有一个默认的安全策略文件,就是 $JAVA_HOME/jre/lib/security/java.policy ,里面有默认的权限。
    内容基本如下:

    grant {
    permission java.lang.RuntimePermission "stopThread";
    permission java.util.PropertyPermission "java.version", "read";
    permission java.util.PropertyPermission "java.vm.version", "read";
    permission java.util.PropertyPermission "java.vm.vendor", "read";
    permission java.util.PropertyPermission "java.vm.name", "read";
    ......
    };

    当然策略文件也可以自定义,再启动JVM的时候,通过这个参数:

    -Djava.security.policy=my.policy (文件的路径)

    配置的基本原则:

    • 没有配置的权限表示没有。
    • 只能配置有什么权限,不能配置禁止做什么。
    • 同一种权限可多次配置,取并集。
    • 统一资源的多种权限可用逗号分割。

    另外,自定义的策略文件,因为原生的grant语法不是很好用,可以考虑pro-grade二方包。

    3.沙箱的基本原理

    沙箱的实现非常暴力,其实就是设置了一个开关(SecurityManager),在所有的Code在访问操作系统和本地资源的时候,检查这个开关有没有启动,如果启动了就检查有没有操作当前资源的权限,没权限就抛异常。

    JDK把检查权限的代码,穿插进各种访问资源的API中。还拿读取文件来举例子,以下是FileInputStream的一段源码:

    public FileInputStream(File file) throws FileNotFoundException {
            String name = (file != null ? file.getPath() : null);
            //这里就是判断有没有开启沙箱
            SecurityManager security = System.getSecurityManager();
            if (security != null) {
                //开启了沙箱,判断有没有读取file的权限
                security.checkRead(name);
            }
            if (name == null) {
                throw new NullPointerException();
            }
            if (file.isInvalid()) {
                throw new FileNotFoundException("Invalid file path");
            }
            fd = new FileDescriptor();
            fd.attach(this);
            path = name;
            open(name);
        }
    

    以上代码,如果有权限,业务逻辑正常;如果没有权限,则会抛出异常(上面提到)。

    4.域 (Domain)

    设想一个问题:同一个资源,本地代码有访问权限,但是远程的代码没有权限,JVM就一个,沙箱开关就一个,那怎么才能做到呢?

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

    这里要说一下权限和域和类之间的关系:

    Domain.png

    在Class.java的源码中的ProtectionDomain就是存储对应的域。

    private native java.security.ProtectionDomain getProtectionDomain0();

    域的概念说完了,在来看上面的问题就明白了:本地代码和远程代码虽然是同一个JVM,同一个沙箱,但是CLassLoader却是不一样,分配的Domain也就可以不一样,权限就自然就区分开了。

    还有一个问题。调用方的权限是分开了,那被调用方的代码都是这一句:

    SecurityManager security = System.getSecurityManager();
    if (security != null) {
        security.checkRead(name);
    }
    

    被调用方是怎么知道是谁调用的呢?

    在SecurityManager的检查权限的代码是这样的:

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

    AccessController中:

    // 省略原来判断空的逻辑,方便阅读
    public static void checkPermission(Permission perm)
            throws AccessControlException{
            //getStackAccessControlContext()是一个native方法,得到调用stack
            AccessControlContext stack = getStackAccessControlContext();
            AccessControlContext acc = stack.optimize();
            acc.checkPermission(perm);
    }
    

    AccessControlContext中:

    //记录当前调用链的所有域
    private ProtectionDomain context[];
    public static void checkPermission(Permission perm)
            throws AccessControlException{
            // 省略原来Debug,方便阅读
            for (int i=0; i< context.length; i++) {
                if (context[i] != null &&  !context[i].implies(perm)) {
                    throw new AccessControlException("access denied "+perm, perm);
                }
            }
    }
    

    读到这里,答案已经很明确了:
    在校验权限的时候,JVM会拿到调用方的域(native方法),用来校验权限;如果出现DomainA中的类A.class调用DomainB中类B.class,最后由B.class去访问资源,那么被调用方拿到的是一个域的数组,必须DomainA和DomainB都有权限才访问资源,换句话说,就是取调方权限的交集。

    至此权限校验的问题搞清楚了。

    但是还有个功能要提一句,就是AccessController的特权功能,AccessController有很多native的方法,类似这种

    public static native <T> T doPrivileged(PrivilegedAction<T> action,
                                                AccessControlContext context);
    

    有什么用呢?提供给其他没有权限的域访问的。
    举个例子,比如DomainA不能读取文件,但是DomainB可以,可是通过DomainA调用DomainB来读取文件,权限取交集,还是不能读取。这个时候,就可以在DomainB中声明一个方法,这个方法内部用AccessController.doPrivileged来读取文件,就能突破权限的校验。

    AccessController.doPrivileged(new PrivilegedAction<String>() {  
        public String run() { 
            readFile(fileName); 
            return null; 
        } 
    }); 
    

    5.结尾

    以上简单的介绍了安全模型,安全模型在平时的应用开发中,比较少用。因为平台,容器都已经做好了。所以在做容器开发,网络开发,有执行三方的Java代码的场景,可能会遇到。
    当然,在做三方的Jar包的时候,如果需要访问什么系统资源,就应该考虑到环境是沙箱的情况,用AccessController.doPrivileged来处理一些场景。

    相关文章

      网友评论

        本文标题:Java 安全模型

        本文链接:https://www.haomeiwen.com/subject/pgleaxtx.html