天天看点

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

标签: java 安全

[toc]

安全管理器在java语言中的作用就是检查操作是否有权限执行。是java沙箱的基础组件。我们一般所说的打开沙箱,也是加<code>-djava.security.manager</code>选项。

其实日常的很多api都涉及到安全管理器,它的工作原理一般是:

请求java api

java api使用安全管理器判断许可权限

通过则顺序执行,否则抛出一个exception。

比如在之前的“理解沙箱”这一章提到的,开启沙箱后,会限制文件访问,那这个代码是如何的呢?看下源码:

比较清晰的api会先去获取安全管理器,如果开启沙箱,则安全管理器不是空,检查checkread(name)。而checkread方法最内层的实现,其实利用了后面要说的访问控制器。

具体点,我们看下securitymanager的主要方法列表:

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

安全管理器可以自定义,作为核心api调用的部分,我们可以自己为自己的业务定制安全管理逻辑。举个例子如下:

注释掉代码中的注释行,系统打印null,然后正常退出。当我们打开注释,并且自己扩展一个securitymanager——mysm,它做的事情很简单,就是覆盖了checkexit方法,在系统退出时抛出一个“no exit”的异常。再执行,结果变成了

显然,安全管理器生效了。

揭开沙箱面纱,第一步是安全管理器,那么第二步就是访问控制器了。因为沙箱的所有check方法实现,都是基于accesscontroller的。

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

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

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

策略是一组权限的总称,用于确定权限应该用于哪些代码源。话说回来,代码源标识了类的来源,权限声明了具体的限制。那么策略就是将二者联系起来,策略类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进行。

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

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

了解了组成,再回头看accesscontroller。这是一个无法实例化的类——仅仅可以使用其static方法。accesscontroller最重要的方法就是checkpermission()方法,作用是基于已经安装的policy对象,能否得到某个权限。

回到[理解沙箱]()那一篇文章里的例子,fileinputstream的构造方法就利用securitymanager来checkread。而securitymanager的checkread方法则

这样来检查权限。

然而,accesscontroller的使用还是重度关联类加载器的。如果都是一个类加载器且都从一个保护域加载类,那么你构造的checkpermission的方法将正常返回。

当使用了其他类加载器或者使用了java扩展包时,这种情况比较普遍。accesscontroller另一个比较实用的功能是doprivilege(授权)。假设一个保护域a有读文件的权限,另一个保护域b没有。那么通过accesscontroller.doprivileged方法,可以将该权限临时授予b保护域的类。而这种授权是单向的。也就是说,它可以为调用它的代码授权,但是不能为它调用的代码授权。

classloader对安全模型有三方面的影响:第一,可以结合jvm定义名称空间,以保护java语言本身安全特性的完整性。第二,在必要时调用securitymanager保证代码在定义或者访问类时有适当的权限。第三,建立了权限与类对象之间的映射,这样accesscontroller就知道哪些类拥有哪些权限了。而这可以绕过建立自定义policy类,通过自定义classloader并在其中定义类权限而实现。

先来说说名称空间,其实就是包名,但是不同的是,不同的classloader可以装在相同包名的类,而这时,其实对于每个classloader,有一个自己的名字空间。为啥这么干?显然啊,就不说包冲突这事了,从安全角度看,你冒名顶替个com.sun.xx咋办?肯定得按照classloader来分。从不同网站加载的applet类,就是不同的classloader来做。

类加载器是个层次结构,最基础的是系统类加载器,下面有很多子类比如urlclassloader。加载一个类时,以委托的形式逐层询问——总结来就一句话:父亲优先。爹能加载,爹先来,不行再由儿子上。一旦为一个域的类定义类加载器,那么其他域的类加载器的整个祖先链路上不包含对应域,也就隔离了彼此的类加载。

总的来说,需要完成以下工作:

1, 询问安全管理器是否允许访问当前处理的类。如果不行,抛一个安全异常。这一步可选,一般在loadclass()方法开始处实现。对应accessclassinpackage权限。

2, 如果类装载器已经载入了此类,它将寻找以前定义的类对象,并返回该对象。这一步在loadclass()内部实现。

3,否则,类装载器将询问其父亲,递归查看父类装载器是否知道如何载入此类。因此总会是系统类加载器最先加载,从而避免了核心java api中的类被其他自定义的类冒充。这一步也在loadclass()里实现。

2和3对应代码如下

4,询问安全管理器是否允许程序创建当前处理的类。如果不行,则抛出一个安全异常。这一步可选,如果实现,则需要在findclass()的开始处完成。这一步不是在操作开始时完成,而是在询问父类装载器之后进行。这一步对应为defineclassinpackage权限。

5,向一个字节数组中读入类文件。读取文件以及创建字节数组的方式因类加载器不同而不同。在findclass()中完成。

6,为该类创建合适的保护域。保护域可以来自默认安全模型(即从策略文件中得到),也可以由类加载器扩展。还有一种方法是可以创建一个代码源对象,并采用其保护域定义。这一步也在findclass()中完成。

7,在findclass()方法中,通过调用defineclass()方法,可以由字节码构造一个class对象。如果使用的是第6步中的代码源,则需要调用getpermissions()方法查找与代码源相关的权限。defineclass()方法还保证了字节码必须通过字节码校验器的检查。

8,最后还需要解析该类。即它所直接引用的类也应由当前类加载器找到。只有直接引用的才算,作为实例变量、方法参数或局部变量来使用的类不算。这一步在loadclass()中完成。对应上面代码中的resovleclass()。

1,对应的代码如下:

urlclassloader的newinstance()方法会构造一个内部的工厂加载器类。这个类的loadclass()方法做了checkpackageaccess的事情。

2,3 两步与超级父类classloader相同。就是上面classloader的loadclass()做的事情。

4,5,6,7 四个步骤涉及到findclass()方法,urlclassloader覆盖了findclass()方法,但是最新版的jdk,其实将这几个步骤做的事情都在defineclass()里做掉了。里面的逻辑实现如下:

defineclass()的逻辑:

而这里面所有的defineclass都在classloader这个超级父类里做了实现。

通常的java的安全性,都是从类加载器、安全管理器和访问控制器之间的关系考虑的。一般来说类加载器的作用更重要。

如果需要灵活的安全策略,往往要自定义类加载器。自定义类加载器允许在定义类时调整安全策略。这与实现一个新的policy类相似。一般认为自定义类加载器会比修改一个policy类要容易。