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

本文涉及的产品
访问控制,不限时长
简介:

标签: Java 安全


[toc]


安全管理器:SecurityManager

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

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

  1. 请求Java API
  2. Java API使用安全管理器判断许可权限
  3. 通过则顺序执行,否则抛出一个Exception。

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

    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);
    }

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

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

checkAccept(String, int)
checkAccess(Thread)
checkAccess(ThreadGroup)
checkAwtEventQueueAccess()
checkConnect(String, int)
checkConnect(String, int, Object)
checkCreateClassLoader()
checkDelete(String)
checkExec(String)
checkExit(int)
checkLink(String)
checkListen(int)
checkMemberAccess(Class<?>, int)
checkMulticast(InetAddress)
checkMulticast(InetAddress, byte)
checkPackageAccess(String)
checkPackageDefinition(String)
checkPermission(Permission)
checkPermission(Permission, Object)
checkPrintJobAccess()
checkPropertiesAccess()
checkPropertyAccess(String)
checkRead(FileDescriptor)
checkRead(String)
checkRead(String, Object)
checkSecurityAccess(String)
checkSetFactory()
checkSystemClipboardAccess()
checkTopLevelWindow(Object)
checkWrite(FileDescriptor)
checkWrite(String)

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

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

public class SecurityManagerTest {

    static class MySM extends SecurityManager {
        public void checkExit(int status) {
            throw new SecurityException("no exit");
        }

    }

    public static void main(String[] args) {
        MySM sm = new MySM();
        System.out.println(System.getSecurityManager());
        System.setSecurityManager(sm);//注释掉测一下
        System.exit(0);
    }
}

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

null
Exception in thread "main" java.lang.SecurityException: no exit
    at com.taobao.cd.security.SecurityManagerTest$MySM.checkExit(SecurityManagerTest.java:7)
    at java.lang.Runtime.exit(Runtime.java:107)
    at java.lang.System.exit(System.java:971)
    at com.taobao.cd.security.SecurityManagerTest.main(SecurityManagerTest.java:16)

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

访问控制器:AccessController

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

组成

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

代码源CodeSource

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

权限Permission

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

策略Policy

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

保护域ProtectionDomain

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

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

AccessController

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

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

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

这样来检查权限。

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

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

类装载器ClassLoader

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对应代码如下

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

4,询问安全管理器是否允许程序创建当前处理的类。如果不行,则抛出一个安全异常。这一步可选,如果实现,则需要在findClass()的开始处完成。这一步不是在操作开始时完成,而是在询问父类装载器之后进行。这一步对应为defineClassInPackage权限。
5,向一个字节数组中读入类文件。读取文件以及创建字节数组的方式因类加载器不同而不同。在findClass()中完成。
6,为该类创建合适的保护域。保护域可以来自默认安全模型(即从策略文件中得到),也可以由类加载器扩展。还有一种方法是可以创建一个代码源对象,并采用其保护域定义。这一步也在findClass()中完成。
7,在findClass()方法中,通过调用defineClass()方法,可以由字节码构造一个Class对象。如果使用的是第6步中的代码源,则需要调用getPermissions()方法查找与代码源相关的权限。defineClass()方法还保证了字节码必须通过字节码校验器的检查。
8,最后还需要解析该类。即它所直接引用的类也应由当前类加载器找到。只有直接引用的才算,作为实例变量、方法参数或局部变量来使用的类不算。这一步在loadClass()中完成。对应上面代码中的resovleClass()。

以URLClassLoader举例。

1,对应的代码如下:

public final Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        // First check if we have permission to access the package. This
        // should go away once we've added support for exported packages.
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            int i = name.lastIndexOf('.');
            if (i != -1) {
                sm.checkPackageAccess(name.substring(0, i));
            }
        }
        return super.loadClass(name, resolve);
    }

URLClassLoader的newInstance()方法会构造一个内部的工厂加载器类。这个类的loadClass()方法做了checkPackageAccess的事情。
2,3 两步与超级父类ClassLoader相同。就是上面ClassLoader的loadClass()做的事情。
4,5,6,7 四个步骤涉及到findClass()方法,URLClassLoader覆盖了findClass()方法,但是最新版的jdk,其实将这几个步骤做的事情都在defineClass()里做掉了。里面的逻辑实现如下:

protected Class<?> findClass(final String name)
        throws ClassNotFoundException
    {
        final Class<?> result;
        try {
            result = AccessController.doPrivileged(
                new PrivilegedExceptionAction<Class<?>>() {
                    public Class<?> run() throws ClassNotFoundException {
                        String path = name.replace('.', '/').concat(".class");
                        Resource res = ucp.getResource(path, false);
                        if (res != null) {
                            try {
                                return defineClass(name, res);
                            } catch (IOException e) {
                                throw new ClassNotFoundException(name, e);
                            }
                        } else {
                            return null;
                        }
                    }
                }, acc);
        } catch (java.security.PrivilegedActionException pae) {
            throw (ClassNotFoundException) pae.getException();
        }
        if (result == null) {
            throw new ClassNotFoundException(name);
        }
        return result;
    }

defineClass()的逻辑:

/*
     * Defines a Class using the class bytes obtained from the specified
     * Resource. The resulting Class must be resolved before it can be
     * used.
     */
    private Class<?> defineClass(String name, Resource res) throws IOException {
        long t0 = System.nanoTime();
        int i = name.lastIndexOf('.');
        URL url = res.getCodeSourceURL();
        if (i != -1) {
            String pkgname = name.substring(0, i);
            // Check if package already loaded.
            Manifest man = res.getManifest();
            definePackageInternal(pkgname, man, url);
        }
        // Now read the class bytes and define the class
        java.nio.ByteBuffer bb = res.getByteBuffer();
        if (bb != null) {
            // Use (direct) ByteBuffer:
            CodeSigner[] signers = res.getCodeSigners();
            CodeSource cs = new CodeSource(url, signers);
            sun.misc.PerfCounter.getReadClassBytesTime().addElapsedTimeFrom(t0);
            return defineClass(name, bb, cs);
        } else {
            byte[] b = res.getBytes();
            // must read certificates AFTER reading bytes.
            CodeSigner[] signers = res.getCodeSigners();
            CodeSource cs = new CodeSource(url, signers);
            sun.misc.PerfCounter.getReadClassBytesTime().addElapsedTimeFrom(t0);
            return defineClass(name, b, 0, b.length, cs);
        }
    }

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

总结

通常的Java的安全性,都是从类加载器、安全管理器和访问控制器之间的关系考虑的。一般来说类加载器的作用更重要。
如果需要灵活的安全策略,往往要自定义类加载器。自定义类加载器允许在定义类时调整安全策略。这与实现一个新的Policy类相似。一般认为自定义类加载器会比修改一个Policy类要容易。

相关实践学习
消息队列+Serverless+Tablestore:实现高弹性的电商订单系统
基于消息队列以及函数计算,快速部署一个高弹性的商品订单系统,能够应对抢购场景下的高并发情况。
云安全基础课 - 访问控制概述
课程大纲 课程目标和内容介绍视频时长 访问控制概述视频时长 身份标识和认证技术视频时长 授权机制视频时长 访问控制的常见攻击视频时长
目录
相关文章
|
2天前
|
JSON Java Apache
Java基础-常用API-Object类
继承是面向对象编程的重要特性,允许从已有类派生新类。Java采用单继承机制,默认所有类继承自Object类。Object类提供了多个常用方法,如`clone()`用于复制对象,`equals()`判断对象是否相等,`hashCode()`计算哈希码,`toString()`返回对象的字符串表示,`wait()`、`notify()`和`notifyAll()`用于线程同步,`finalize()`在对象被垃圾回收时调用。掌握这些方法有助于更好地理解和使用Java中的对象行为。
|
1月前
|
存储 缓存 安全
java 中操作字符串都有哪些类,它们之间有什么区别
Java中操作字符串的类主要有String、StringBuilder和StringBuffer。String是不可变的,每次操作都会生成新对象;StringBuilder和StringBuffer都是可变的,但StringBuilder是非线程安全的,而StringBuffer是线程安全的,因此性能略低。
46 8
|
1月前
|
Java 开发者
在 Java 中,一个类可以实现多个接口吗?
这是 Java 面向对象编程的一个重要特性,它提供了极大的灵活性和扩展性。
139 57
|
1月前
|
存储 安全 Java
java.util的Collections类
Collections 类位于 java.util 包下,提供了许多有用的对象和方法,来简化java中集合的创建、处理和多线程管理。掌握此类将非常有助于提升开发效率和维护代码的简洁性,同时对于程序的稳定性和安全性有大有帮助。
73 17
|
1月前
|
SQL 安全 Java
安全问题已经成为软件开发中不可忽视的重要议题。对于使用Java语言开发的应用程序来说,安全性更是至关重要
在当今网络环境下,Java应用的安全性至关重要。本文深入探讨了Java安全编程的最佳实践,包括代码审查、输入验证、输出编码、访问控制和加密技术等,帮助开发者构建安全可靠的应用。通过掌握相关技术和工具,开发者可以有效防范安全威胁,确保应用的安全性。
54 4
|
1月前
|
安全 Java
Java多线程集合类
本文介绍了Java中线程安全的问题及解决方案。通过示例代码展示了使用`CopyOnWriteArrayList`、`CopyOnWriteArraySet`和`ConcurrentHashMap`来解决多线程环境下集合操作的线程安全问题。这些类通过不同的机制确保了线程安全,提高了并发性能。
|
1月前
|
存储 Java 程序员
Java基础的灵魂——Object类方法详解(社招面试不踩坑)
本文介绍了Java中`Object`类的几个重要方法,包括`toString`、`equals`、`hashCode`、`finalize`、`clone`、`getClass`、`notify`和`wait`。这些方法是面试中的常考点,掌握它们有助于理解Java对象的行为和实现多线程编程。作者通过具体示例和应用场景,详细解析了每个方法的作用和重写技巧,帮助读者更好地应对面试和技术开发。
128 4
|
1月前
|
Java 编译器 开发者
Java异常处理的最佳实践,涵盖理解异常类体系、选择合适的异常类型、提供详细异常信息、合理使用try-catch和finally语句、使用try-with-resources、记录异常信息等方面
本文探讨了Java异常处理的最佳实践,涵盖理解异常类体系、选择合适的异常类型、提供详细异常信息、合理使用try-catch和finally语句、使用try-with-resources、记录异常信息等方面,帮助开发者提高代码质量和程序的健壮性。
71 2
|
1月前
|
存储 安全 Java
如何保证 Java 类文件的安全性?
Java类文件的安全性可以通过多种方式保障,如使用数字签名验证类文件的完整性和来源,利用安全管理器和安全策略限制类文件的权限,以及通过加密技术保护类文件在传输过程中的安全。
57 4
|
26天前
|
SQL 安全 Java
Java 异常处理:筑牢程序稳定性的 “安全网”
本文深入探讨Java异常处理,涵盖异常的基础分类、处理机制及最佳实践。从`Error`与`Exception`的区分,到`try-catch-finally`和`throws`的运用,再到自定义异常的设计,全面解析如何有效管理程序中的异常情况,提升代码的健壮性和可维护性。通过实例代码,帮助开发者掌握异常处理技巧,确保程序稳定运行。
39 0