13.3 权限控制
在一个主体通过了身份认证之后,程序中就具有了表示该主体的Subject类的对象。该Subject类的对象中包含了该主体的身份标识和凭证等信息。如果程序的实现中并不区分不同的用户,那么只进行身份认证就足够了。不过大多数程序会把用户划分成不同的组,每个组的用户可以执行的操作是不同的。在用户能够执行某些操作之前,需要先检查当前用户是否具有相应的权限。在访问控制中两个重要的概念是权限和策略。权限用来表示允许执行某些操作的能力,而策略用来说明权限的分配方式。如果在策略中为某个主体分配了某个权限,那么该主体可以执行由该权限对应的操作。
13.3.1 权限、策略与保护域
下面介绍与访问控制权限相关的权限、策略和保护域的概念。
1.权限
Java安全中的权限由java.security.Permission类及其子类来表示。每个权限都有一个名称,该名称的具体含义与权限类型相关。在创建Permission类的对象时,需要提供这个名称。通过getName方法可以获取权限的名称。某些权限有与之关联的动作列表。这些权限通常用名称来表示所操作的对象,用动作列表来表示在该对象上所能执行的操作的集合。比如文件操作权限java.io.FilePermission类,它的名称是文件的路径,而它的动作列表包括读取、写入和执行等。通过getActions方法可以获取权限的动作列表。只包含名称的权限适合于描述简单的“是”或“否”类型的访问控制要求。如果分配了只包含名称的权限,就说明可以执行对应的操作,而不会对操作的类型进行进一步的细分。
Permission类中最重要的是implies方法,它定义了权限之间的包含关系,是验证权限是否具备的基础。在分配权限时,不可能穷举所有可能的情况,只能从逻辑上进行判断。同一类型的权限的不同声明或不同类型的权限之间可能存在包含关系。以文件系统访问权限FilePermission类为例,如果一个主体拥有某个目录上的读取权限,那么该主体同样拥有对该目录下某个子目录或文件的读取权限。这种包含关系通过implies方法的实现来声明。implies方法的参数是另外一个Permission类的对象。如果当前Permission类的对象在逻辑上包含了作为参数的Permission类的对象,则implies方法返回值为true。如果“A.implies(B)”的返回值为true,则拥有权限A的主体就自动拥有权限B。对于FilePermission类来说,分配权限时通常只分配某个目录的权限,不可能为目录中的所有文件逐一分配权限。在FilePermission类的implies方法的实现中,如果当前FilePermission类的对象所对应的路径中包含了作为参数的FilePermission类的对象所对应的路径,则implies方法返回值为true。
在实际开发中,通常需要对一组权限进行处理。Java安全API中提供了两种不同的权限集合,一种是java.security.PermissionCollection类的对象,表示相同类别的权限;另外一种是java.security.Permissions类的对象,表示不同类别的权限。Permissions类是PermissionCollection类的子类。在需要使用权限集合时,应该先调用Permission类的newPermissionCollection方法来获取一个PermissionCollection类的对象。有些Permission类的子类会通过覆写newPermissionCollection方法来提供自定义的权限集合的实现,这是为了保证PermissionCollection类的implies方法提供正确的权限包含语义。如果newPermissionCollection方法的返回值为null,则调用者可以使用自己的权限集合实现方式;否则,应该直接使用newPermissionCollection方法的返回值。通过PermissionCollection类的add方法可以向集合中添加新的Permission类的对象,而通过implies方法可以判断该权限集合是否包含另外一个Permission类的对象。Permissions类的对象可以看成由多个PermissionCollection类的对象组成的不同类别的权限的集合。
Java标准库提供了一些常用的Permission类的子类。类java.security.AllPermission表示的是所有的权限。AllPermission类的implies方法的返回值总是true,分配此权限相当于禁用了访问权限控制,因此需要谨慎使用AllPermission类。类java.security.BasicPermission可以作为自定义的Permission类的实现基础。BasicPermission类的权限名称使用的是类似名称空间的形式,可以在名称的最后部分使用通配符,如“myapp.login”和“my.app.ui.*”等都是合法的权限名称。BasicPermission类的implies方法会负责处理通配符。标准库提供的很多权限实现类都是BasicPermission类的子类。
2.策略
策略是根据程序当前的访问控制权限设置来判断主体是否具有某个权限。判断的方式是基于显式声明的权限和权限之间的包含关系。类java.security.Policy是Java安全API中策略的表示。Policy类的对象的使用方式类似于登录过程中使用的Configuration类的对象。在同一时间内,只能有一个Policy类的对象处于活动状态。与权限相关的决策都通过当前活动的Policy类的对象来处理。通过Policy类的静态方法getPolicy和setPolicy可以获取和设置该活动对象。
Java平台提供的默认策略实现是基于文件来存储的。在JRE安装目录下的“lib/security/java.policy”文件是Java运行环境自身使用的策略文件。程序也可以提供自己的策略文件,通过虚拟机启动参数“java.security.policy”来指定文件的路径。策略文件的格式比较复杂,包含密钥、签名、主体身份标识和授权条目等内容,其中最重要的内容是授权条目。每一个授权条目表示的是显式声明的主体所具有的一个权限。在条目中包含权限的类名、名称和动作列表。代码清单13-6给出了策略文件的示例,以“permission”开始的每一行表示一个权限。
代码清单13-6 默认策略文件的示例
grant{
permission java.io.FilePermission"c:\tmp","write";
permission java.util.PropertyPermission"user.*","read, write";
};
由于策略文件的格式比较复杂,因此JDK中提供了可视化policytool工具对策略文件进行编辑。如果需要使用数据库或其他方式的存储策略,那么可以继承Policy类来提供自己的实现。Policy类的对象的使用方式是调用其implies方法来判断参数中的权限是否符合策略的要求。不过,在进行访问控制权限检查时,一般不需要显式调用当前活动的Policy类的对象的implies方法,而是使用更加方便的方法。
3.保护域
保护域是Java安全机制中的基本概念。保护域是一个主体所能访问的对象的集合。程序通过策略中的声明把不同的权限分配给不同的主体,这些权限定义了主体所能访问的对象的集合。一般将Java平台分成两类保护域:一类是系统域,用来保护系统中的资源,如文件系统、网络连接、屏幕显示和键盘鼠标等;另一类是应用域,由应用根据需要来划分。
每个保护域中包含一组Java类、主体的身份标识和权限列表。当访问请求的来源是这些身份标识所对应的主体时,这一组Java类的对象实例自动拥有给定权限列表中的所有权限。保护域的权限既可以是固定的,也可以根据策略动态变化。类java.security.ProtectionDomain表示保护域,它的两个构造方法分别用来支持静态和动态的权限。在创建ProtectionDomain类的对象时需要提供java.security.CodeSource类的对象。CodeSource类表示的是代码的来源,包含表示来源地址的java.net.URL类的对象及对该来源的代码进行验证的数字证书链或代码签名工具。比如从远程服务器下载到浏览器中执行的Java Applet,它的代码来源是Applet中jar包的URL。对于Applet的jar包,通常需要进行数字签名以保证来源是真实有效的。在创建ProtectionDomain类的对象时,除了CodeSource类的对象之外,还需要提供表示该保护域所具有的权限的PermissionCollection类的对象。如果只提供这两个参数,那么ProtectionDomain类的对象所表示的保护域中的权限是静态的,在创建后不会改变,在进行权限检查时也不会考虑当前活动的Policy类的对象。如果要求权限根据策略动态变化,那么需要提供额外的两个参数:一个参数是ClassLoader类的对象,另外一个参数是包含主体身份标识的Principal接口的实现对象的数组。使用这个构造方法创建出来的ProtectionDomain类的对象在进行权限检查时,不但要考虑构造方法的参数中给出的静态权限,还要考虑通过当前活动的Policy类的对象所赋予的权限。在具体使用中,ProtectionDomain类的对象使用implies方法来检查一个Permission类的对象所对应的权限是否被该保护域所允许。