Java API中有很多都使用了SecurityManager,这到底是什么玩意?最近看公司的产品的源码,也有不少SecurityManager、AccessControlContext等相关的代码,只是知道它们与安全有关,但是它们到底是怎么一回事呢?Spring也有一个Security框架,与Java Security有什么关联呢?另外有经验的开发人员调试程序时可能会查看ProtectionDomain、CodeSource,这两者又是什么呢?
Java Sandbox
提到Java Security,就不得不说Java Sandbox模型。
Java2 Security Model:
Java2平台上,加载类时,会形成不同的sandbox,同时也会根据相关的security policy,为这些sandbox生成不同的安全策略,这些安全策略会在应用程序执行时,进行检查,以保护资源被恶意的操作。
这张图指出了Java应用程序的真实的执行过程。
1) 编译期强制规则验证,而后生成class file
Java的强制性规则有:
A: private, protected, default, public 。这个都知道,是关系到可见性,是对应用程序中内存资源的保护。
B: final的变量初始化后不能被改变
C: 变量要先初始化后使用
以及一些其他的规则,通过这些规则验证后,就生成class file,也就是常说的字节码文件。
2)ClassLoader加载class file后定义类生成Class对象
类加载器也是一道坎,不是说你让它加载,它就加载的,它也是要进行验证的。
假如骇客写了一些java文件编译后放到classpath目录下,或者是将jdk中自带某些核心API反编译后进行某些修改,覆盖原有文件,这样对程序的危害可以极大的。所以类加载时,也是有必要进行检查的。
从这张图片可以看出在类加载器定义类的过程也会对字节码进行检查的,下面可以看一下ClassLoader中defineClass的过程:
protected final Class<?> defineClass(String name, byte[] b, int off, int len, ProtectionDomain protectionDomain) throws ClassFormatError { // 检查类加载器是否初始化 check(); // 形成为该类生成protectionDomain和codesource protectionDomain = preDefineClass(name, protectionDomain); Class c = null; String source = defineClassSourceLocation(protectionDomain); // 真实的定义Class的过程,这个方法是native的,字节码检查的过程也是这里进行的,这里是看不到的,也是不能让我们看到的,如果我们可见,就可以自定义,这样检查就形同虚设。 try { c = defineClass1(name, b, off, len, protectionDomain, source); } catch (ClassFormatError cfe) { c = defineTransformedClass(name, b, off, len, protectionDomain, cfe, source); } // 完善证书等信息 postDefineClass(c, protectionDomain); return c; }
既然代码层面,看不到如何检查字节码的,那么至少可以来了解一下,到底做了哪些检查呢?
D:检查class file的格式是否正确,JVM Specification 中说明了class file的格式,感兴趣的话可以到官网下载看看。例如:class file要有正确的长度、魔数 等。
魔数用于确定文件类型,UNIX系统不是根据扩展名来确定文件类型的,就是根据这个魔数来的。想要知道class file的魔数、以及是怎么定义的,可以参考《深入理解Java虚拟机》。
E: final的类没有子类
F: 原生类型的数据有无不合法的类型转换(E.G.: int to Object)
G: 引用类型的数据有无不合法的类型转换,例如将父类对象转换为子类类型。
H: 有没有操作数出现栈溢出现象
等。
其实还有两种检查,这两种是在运行时进行的:
I: 数组不能越界
J: 数据不能强制转化为其他不相干的类型
在定义类的过程中,还产生了与这个类相关联的ProtectionDomain。Java Security模块的设计如下图所示。
但并不是所有的ClassLoader都会生成ProtectionDomain。例如我前之前的一篇博客中定义的那个类加载器,又或者时bootstrapClassLoader。 只有继承了SecurClassLoader的ClassLoader在defineClass时都会生成相关联的ProtectionDomain, 一般情况下我们自定义ClassLoader时都会继承UrlClassLoader,而UrlClassLoader又继承了SecurClassLoader,所以我们定义的ClassLoader在执行defineClass时一般都会生成ProtectionDomain。
ProtectionDomain的设计模型是很重要的,接下来要说的AccessController和SecurityManager都是在ProtectionDomain的基础上才有所作为的。所以ProtectionDomain就在类加载时就确立。
默认情况下,一个jar包就对应一个ProtectionDomain。
网上关于Java Security方面的教程,说的最多莫过于Policy了,因为它是配置安全策略的。我们可能不会去定义Permission(Java中定义的Permission已经够我们使用),但是我们不可或缺的要去配置安全策略,来使用这些Permission为我们服务。
3)应用程序访问相关资源
3.1 SecurityManager#checkPermission()
Java提供了安全模型,我们在程序中如何使用呢?
一般来说都是通过SecurityManager来完成的,使用方式为:
SecurityManager sm = getSecurityManager(); if (sm != null) { // sm.checkPermission(); }
例如:
System.getProperty(String key)
public static String getProperty(String key) { checkKey(key); SecurityManager sm = getSecurityManager(); if (sm != null) { sm.checkPropertyAccess(key); } return props.getProperty(key); }
例如:
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(); } fd = new FileDescriptor(); open(name); }
默认情况下,我们的程序并没有开启Java的安全策略。想要看看开启安全策略后你的应用程序会是什么样的,可以使用JVM参数:-Djava.security.manager 。
如果要使用代码来开启,可以使用System.setSecurityManager(securitymanager)来启动。
在代码中只要像上面那样简单的写上两三行代码就可以检查相应的权限了。那么它们的执行过程是什么呢?
SecurityManager中所有检查权限相关的方法都会调用SecurityManager的checkPermission方法,下面的这个时序图说明了SecurityManager#checkPermission(Permission)的执行过程。
从这个图上也能看到最后还是Permission#implies起作用的。
3.2 AccessController.doPrivileged()
有时我们还会在代码中看到使用AccessController.doPrivileged()方法的,这个又是做什么呢?
假设有下列一个应用场景:有一个ProtectionDomain的CodeSource是com目录,在它下面有三个目录:core,moduleA,web,在这个 ProtectionDomain中,对所有的文件都有read权限,只有web目录下的resource目录下的文件,可以有write权限。现在有一需求,要在core目录下的某个文件有write权限。
/com |--core |--moduleA |--web |--bean |--service |--dao |--resource
我们的程序中肯定会这样写:new FileOutputStream(File file)。上面已经粘出来FileInputStream(File file)实现过程。也就是说检查对该文件有无读权限。那么对应的FileOutputStream中肯定也会有检查是否有写权限的过程。上面的描述中已经知道,对于core下没有写权限的,所以我们的需求是无法满足的。那怎么办呢?
AccessController.doPrivileged()就可以帮肋完成上述任务。
FileOutputStream fos=null; String filepath=”./com/core/xx”; fos=AccessController.doPrivileged(new PriviliegedAction(){ public FileOutputStream run(){ return new FileOutputStream(filepath); } }); if(fos!=null){ // xxxxxxxx }
这到底是怎么回事呢?下面贴Java API中AccessController描述中的一段话:
A caller can be marked as being "privileged" (see doPrivileged and below). When making access control decisions, the checkPermission method stops checking if it reaches a caller that was marked as "privileged" via a doPrivileged call without a context argument (see below for information about a context argument). If that caller‘s domain has the specified permission, no further checking is done and checkPermission returns quietly, indicating that the requested access is allowed. If that domain does not have the specified permission, an exception is thrown, as usual.
这段话大意就是说:
如果使用了doPrivileged方法将调用者标记为privileged,在执行AccessController.checkPermission()做检查时,当检查到这个调用者时,就会终止检查,然后只作一个判断:如果caller所在的域有指定的权限就可以了。
SecurityManager#checkPermission实际上就是调用了AccessController.checkPermission(),所以这个解决方案对于SecurityManager#checkPermission也是适用的。
就暂说到这里吧,Java Security还有很多细节的东西没有提到。本文只是对Java Security有了一个整体结构上的说明。以及一些常用代码的解释,看完这篇文章,相信以往对一些有疑惑的代码,现在也应该可以明白七八分了。