Java安全:SecurityManager与AccessController

前言

什么是安全?

  • 程序不能恶意破坏用户计算机的环境,好比特洛伊木马等可自我进行复制的恶意程序。
  • 程序不可获取主机及其所在网络的私密信息。
  • 程序的提供者和使用者的身份须要经过特殊验证。
  • 程序所涉及的数据在传输、持久化后都应是被加密的。
  • 程序的操做有相关规则限制,而且不能耗费过多的系统资源。

保护计算机上的信息不被非法获取和修改时Java最初的,也是最基本的设计目标,但同时还要保证Java程序在主机上的运行不受影响。java

Java安全方面的支持

JDK自己提供了基本的安全方面的功能,好比可配置的安全策略、生成消息摘要、生成数字签名等等。同时,Java也有一些扩展程序,更加全面地支撑了整个安全体系。web

Java加密扩展包(JCE)提供了密码、安全密钥交换、安全消息摘要、密钥管理系统等功能。数组

Java安全套接字扩展包(JSSE)提供了SSL(安全套接字层)的加密功能,保证了与SSL服务器或SSL客户的通讯安全。安全

Java鉴别与受权服务(JAAS)能够在Java平台上提供用户鉴别,而且容许开发者根据用户提供的鉴别信任状准许或拒绝用户对程序的访问。bash

1、Java沙箱

如何理解?程序要在主机上安装,那么主机必须为该程序提供一个运行的场所(运行环境),该场所支持程序运行的同时,也限制其能够获取的资源。就比如小孩子去你家玩,你须要提供一个空间让她玩耍且不会受伤,同时还要保证你女友新买的化妆镜不会被孩子打碎。服务器

Java沙箱负责保护一些系统资源,并且保护级别是不一样的。网络

  • 内部资源,如本地内存;
  • 外部资源,如访问其文件系统或是在同一局域网的其余机器;
  • 对于运行的组件(applet),能够访问其web服务器;
  • 主机经过网络传输到磁盘的数据流。

通常来说,沙箱的默认状态容许其中的程序访问CPU、内存等资源,以及其上装在的Web服务器。若沙箱彻底开放,则其中程序的权限与主机相同。多线程

当前最新的安全机制实现,则引入了域 (Domain) 的概念,能够理解为将沙箱细分为多个具体的小沙箱。虚拟机会把全部代码加载到不一样的系统域和应用域,系统域部分专门负责与关键资源进行交互,而各个应用域部分则经过系统域的部分代理来对各类须要的资源进行访问。虚拟机中不一样的受保护域 (Protected Domain),对应不同的权限 (Permission)。存在于不一样域中的类文件就具备了当前域的所有权限,以下图所示: app

沙箱的实现取决于下面三方面的内容:dom

  • 安全管理器,利用其提供的机制,可使Java API肯定与安全相关的操做是否容许执行。
  • 存取控制器,安全管理器默认实现的基础。
  • 类装载器,能够实现安全策略和类的封装。

从Java API的角度去看,应用程序的安全策略是由安全管理器去管理的。安全管理器决定应用是否能够执行某项操做。这些操做具体是否能够执行的依据,是看其可否对一些比较重要的系统资源进行访问,而这项验证由存取控制器进行管控。这么看来,存取控制器是安全管理器的基础实现,安全管理器能作的,存取控制器也能够作。那么问题来了,为何还须要安全管理器?

Java2之前是没有存取控制器的,那个时候安全管理器利用其内部逻辑决定应用的安全策略,若要调整安全策略,必须修改安全管理器自己。Java2开始,安全管理器将这些工做交由存取控制器,存取控制器能够利用策略文件灵活地指定安全策略,同时还提供了一个更简单的方法,实现了更细粒度地将特定权限授予特定的类。所以,Java2以前的程序都是利用安全管理器的接口实现系统安全的,这意味着安全管理器是不能修改的,那么引入的存取控制器并不能彻底替代安全管理器。二者的关系以下图:

2、 安全管理器

安全管理器是Java API和应用程序之间的“第三方权威机构”。比如贷款时,银行会根据央行征信系统查询用户的信用状况决定是否放款。Java应用程序请求Java API完成某个操做,Java API会向安全管理器询问是否能够执行,安全管理器若不但愿执行该操做,会抛一个异常给Java API,不然Java API将完成操做并正常返回。

1.初识SecurityManager

SecurityManager类是Java API中一个至关关键的类,它为其余Java API提供相应的接口,使之能够检查某项操做可否执行,充当了安全管理器的角色。咱们从下面的代码来看安全管理器是如何工做的?

public static void main(String[] args) {
        String s;
        try {
            FileReader fr = new FileReader(new File("E:\\test.txt"));
            BufferedReader br = new BufferedReader(fr);
            while ((s = br.readLine()) != null)
                System.out.println(s);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
复制代码

第一步,在建立FileReader对象的时候会先根据File对象建立FileInputStream实例,源码以下:

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.attach(this);
        path = name;
        open(name);
    }
复制代码

第二步,Java API但愿建立一个读取File的字节流对象,首先必须获取当前系统的安全管理器,而后经过安全管理器进行操做校验,若经过,再调用私有方法真正执行操做(open()是FileInputStream类的私有实例方法),若校验失败,则抛出一个安全异常,层层上抛,直至用户面前。

public void checkRead(String file) {
        checkPermission(new FilePermission(file,SecurityConstants.FILE_READ_ACTION));
    }
    
    public void checkPermission(Permission perm) {
        java.security.AccessController.checkPermission(perm);
    }
复制代码

上面即是此处涉及SecurityManager的两个方法源码(jdk1.8)。能够看到,SecurityManager对访问文件的校验,最终是交由存取控制器实现的,AccessController在检查权限期间则会抛出一个AccessControlException异常告诉调用者校验失败。该异常继承自SecurityException,SecurityException又继承自RuntimeException,所以AccessControlException是一个运行期异常。一般,调用方法每每涉及到一系列其余方法的调用,一旦出现安全异常,异常会顺着调用链传向顶部方法,最后线程中断结束。

2.操做SecurityManager

通常状况下,安全管理器是默认没有被安装的。所以,上面建立FileInputStream的源码中,security==null,是不会执行checkRead的(感兴趣的同窗能够在main方法里直接使用System提供的方法进行验证)。System类为用户操做安全管理器提供了两个方法。

public static SecurityManager getSecurityManager() 该方法用于得到当前安装的安全管理器引用,若未安装,返回null。 public static void setSecurityManager(final SecurityManager s) 该方法用于将指定的安全管理器的实例设置为系统的安全管理器。

上面读取test.txt的代码时能够正常执行的,控制台会一行一行打印文件的内容。在配置上自定义的安全管理器(继承SecurityManager,重写checkRead方法)后,再看执行结果。

public class Main {

    class SecurityManagerImpl extends SecurityManager {

        public void checkRead(String file) {
            throw new SecurityException();
        }
    }
    
    public static void main(String[] args) {
        System.out.println("CurrentSecurityManager is " + System.getSecurityManager());
        Main m = new Main();
        System.setSecurityManager(m.new SecurityManagerImpl());
        System.out.println("CurrentSecurityManager is " + System.getSecurityManager());
        String s;
        try {
            FileReader fr = new FileReader(new File("E:\\test.txt"));
            BufferedReader br = new BufferedReader(fr);
            while ((s = br.readLine()) != null) {
                System.out.println(s);
            }
        }catch (IOException e) {
            e.printStackTrace();
        }
    }
}
复制代码

执行结果:

CurrentSecurityManager is null
CurrentSecurityManager is Main$SecurityManagerImpl@135fbaa4
Exception in thread "main" java.lang.SecurityException
	at Main$SecurityManagerImpl.checkRead(Main.java:10)
	at java.io.FileInputStream.<init>(FileInputStream.java:127)
	at java.io.FileReader.<init>(FileReader.java:72)
	at Main.main(Main.java:21)
复制代码

注:若是想要java环境安装默认的管理器,一种方式如上设置默认安全管理器的实例,另外一种方式也能够在配置JVM 运行参数的时候加上-Djava.security.manager。通常推荐后者,由于能够不用去改动代码,同时能够灵活的经过再配置一个-Djava.security.policy="x:/xxx.policy"参数的方式指定安全策略文件。

3.使用SecurityManager

安全管理器提供了各个方面的安全检查的公共方法,容许任意调用。核心Java API中有不少方法,直接或间接调用安全管理器提供的方法实现各自的安全检查操做。在安全检查中还存在一个概念,可信类与不可信类。显然,一个类不是可信类就是不可信类。

若是一个类是核心Java API类,或者它显示地拥有执行某项操做的权限,那么这个类就是可信类。

3.1 文件访问相关的安全检查方法

这里的文件访问指的是局域网中文件访问的处理,并不仅仅是本地磁盘上的文件访问。

public void checkRead(FileDescriptor fd)

public void checkRead(String file)

public void checkRead(String file, Object context)

检查程序可否读取指定文件。不一样入参表明不一样的校验方式。第一个方法校验当前保护域是否拥有名为readFileDescriptor的运行时权限,第二个方法检验当前保护域是否拥有指定文件的读权限,第三个方法和第二个方法相同,不一样的是在指定的存取控制器上下文中检验。

public void checkWrite(FileDescriptor fd)

public void checkWrite(String file)

检查是否容许程序写指定文件。第一个方法校验当前保护域是否拥有名为writeFileDescriptor的运行时权限,第二个方法检验当前保护域是否拥有指定文件的写权限。

public void checkDelete(String file)

检查是否容许程序删除指定文件。检验当前保护域是否拥有指定文件的删除权限。

下表简单列出了Java API中直接调用了checkRead()、checkWrite()和checkDelete()的方法。

3.2 网络访问相关的安全检查方法

Java中的网络访问通常是经过打开一个网络套接字实现的。网络套接字在逻辑上分为客户套接字和服务器套接字两类。

public void checkConnect(String host, int port)

public void checkConnect(String host, int port, Object context)

检查程序可否向指定的主机上指定的端口打开一个客户套接字。检验当前保护域是否拥有指定主机名和端口的链接权限。

public void checkListen(int port)

检查程序可否建立一个监听特定端口的服务器套接字。

public void checkAccept(String host, int port)

检查程序可否在当前服务器套接字上接收指定主机和端口发出的客户链接。

public void checkMulticast(InetAddress maddr)

检查程序可否在指定的多播地址上建立一个多播套接字。

public void checkSetFactory()

检查程序可否修改默认的套接字实现。使用Socket建立套接字时,会由套接字工厂得到一个新的套接字。程序能够经过安装套接字工厂扩展不一样语义的套接字,这就要求保护域拥有名为setFactory的运行时权限。

3.3 保护Java虚拟机的安全检查方法

对于不可信类,有必要去提供一些方法避免它们绕过安全管理器和Java API,从而保证Java虚拟机的安全。

public void checkCreateClassLoader()

检查当前保护域是否拥有creatClassLoader的运行时权限,肯定程序可否建立一个类加载器。

public void checkExec(String cmd)

检查保护域是否拥有指定命令的执行权限,肯定程序可否执行一个系统命令。

public void checkLink(String lib)

检查程序可否程序可否链入虚拟机中连接共享库(本地代码经过该库执行)。

public void checkExit(int status)

检查程序是否有权限关闭虚拟机。

public void checkPermission(Permission perm) public void checkPermission(Permission perm, Object context)

检查当前保护域(能够理解为当前线程)是否拥有指定的权限。

3.4 保护程序线程的安全检查方法

一个Java程序的运行依赖于不少线程。除了程序自己的线程,虚拟机会自动为用户建立不少系统级的线程,好比垃圾回收、管理相关接口的输入输出请求等等。不可信类是不能管理这些影响程序的线程的。

public void checkAccess(Thread t)

public void checkAccess(ThreadGroup g)

检查是否容许修改指定线程(线程组及组内线程)的状态。

3.5 保护系统资源的安全检查方法

Java程序是能够访问一些系统级的资源的,好比打印任务、剪贴板、系统属性等等。出于安全考虑,不可信类是不能访问这些资源的。

public void checkPrintJobAccess()

检查程序可否访问用户打印机(queuePrintJob-运行时权限)

public void checkSystemClipboardAccess()

检查程序是否能够访问系统剪贴板(accessClipboard-AWT权限)

public void checkAwtEventQueueAccess()

检查程序可否得到系统时间队列(accessEventQueue-AWT权限)

public void checkPropertiesAccess() public void checkPropertyAccess(String key)

检查程序嫩否获取Java虚拟机拥有的系统属性信息

public boolean checkTopLevelWindow(Object window)

检查程序可否在桌面新建一个窗口

3.6 保护Java 安全机制自己的安全检查方法

public void checkMemberAccess(Class<?> clazz, int which)

反射时检查程序可否访问类的成员。

public void checkSecurityAccess(String target)

检查程序可否执行安全有关的操做。

public void checkPackageAccess(String pkg)

public void checkPackageDefinition(String pkg)

在使用类装载器装载某个类且指定了包名时,会检查程序可否访问指定包下的内容。

3、存取控制器

核心Java API由安全管理器提供安全策略,可是大多数安全管理器的实现是基于存取控制器的。

1. 创建存取控制器的基础

代码源:对于从其上装载Java类的地址,须要用代码源进行封装

权限:要实现某个特定操做,须要权限封装相应的请求

策略:对指定代码源授予相应的权限,策略能够表示为对全部权限的封装

保护域:对代码源及该代码源相应权限的封装

1.1 CodeSource

代码源对象表示从其上装在类的URL地址,以及类的签名相关信息,由类装载器负责建立和管理。

public CodeSource(URL url, Certificate certs[])

构造器函数,针对指定url装载获得的代码,建立一个代码源对象。第二个参数是证书数组,可选,用来指定公开密钥,该密钥能够实现对代码的签名。

public boolean implies(CodeSource codesource)

按照权限类的(Permission)的要求,判断当前代码源可否用来表示参数所指定的代码源。一个代码源能表示另外一个代码源的条件是,前者必须包括后者的全部证书,并且由前者的URL能够得到后者地URL。

1.2 Permission

Permission类的实例对象就是权限对象,它是存取控制器处理的基本实体。Permission类是一个抽象类,不一样的实现类在安全策略文件中体现为不一样的权限类型。Permission类的一个实例表明一个特定的权限,一组特定的权限则由Permissions的一个实例表示。

要实现自定义权限类的时候须要继承Permission类的,其抽象方法以下:

//校验权限参数对象拥有的权限名和权限操做是否符合建立对象时的设置是否一致
public abstract boolean implies(Permission permission);
//比较两个权限对象的类型、权限名以及权限操做
public abstract boolean equals(Object obj);
public abstract int hashCode();
//返回建立对象时设置的权限操做,未设置返回空字符串
public abstract String getActions();
复制代码

1.3 Policy

存取控制器须要肯定权限应用于哪些代码源,从而为其提供相应的功能,这就是所谓的安全策略。Java使用了Policy对安全策略进行了封装,默认的安全策略类由sun.security.provider.PolicyFile提供,该类基于jdk中配置的策略文件(%JAVA_HOME%/ jre/lib/security/java.policy)进行对特定代码源的权限配置。默认配置以下:

// Standard extensions get all permissions by default

grant codeBase "file:${{java.ext.dirs}}/*" {
        permission java.security.AllPermission;
};

// default permissions granted to all domains

grant {
        // Allows any thread to stop itself using the java.lang.Thread.stop()
        // method that takes no argument.
        // Note that this permission is granted by default only to remain
        // backwards compatible.
        // It is strongly recommended that you either remove this permission
        // from this policy file or further restrict it to code sources
        // that you specify, because Thread.stop() is potentially unsafe.
        // See the API specification of java.lang.Thread.stop() for more
        // information.
        permission java.lang.RuntimePermission "stopThread";
        // allows anyone to listen on dynamic ports
        permission java.net.SocketPermission "localhost:0", "listen";

        // "standard" properies that can be read by anyone

        permission java.util.PropertyPermission "java.version", "read";
        permission java.util.PropertyPermission "java.vendor", "read";
        permission java.util.PropertyPermission "java.vendor.url", "read";
        permission java.util.PropertyPermission "java.class.version", "read";
        permission java.util.PropertyPermission "os.name", "read";
        permission java.util.PropertyPermission "os.version", "read";
        permission java.util.PropertyPermission "os.arch", "read";
        permission java.util.PropertyPermission "file.separator", "read";
        permission java.util.PropertyPermission "path.separator", "read";
        permission java.util.PropertyPermission "line.separator", "read";

        permission java.util.PropertyPermission "java.specification.version", "read";
        permission java.util.PropertyPermission "java.specification.vendor", "read";
        permission java.util.PropertyPermission "java.specification.name", "read";

        permission java.util.PropertyPermission "java.vm.specification.version", "read";
        permission java.util.PropertyPermission "java.vm.specification.vendor", "read";
        permission java.util.PropertyPermission "java.vm.specification.name", "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";
};
复制代码

第一个grant定义了系统属性${{java.ext.dirs}}路径下的全部的class及jar(/*号表示全部class和jar,若是只是/则表示全部class但不包括jar)拥有全部的操做权限(java.security.AllPermission),java.ext.dirs对应路径为%JAVA_HOME%/jre/lib/ext目录,而第二个grant后面定义了全部JAVA程序都拥有的权限,包括中止线程、启动Socket 服务器、读取部分系统属性。

Policy类提供addStaticPerms(PermissionCollection perms, PermissionCollection statics)方法添加特定权限集给策略对象内部的权限集,也提供public PermissionCollection getPermissions(CodeSource codesource)方法设置安全策略的权限集给来自特定代码源的类。

虚拟机中任何状况下只能安装一个安全策略类的实例,可是能够经过Policy.setPolicy(Policy p)替换当前系统的安全策略,也能够经过Policy.getPolicy()得到程序当前的安全策略类。

1.4 ProtectionDomain

保护域就是一个受权项,能够理解为是代码源和对应权限的组合。虚拟机中每一个类都属于且仅属于一个保护域,由代码源指定的地址装载获得,同时代码源所在保护域包含的权限集规定了一些权限,这个类就拥有这些权限。保护域的构造方法以下:

public ProtectionDomain(CodeSource codesource,PermissionCollection permissions)

2. 存取控制器AccessController

AccessController类的构造器是私有的,所以不能对其进行实例化。它向外部提供了一些静态方法,其中最关键的就是checkPermission(Permission p),该方法基于当前安装的Policy对象,断定当前保护欲是否拥有指定权限。安全管理器SecurityManager提供的一系列check***的方法,最后基本都是经过AccessController.checkPermission(Permission p)完成。

public static void main(String[] args) {
        System.setSecurityManager(new SecurityManager());
        SocketPermission sp = new SocketPermission(
                "127.0.0.1:6000", "connect");
        try {
            AccessController.checkPermission(sp);
            System.out.println("Ok to open socket");
        } catch (AccessControlException ace) {
            System.out.println(ace);
        }
    }
复制代码

上面的代码首先安装了默认的安全管理器,而后实例化了一个链接本地6000端口的权限对象,最后经过存取控制器检查。 打印结果以下:

java.security.AccessControlException: access denied ("java.net.SocketPermission" "127.0.0.1:6000" "connect,resolve")
复制代码

存取控制器抛出了一个异常,提示没有链接该地址的权限。在默认的安全策略文件上配置此端口的链接权限:

permission java.net.SocketPermission "127.0.0.1:6000", "connect";

打印结果:

Ok to open socket

实际工做中,可能会面临多个项目之间的方法调用。假设有两个项目A和B,A项目中的TestA类中有testA()方法内部调用了B项目中的TestB类的testB()方法,去打开一个项目B所在服务器的套接字。在权限校验时,要想此种调用正常操做。须要在A项目所在虚拟机的安全策略文件中配置TestA类打开项目B所在服务器制定端口的链接权限,同时还要在B项目所在虚拟机的安全策略文件中配置TestB类打开同一地址及端口的链接权限。这种操做方式当然能够,可是显然太复杂且不可预计。AccessController提供了doPivileged()方法,为调用者临时开放权限,可是要求被调用者必须有对应操做的权限。

相关文章
相关标签/搜索