Java——安全机制详解

Java安全机制

Java提供了以下三种确保安全的机制:

  1. 语言设计特性(对数组的边界进行检查,无不受检查的类型转换,无指针算法等)
  2. 访问控制机制,用于控制代码能够执行的操作(比如文件访问,网络访问等)
  3. 代码签名,利用该特性,代码的作者就能够用标准的加密算法来认证Java代码。这样该代码的使用折就能够准确地知道谁创建了该代码,以及代码签名后是否被修改过

1、类加载器

Java编译器会为虚拟机转换源指令。虚拟机代码存储在以.class为扩展名的类文件中,每个类文件都包含某个类或者接口的定义和实现代码。

获取类加载器:

  1. java.lang.Class的getClassLoader():获取加载该类的类加载器
  2. java.lang.Thread的getContextClassLoader():获取类加载器,该线程的创建者将其指定为执行该线程时最合适使用的类加载器

java.lang.ClassLoader:

  • ClassLoader getParent():返回父类加载器,如果父类加载器是引导类加载器,则返回null。
  • static ClassLoader getSystemClassLoader():获取系统类加载器,即用于加载第一个应用类的类加载器。
  • protected Class findClass(String name):类加载器应该覆盖的方法,以查找类的字节码,并通过调用defineClass方法将字节码传给虚拟机。在类的名字中,使用.作为包名分隔符,并且使用.class后缀。
  • Class defineClass(String name, byte[] byteCodeData, int offset, int length):将一个新类添加到虚拟机中,其字节码在给定的数据范围中。

1.1、类加载过程

虚拟机只加载程序执行时所需要的类文件。例如,假设程序从MyProgram.class开始运行,下面是虚拟机执行的步骤:

  1. 虚拟机有一个用于加载类文件的机制,例如,从磁盘上读取那件或者请求Web上的文件,它使用该机制来加载MyProgram类文件中的内容。
  2. 如果MyProgram类拥有类型为另一个类的域,或者是拥有超类,那么这些类文件也会被加载。(加载某个类所依赖的所有类的过程成为类的解析。)
  3. 虚拟机执行MyProgram中的main方法(它是静态的,无须创建类的实例)。
  4. 如果main方法或者main调用的方法要用到更多的类,那么接下来就会加载这些类。

然而,类加载机制并非只有使用单个的类加载器。每个Java程序至少有三个类加载器:

  • 引导类加载器
  • 平台类加载器
  • 系统类加载器(应用类加载器)

引导类加载器负责加载包含在下列模块以及大量的JDK内部模块中的平台类:

  • java.base
  • java.datatransfer
  • java.desktop
  • java.instrument
  • java.logging
  • java.management
  • java.management.rmi
  • java.naming
  • java.naming
  • java.prefs
  • java.rmi
  • java.security.sasl
  • java.xml

引导类加载器没有对应的ClassLoader对象,例如:

StringBuilder.class.getClassLoader()
//返回null

在Java9之前,Java平台位于rt.jar中。如今,Java平台是模块化的,每个平台模块都包含一个JMOD文件。平台类加载器会加载引导类加载器没有加载的Java平台中的所有类。

系统类加载器会从模块路径和类路径中加载应用类。

在Java9之前,“扩展类加载器"会加载jre/lib/ext目录中的"标准扩展”,而"授权标准覆盖"机制提供了一种方式,可以用更新的版本覆盖某些平台类(包括CORBA和XML的实现)。这两种机制都被移除了。

1.2、累加载器的层次结构

类加载器有一种父/子关系。除了引导类加载器外,每个类加载器都有一个父类加载器。根据规定,类加载器会为它的父类加载器提供一个机会,以便加载任何给定的类,并且只有在其父类加载器加载失败时,它才会加载该给定类。

例如,当要求系统类加载器加载一个系统类
(比如,java.lang.StringBuilder)时,它首先要求平台类加载器进行加载,该加载器则首先要求引导类加载器进行加载。引导类加载器会找到并加载这个类,而无须其他类加载器做更多的搜索。

某些程序具有插件架构,其中代码的某些部分是作为可选的插件打包的。如果插件被打包为JAR文件,那就可以直接用URLClassLoader类的实例去加载插件类。

var url = new URL("file:///path/to/plugin.jar");
var pluginLoader = new URLClassLoader(new URL[] {url});
Class<?> cl = pluginLoader.loadClass("mypakage.MyClass");

由于在URLClassLoader构造器中没有指定父类加载器,因此pluginLoader的父类就是系统类加载器,如下图:
在这里插入图片描述

警告:在Java9之前,系统类加载器是URLClassLoaser类的实例。有些程序员会使用强制转型来访问其getURLs方法,或者通过反射机制调用受保护的addURLs方法将JAR文件添加到类路径中。现在无法这样操作了。

java.net.URLClassLoader

  • URLClassLoader(URL[] ruls)
  • URLClassLoader(URL[[] urls, ClassLoader parent):构建一个类加载器,它可以从给定的URL处加载类。如果URL以`/``结尾,那么它表示的是一个目录,否则,它表示的是一个JAR文件。

1.3、将类加载器用作命名空间

每个Java程序员都知道,包的命名是为了消除名字冲突。在标准类库中,有两个名为Date的类,它们的实际名字分别为java.util.Date和java.sql.Date。使用简单的名字只是为了方便程序员,它们要求程序包含恰当的import语句。在一个正在执行的程序中,所有的类名都包含它们的包名。

然而,令人惊奇的是,在同一个虚拟机中,可以有两个类,它们的类名和包名都是相同的。类是由它的全名和类加载器来确定的。这项技术在加载来自多处的代码时很有用。例如,应用服务器会为每一个应用使用单独的类加载器,这使得虚拟机可以区分来自不同应用的类,而无论它们是怎样命名的。下图展示了一个示例。假设一个应用服务器加载了两个不同的应用,它们都有一个名为Util类。因为每个类都是由单独的类加载器加载的,所以这些类可以彻底地区分开而不会产生任何冲突。

在这里插入图片描述

  • 同一个命名空间的类是相互可见的。
  • 子加载器的命名空间包含所有父加载器的命名空间。因此由子加载器加载的类能看见父加载器加载的类。例如系统类加载器能看见根类加载器加载的类。
  • 由父类加载器加载的类不能看见子加载器加载的类。
  • 如果两个加载器没有父子关系,那么他们自己加载的类互相不可见。

在这里插入图片描述

这样,子加载器的命名空间,包含了父加载器的命名空间,就可以保证,子加载器加载的类,可以使用父加载器加载的类。也就是说,父加载器加载的类,对子加载器可见,同级别加载器加载的类,不可见。

1.4、双亲委派机制

这也是类加载器加载一个类的整个过程。

假设我现在从类路径下加载一个类A:

  1. 那么AppClassLoader会先查找是否加载过A,若有,直接返回;
  2. 若没有,去ExtClassLoader检查是否加载过A,若有,直接返回;
  3. 若没有,去BootStrapClassLoader检查是否加载过A,若有,直接返回;
  4. 若没有,那就BootStrapClassLoader加载,若在jdk\jre\lib*.jar下找到了指定名称的类,则加载,结束;
  5. 若没找到,BootStrapClassLoader加载失败;
  6. ExtClassLoader开始加载,若在jre\lib\ext*.jar下找到了指定名称的类,则加载,结束;
  7. 若没找到,ExtClassLoader加载失败;
  8. AppClassLoader加载器加载,若在类路径(classpath)下找到了指定名称的类,则加载,结束;
  9. 若没有找到,抛出异常ClassNotFoundException

在上述过程中的1)2)3)4)6)8)后边,都要去判断是否需要进行"解析"过程

类的加载过程只有向上的双亲委托,没有向下的查询和加载,假设是ext在jdk\jre\lib\ext*.jar下加载一个类,那么整个查询与加载的过程与app无关。

假设A加载成功了,那么该类就会缓存在当前的类加载器实例对象C中,key是(A,C)(其中A是类的全类名,C是加载A的类加载器对象实例),value是对应的java.lang.Class对象

上述的1)2)3)都是从相应的类加载器实例对象的缓存中进行查找

进行缓存的目的是为了同一个类不被加载两次

使用(A,C)做key是为了隔离类,假设现在有一个类加载器B也加载了A,key为(A,B),则这两个A是不同的A。这种情况怎么发生呢?

假设有custom1、custom2两个自定义类加载器,他们是兄弟关系,同时加载A,这就是有可能的了

总结:

  • 从底向上检查是否加载过指定名称的类;从顶向下加载该类。(在其中任何一个步骤成功之后,都会中止类加载过程)
  • 双亲委托的好处:假设自己编写了一个java.lang.Object类,编译后置于类路径下,此时在系统中就有两个Object类,一个是rt.jar的,一个是类路径下的,在类加载的过程中,当要按照全类名去加载Object类时,根据双亲委托,boot会加载rt.jar下的Object类,这是方法结束,即类路径下的Object类就没有加载了。这样保证了系统中类不混乱。

ClassLoader.java类提供了loadClass方法,确保双亲委派机制:

/**
 * 根据指定的binary name加载class。
 * 步驟:
 * 假设我现在从类路径下加载一个类A,
 * 1)那么app会先查找是否加载过A(findLoadedClass(name)),若有,直接返回;
 * 2)若没有,去ext检查是否加载过A(parent.loadClass(name, false)),若有,直接返回;
 * findBootstrapClassOrNull(name) 3)4)5)都是这个方法
 * 3)若没有,去boot检查是否加载过A,若有,直接返回;
 * 4)若没有,那就boot加载,若在E:\Java\jdk1.6\jre\lib\*.jar下找到了指定名称的类,则加载,结束;
 * 5)若没找到,boot加载失败;
 * findClass(name) 6)7)8)9)都是这个方法
 * 在findClass中调用了defineClass方法,该方法会生成当前类的java.lang.Class对象
 * 6)ext开始加载,若在E:\Java\jdk1.6\jre\lib\ext\*.jar下找到了指定名称的类,则加载,结束;
 * 7)若没找到,ext加载失败;
 * 8)app加载,若在类路径下找到了指定名称的类,则加载,结束;
 * 9)若没有找到,抛出异常ClassNotFoundException
 * 注意:在上述过程中的1)2)3)4)6)8)后边,都要去判断是否需要进行"解析"过程
 */
protected synchronized Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException {
    Class c = findLoadedClass(name);//检查要加载的类是不是已经被加载了
    if (c == null) {//没有被加载过
        try {
            if (parent != null) {
                //如果父加载器不是boot,递归调用loadClass(name, false)
                c = parent.loadClass(name, false);
            } else {//父加载器是boot
                /*
                 * 返回一个由boot加载过的类;3)
                 * 若没有,就去试着在E:\Java\jdk1.6\jre\lib\*.jar下查找 4)
                 * 若在bootstrap class loader的查找范围内没有查找到该类,则返回null 5)
                 */
                c = findBootstrapClassOrNull(name);
            }
        } catch (ClassNotFoundException e) {
            //父类加载器无法完成加载请求
        }
        if (c == null) {
            //如果父类加载器未找到,再调用本身(这个本身包括ext和app)的findClass(name)来查找类
            c = findClass(name);
        }
    }
    if (resolve) {
        resolveClass(c);
    }
    return c;
}

1.5、自定义类加载器

如果要编写自己的类加载器,只需要继承ClassLoader类,然后覆盖下面的这个方法:

findClass(String className)

ClassLoader超类的loadClass方法用于将类的加载操作委托给其父类加载器去进行,只有当该类尚未加载并且父类加载器也无法加载该类时,才调用findClass方法。

如果要实现该方法,必须做到以下几点:

  1. 为来自本地文件系统或者其他来源的类加载
    其字节码。
  2. 调用ClassLoader超类的defineClass方法,向
    虚拟机提供字节码。

pojo类:

public class Person {

    private String name;
    private String age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getAge() {
        return age;
    }

    public void setAge(String age) {
        this.age = age;
    }
}

ClassLoader:

public class MyClassLoader extends ClassLoader {

    private String classLoaderName;
    private String path;
    private String fileExtension = ".class";

    public MyClassLoader(String classLoaderName) {
        super();
        this.classLoaderName = classLoaderName;
    }

    public MyClassLoader(ClassLoader parent, String classLoaderName) {
        super(parent);
        this.classLoaderName = classLoaderName;
    }

    @Override
    protected Class<?> findClass(String className) throws ClassNotFoundException {
        System.out.println("findClass invoked : " + classLoaderName);
        System.out.println("class loader name : " + this.classLoaderName);
        byte[] data = this.loadClassBytes(className);
        return this.defineClass(className, data, 0, data.length);
    }

    private byte[] loadClassBytes(String className) {
        byte[] data = null;
        className = className.replace(".", "/");
        try (InputStream is = new FileInputStream(new File(this.path + className + this.fileExtension));
            ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
            int ch;
            while (-1 != (ch = is.read())) {
                baos.write(ch);
            }
            data = baos.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return data;
    }

    public void setPath(String path) {
        this.path = path;
    }
}

测试1:

public class ClassLoaderTest {
    public static void main(String[] args) throws Exception {
        MyClassLoader loader1 = new MyClassLoader("loader1");
        MyClassLoader loader2 = new MyClassLoader("loader2");
        String path = "";
        loader1.setPath(path);
        loader2.setPath(path);

        /*
            第一次加载:
            MyClassLoader请求父类加载
                -> AppClassLoader请求父类加载
                    -> ExtClassLoader请求父类加载
                        -> BootStrapClassLoader调用indBootstrapClassOrNull(name),因为是自定义的类,所以没找到,返回null
                    -> 栈返回,调用ExtClassLoader的findclass()方法,因为是自定义的类,不是/java/ext路径,所以也没有找到,返回null
                -> 栈返回,调用AppClassLoader的findclass()方法,自定义的类属于classpath路径,所以找到了Person类,完成了加载
         */
        Class<?> clazz1 = loader1.loadClass("pers.zhang.classloader.Person");
        /*
            第二次加载:
            MyClassLoader请求父类加载
                -> AppClassLoader发现已经加载过,在缓存中找到Person,直接返回
         */
        Class<?> clazz2 = loader2.loadClass("pers.zhang.classloader.Person");

        //clazz1的classLoader是sun.misc.Launcher$AppClassLoader@18b4aac2
        System.out.println("clazz1的classLoader是" + clazz1.getClassLoader());
        //clazz2的classLoader是sun.misc.Launcher$AppClassLoader@18b4aac2
        System.out.println("clazz2的classLoader是" + clazz2.getClassLoader());
        //true
        System.out.println(clazz1 == clazz2);
    }
}

因此,person的加载器实际上为AppClassLoader,因此我们不能把Person放在类路径下。否则,由于双亲委托机制的存在,会直接导致该类由AppClassLoader加载,而不会通过我们自定义类加载器来加载。

测试2:

删除target下的person.class,同时将Person.java换一个位置存放/Users/acton_zhang/J2EE/MavenWorkSpace/java_safe_demo /pers/zhang/classloader/Person.java,然后javac Person.java手动的编译为Person.class。

public static void main(String[] args) throws Exception {
    MyClassLoader loader1 = new MyClassLoader("loader1");
    MyClassLoader loader2 = new MyClassLoader("loader2");
    String path = "/Users/acton_zhang/J2EE/MavenWorkSpace/java_safe_demo/";
    loader1.setPath(path);
    loader2.setPath(path);

    /*
        第一次加载:
        MyClassLoader请求父类加载
            -> AppClassLoader请求父类加载
                -> ExtClassLoader请求父类加载
                    -> BootStrapClassLoader调用indBootstrapClassOrNull(name),因为是自定义的类,所以没找到,返回null
                -> 栈返回,调用ExtClassLoader的findclass()方法,因为是自定义的类,不是/java/ext路径,所以也没有找到,返回null
            -> 栈返回,调用AppClassLoader的findclass()方法,类路径中也没有找到,返回null
        -> 栈返回,调用MyClassLoader的findclass()方法,找到并加载Person类。
     */
    Class<?> clazz1 = loader1.loadClass("pers.zhang.classloader.Person");
    /*
        第二次加载:因为MyClassLoader是两个实例,也就是两个独立命名空间,所以loader1加载Person类成功之后,
        同级别的loader2并不可见,所以loader2又会加载一次,步骤和第一次一致。
     */
    Class<?> clazz2 = loader2.loadClass("pers.zhang.classloader.Person");

    //clazz1的classLoader是pers.zhang.classloader.MyClassLoader@610455d6
    System.out.println("clazz1的classLoader是" + clazz1.getClassLoader());
    //clazz2的classLoader是pers.zhang.classloader.MyClassLoader@60e53b93
    System.out.println("clazz2的classLoader是" + clazz2.getClassLoader());
    //false 两个class是由两个类加载器加载,所以并不一致
    System.out.println(clazz1 == clazz2);
}

1.6、字节码校验

当类加载器将新加载的Java平台类的字节码传递给虚拟机时,这些字节码首先要接受校验器(verifier)的校验。校验器负责检查那些指令无法执行的明显有破坏性的操作。除了系统类外,所有的类都要被校验。

下面是校验器执行的一些检查:

  • 变量要在使用之前进行初始化。
  • 方法调用与对象引用类型之间要匹配。
  • 访问私有数据和方法的规则没有被违反。
  • 对本地变量的访问都落在运行时堆栈内。
  • 运行时堆栈没有溢出。

如果以上这些检查中任何一条没有通过,那么该类就被认为遭到了破坏,并且不予加载。

这种严格的校验是出于安全上的考虑,有一些偶然性的错误,比如变量没有初始化,如果没有被捕获,就很容易对系统造成严重的破坏。更为重要的是,在因特网这样开放的环境中,你必须保护自己以防恶意的程序员对你实施攻击,因为他们的目的就是要造成恶劣的影响。例如,通过修改运行时堆栈中的值,或者向系统对象的私有数据字段写入数据,某个程序就会突破浏览器的安全防线。

2、安全管理器与访问权限

一旦某个类被加载到虚拟机中,并由检验器检查过之后,Java平台的第二种安全机制就会启动,这个机制就是安全管理器。

2.1、权限检查

安全管理器是一个负责控制具体操作是否允许执行的类。安全管理器负责检查的操作包括一下内容:

  • 创建一个新的类加载器
  • 退出虚拟机
  • 使用反射访问另一个类的成员
  • 访问本地文件
  • 打开Socket连接
  • 启动打印作业
  • 访问系统剪贴板
  • 访问AWT事件对嘞
  • 打开一个顶层窗口

整个Java类库中还有许多其他类似的检查。

在运行Java应用程序时,默认的设置是不安装安全管理器的,这样所有的操作都是允许的。另一方面,applet浏览器会执行一个功能受限的安全策略。更严格的安全性对其他情况也具有意义。

例如,假设你运行了一个Tomcat的实例,并允许合作者或学生在其中安装Servlet。你并不想让他们中的任何人调用System.exit,因为这会终止该Tomcat实例。你可以设置一个安全策略,让对System.exit的调用抛出安全异常而不是真的关闭虚拟机。下面将详细说明这种情况。Runtime类的exit方法会调用安全管理器的checkExit方法,下面是exit方法的全部代码:

public void exit(int status) {
    SecurityManager security = System.getSecurityManager();
    if (security != null) {
        security.checkExit(status);
    }
    Shutdown.exit(status);
}

这时安全管理器要检查退出请求是来自浏览器还是单个的applet程序。如果安全管理器同意了退出请求,那么checkExit便直接返回并继续处理下面正常的操作。但是,如果安全管理器不同意退出请求,那么checkExit方法就会抛出一个SecurityException异常。

只有当没有任何异常发生时,exit方法才能继续执行。然后它调用本地私有的exitInternal方法,以真正终止虚拟机的运行。没有其他的方法可以终止虚拟机的运行,因为
exitInternal方法是私有的,任何其他类都不能调用它。因此,任何试图退出虚拟机的代码都必须通过exit方法,从而在不触发安全异常的情况下,通过checkExit安全检查。

显然,安全策略的完整性依赖于谨慎的编码。标准类库中系统服务的提供者,在试图继续任何敏感的操作之前,都必须与安全管理器进行协商。

Java平台的安全管理器,不仅允许系统管理员,而且允许程序员对各个安全访问权限实施细致的控制.

2.2、Java平台安全性

JDK1.0具有一个非常简单的安全模型,即本地类拥有所有的权限,而远程类只能在沙盒里运行。就像儿童只能在沙盒里玩沙子一样,远程代码只被允许打印屏幕和与用户进行交互。applet的安全管理器拒绝了远程代码对本地资源的所有访问。JDK1.1对此进行了微小的修改,如果远程代码带有可信赖的实体的签名,将被赋予和本地类相同的访问权限。不过,JDK1.0和1.1这两个版本提供的都是一种“要么都有,要么都没有”的权限赋予方法。程序要么拥有所有的访问权限,要么必须在沙盒里运行。

从Java 1.2开始,Java平台拥有了更灵活的安全机制,它的安全策略建立了代码来源和访问权限集之间的映射关系。

在这里插入图片描述

**代码来源(code source)**是由一个代码位置和一个证书集指定的。代码位置指定了代码的来源。例如,远程applet代码的代码位置是下载applet的HTTP URL,位于JAR文件中的代码的代码位置是该文件的URL。证书的目的是要由某一方来保障代码没有被篡改过。

**权限(permission)**是指由安全管理器负责检查的任何属性。Java平台支持许多访问权限类,每个类都封装了特定权限的详细信息。例如,下面这个FilePermission类的实例表示:允许在/tmp目录下读取和写入任何文件。

var p = new FilePermisson("/tm/*", "read,write");

Policy类的默认实现可以从范文权限文件中读取权限。在权限文件中,同样的读权限表示为:

permission java.io.FilePermission "/tmp/*", "read,write";

下图显示了java1.2中提供的权限类的层次结构,后续版本中添加了更多的权限类:
在这里插入图片描述

SecurityManager类有许多类似checkExit的安全检查方法,这些方法的存在,知识为了程序员的方便和向后的兼容性,它们都已被映射为标准的权限检查,例如,下面是checkExit方法的源代码:

public void checkExit() {
	checkPermission(new RuntimePermission("exitVM"));
}

每个类都有一个保护域,它是一个用于封装类的代码来源和权限集合的对象。当SecurityManager类需要检查某个权限时,它要查看当前位于调用堆栈上的所有方法的类,然后它要获得所有类的保护域,并且询问每个保护域,其权限集合是否允许执行当前正在被检查的操作。如果所有的域都同意,那么检查得以通过。否则,就会抛出一个SecurityException异常。

为什么在调用堆栈上的所有方法都必须允许某个特定的操作呢?让我们通过一个实例来说明这个问题。假设一个Servlet的init方法想要打开一个文件,它可能会调用下面的语句:

var in new FileReader(name)

FileReader构造器调用FileInputStream构造器,而FileInputStream构造器调用安全管理器的checkRead方法,安全管理器最后用FilePermission(name,"read")对象调用checkPermission。

下表显示了该调用堆栈:

方法代码来源权限
SecutiryManagercheckPermissionnullAllPermission
SecurityManagercheckReadnullAllPermission
FileInputStreamConstrutornullAllPermission
FileReaderConstructornullAllPermission
ServletinitServlet代码来源TomcatWeb应用权限

FileInputStream和SecurityManager类都属于系统类,它们的CodeSource为null,它们的权限都是由AllPermission类的一个实例组成的,AllPermission类允许执行所有的操作。显然地,仅仅根据它们的权限是无法确定检查结果的。正如我们所看到的那样,checkPermission方法必须考虑applet类的受限制的权限问题。通过检查整个调用堆栈,安全机制就能够确保一个类决不会要求另一个类代表自己去执行某个敏感的操作。

java.lang.SecurityManager

  • void checkPermission(Permission p):检查当前的安全管理器是否授予给定的权限。如果没有授予该权限,本方法抛出一个SecurityException异常。

java.lang.Class

  • ProtectionDomain getProtectionDomain():获取该类的保护域,如果该类被加载时没有保护域,则返回null。

java.security.ProtectionDomain

  • ProtectionDomain(CodeSource source, PermissionCollection permissions):用给定的代码来源和权限构建一个保护域
  • CodeSource getCodeSource():获取该保护域的代码来源
  • boolean implies(Permission p):如果该保护域允许给定的权限,则返回true

2.3、安全策略文件

策略管理器要读取相应的策略文件,这些文件包含了将代码来源映射为权限的指令。

例如:

grant codeBase "http://www.horstmann.com/classes"
{
	permission java.io.FilePermission "/tmp/*", "read,write";
}

该文件给所有下载自http:/ww.horstmann.com/classes的代码授予在/tmp目录下读取和写入文件的权限。

可以将策略文件安装在标准位置上。默认情况下,有两个位置可以安装策略文件:

  • Java平台主目录的java.policy文件。
  • 用户主目录的.java.policy文件(注意文件名前面的圆点)。

在测试期间,我们不喜欢经常地修改这些标准文件。因此,我们更愿意为每一个应用程序单独命名策略文件,这样将权限写入一个独立的文件(比如MyApp.policy)中即可。要应用这个策略文件,可以有两个选择。

一种是在应用程序的man方法内部设置系统属性:

System.setProperty("java.security.policy", "MyApp.policy");

第二种是使用命令行参数:

java -Djava.security.policy=MyApp.policy MyApp

在这些例子中,MyApp.policy文件被添加到了其他有效的策略中。如果在命令行中添加了第二个等号,比如:

java -Djava.security.policy=MyApp.policy MyApp

那么应用程序就只使用指定的策略文件,而标准策略文件将被忽略。

警告:在测试期间,一个容易犯的错误是在当前目录中留下了一个,java.policy文件,该文件授予了许许多多的权限,甚至可能授予了AllPermission。如果发现你的应用程序似乎没有应用策略文件中的规定,就应该检查当前目录下是否留有,.java.policy文件。如果使用的是UNIX系统,就更容易犯这样的错误,因为在UNIX中,文件名以圆点开头的文件默认是不显示的。

默认情况下,Java应用程序是不安装安全管理器的。因此,在安装安全管理器之前,看不到策略文件的作用。当然,可以将这行代码:

System.setSecurityManager(new SecurityManager());

添加到main方法中,或者在启动虚拟机的时候添加命令行选项:

java -Djava.security.manager -Djava.security.policy=MyApplpolicy MyApp

一个策略文件包含一系列grant项。每一项都具有以下的形式:

grant codesource
{
	permission1;
	permission2;
	...
}

代码来源包含一个代码基(如果某一项适用于所有来源的代码,则代码基可以省略)和值的新来的用户特征(principal)与证书签名者的名字(如果不要求对该项签名,则可以省略)。

代码基可以设定为:

codeBase "url"

如果URL以"/"结束,那么它是一个目录。否则,它被视为一个JAR文件的名字。

grant codeBase "www.horstmann.com/classes/" {...}
grant codeBase "www.horstmann.com/classes/MyApp.jar" {...}

代码基是一个URL并且总是以斜杠作为文件分隔符,即使是Windows中的文件URL:

grant codeBase "file:C:/myapps/classes/" {...}

注释:请考虑编译Java代码的应用程序,它需要大量的权限。在JDK9之前,你可以被授权获得对tools.jar中的代码的所有权限。这个JAR文件现在已经不存在了。因
此,需要像下面这样授予对适合的模块进行访问的权限:
grant codeBase “jrt:/jdk/compiler”
{
permision java.security.AllPermission;
}

权限采用下面的结构:

permission cassName targetName, actionList;

类名是权限类的全称类名(比如java.io.FilePermission)。目标名是个与权限相关的值,例如,文件权限中的目录名或者文件名,或者是socket权限中的主机和端口。操作列表同样是与权限相关的,它是一个操作方式的列表,比如read或者connect等操作,用逗号分隔。有些权限类并不需要目标名和操作列表。

下表列出了标准的权限和它们执行的操作:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

大部分权限只允许执行某种特定的行为。可以将这些行为视为带有一个隐含操作"permit"的目标。这些权限类都继承自BasicPermission类。然而,文件、socket和属性权限的目标都比较复杂:

文件权限的目标可以有下面几种形式:

  • file:文件
  • directory/:目录
  • directory/*:目录中的所有文件
  • *:当前目录中的所有文件
  • directory/-:目录和其子目录中的所有文件
  • -:当前目录和其子目录中所有文件
  • <<ALL FILES>>:文件系统中的所有文件

例如,下面的权限项赋予对/myapp目录和它的子目录中的所有文件的访问权限:

permission java.io.FilePermission "/myapp/-", "read,write,delete";

必须使用\\转义字符序列来表示Window文件名中的反斜杠:

permission java.io.FilePermission "c:\\myapp\\-", "read,write,delete"

Socket权限的目标由主机和端口范围组成:

对主机的描述具有下面几种形式:

  • hostname或IPaddress:单个主机
  • localhost或空字符串:本地主机
  • *.domainSuffix:以给定后缀结尾的域中所有的主机
  • *:所有主机

端口范围是可选的,具有下面几种形式:

  • :n:单个端口
  • :n-:编号大于等于n的所有端口
  • :-n:编号小于等于n的所有端口
  • :n1-n2:位于给定范围内的所有端口

例如:

permission java.net.SocketPermission ".horstmann.com:8000-8999", "connect";

属性权限的目标可以采用下面两种形式之一:

  • proeprty:一个具体的属性
  • propertyPrefix.*:带有给递归前缀的所有属性

例如,下面的权限项允许程序读取以java.vm开头的所有属性:

permission java.util.PropertyPermission "java.vm.*", "read";

可以在策略文件中使用系统属性,其中的${property}标记会被属性值替代,例如${user.home}会被用户主目录替代。

permission java.io.FilePermission "${user.home}", "read,write";

为了创建平台无关的策略文件,使用file.separator属性而不是使用显式的/或者\\分隔符绝对是个好主意。如果要使它更加简单,可以使用符号${/}作为${file.separator}的缩写。
例如:

permission java.io.FilePermission "${user.home}s{/}-","read,write";

是一个可在平台之间移植的项,用于授予对在用户的主目录及其子目录中的文件进行读写的权限。

2.4、定制权限

如果要实现自己的权限类,可以继承Permission类,并提供一下方法:

  • 带有两个String参数的构造器,这两个参数分别是目标和操作列表
  • String getActions()
  • boolean equals(Object other)
  • int hashCode()
  • boolean implies(Permisssion other)

最后一个方法是最重要的。权限有一个排序,其中更加泛化的权限隐含了更加具体的权限。

p1 = new FilePermission("/tmp/-", "read,write");

该权限允许读写/tmp目录以及子目录中的任何文件,该权限隐含了其他更加具体的权限:

p2 = new FilePermission("/tmp-", "read");
p3 = new FilePermission("/tmp/aFile", "read,write");
p4 = new FilePermission("/tmp/aDirectry/-", "write");

换句话说,如果

  1. p1的目标文件集包含p2的目标文件集
  2. p1的操作集包含p2的操作集

那么,文件访问权限p1就隐含了另一个文件访问权限p2。

implies方法的用法:

当FileInputStream构造器想要打开一个文件,以读取该文件时,要检查它是否拥有操作权限。如果要执行这种检查,就应将一个具体的文件权限对象传递给checkPermission方法:

checkPermission(new FilePermission(fileName,"read"));

现在安全管理器询问所有适用的权限是否隐含了该权限。如果其中某个隐含了该权限,就通过了检查。

特别地,AllPermission隐含了其他所有的权限。

如果你定义了自己的权限类,那么必须对权限对象定义一个合适的隐含法则。例如,假设你为采用Java技术的机顶盒定义一个TVPermission,那么下面这个访问权限

new TVPermission("Tommy:2-12:1900-2200","watch,record");

将允许Tommy在19点到22点之间对2至12频道的电视节目进行观看和录像。必须实现implies方法,以隐含像下面这样的更具体的权限。

new TVPermission("Tommy:4:2000-2100","watch")

2.5、实现权限类

需要继承Permission类:

  • Permission(String name):用指定的目标名构建一个权限
  • String getName():返回该权限的对象名称
  • boolean implies(Permission other):检查该权限是否隐含了other权限。如果other权限描述了一个更加具体的条件,而这个具体条件是由该权限所描述的条件所产生的结果,那么该权限就隐含这个other权限。

1. 需求:

实现一个权限,用于监视将文本插入到文本域的操作。该程序会确保我们不能输入"不良单词",例如:sex,drugs以及C++等。

如果安全管理器赋予了WordCheckPermission权限,那么该文本就可以追加。否则,checkPermision方法就会抛出一个异常。

单词检查权限有两个可能的操作:

  • insert:用于插入具体文本的权限
  • avoid:添加不包含某些不良单词的任何文本的权限

应该用下面的策略文件:

grant
{
	permission pmissions.WordCheckPermission "sex,drugs,C++", "avoid";
};

这个策略文件赋予的权限是可以插入不包含不良单词sexdtrugsC++的任何文本。

当设计权限类时,必须特别注意implies方法,下面是控制权限p1是否隐含p2的规则:

  • 如果p1有avoid操作,p2有insert操作,那么p2的目标必须避开p1中的所有单词。

例如:下面这个权限:

perissions.WordCheckPermission "sex,drugs,C++", "avoid"

隐含了下面这个权限:

permissions.WordCheckPermission "Mary had a litte lamb", "insert"
  • 如果p1和p2都有avoid操作,那么p2的单词集合必须包含p1单词集合中的所有单词,例如,下面这个权限:
permissions.WordCheckPermission "sex,drugs", "avoid"

隐含了下面的这个权限:

permissions.WordCheckPermission "sex,drugs,C++", "avoid"
  • 如果p1和p2都有insert操作,那么p1的文本必须包含p2的文本。例如下面这个权限:
permissions.WordCheckPermission "Mary had a litte lamb", "insert"

包含了下面这个权限:

permissions.WordCheckPermission "a litte lamb", "insert"

注意:可以用Permission类中名字容易混淆的getName方法来获取权限的目标。由于在策略文件中权限是由一对字符串来表示的,因此,权限类需要准备好解析这些字符串。

2. 权限文件:
my.policy:

grant
{
    permission pers.zhang.securitymanager.WordCheckPermission "sex,drugs,C++", "avoid";
    permission java.lang.reflect.ReflectPermission "suppressAccessChecks";
};

3. 权限类:

public class WordCheckPermission extends Permission {

    private String action;

    public WordCheckPermission(String name, String anAction) {
        super(name);
        action = anAction;
    }

    //处理一个权限如何判断是否包含另一个权限
    @Override
    public boolean implies(Permission permission) {
        //不是相同类型
        if (!(permission instanceof WordCheckPermission))
            return false;
        WordCheckPermission p = (WordCheckPermission)permission;
        if (action.equals("insert")) {
            return p.action.equals("insert") && getName().indexOf(p.getName()) >= 0;
        } else if (action.equals("avoid")) {
            if (p.action.equals("avoid")) {
                return p.badWordSet().containsAll(badWordSet());
            } else if (p.action.equals("insert")) {
                for (String bad : badWordSet()) {
                    if (p.getName().indexOf(bad) >= 0) {
                        return false;
                    }
                }
                return true;
            } else {
                return false;
            }
        } else {
            return false;
        }
    }

    @Override
    public boolean equals(Object other) {
        if (other == null)
            return false;
        if (!getClass().equals(other.getClass()))
            return false;
        WordCheckPermission p = (WordCheckPermission)other;
        if (!Objects.equals(action, p.action))
            return false;
        if ("insert".equals(action))
            return Objects.equals(getName(), p.getName());
        else if ("avoid".equals(action))
            return badWordSet().equals(p.badWordSet());
        else
            return false;
    }

    @Override
    public int hashCode() {
        return Objects.hash(getName(), action);
    }

    @Override
    public String getActions() {
        return action;
    }

    //解析权限字符串
    public Set<String> badWordSet() {
        HashSet<String> set = new HashSet<>();
        String[] bads = getName().split(",");
        for (String bad : bads) {
            set.add(bad);
        }
        return set;
    }
}

4. 测试:

public class PermissionTest {
    public static void main(String[] args) {
        //设置权限文件
        System.setProperty("java.security.policy", "src/main/java/pers/zhang/securitymanager/my.policy");
        //设置安全管理器
        System.setSecurityManager(new SecurityManager());
        //启动窗体
        EventQueue.invokeLater(() -> {
            PermissionTestFrame frame = new PermissionTestFrame();
            frame.setTitle("PermissionTest");
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.setVisible(true);
        });
    }
}


//窗体
class PermissionTestFrame extends JFrame {
    private JTextField textField;
    private WordCheckTextArea textArea;
    private static final int TEXT_ROWS = 20;
    private static final int TEXT_COLUMS = 60;

    public PermissionTestFrame() {
        textField = new JTextField(20);
        JPanel panel = new JPanel();
        panel.add(textField);
        JButton openButton = new JButton("Insert");
        panel.add(openButton);
        openButton.addActionListener(event -> insertWords(textField.getText()));

        add(panel, BorderLayout.NORTH);

        textArea = new WordCheckTextArea();
        textArea.setRows(TEXT_ROWS);
        textArea.setColumns(TEXT_COLUMS);
        add(new JScrollPane(textArea), BorderLayout.CENTER);
        pack();
    }


    public void insertWords(String words) {
        try {
            textArea.append(words + "\n");
        } catch (SecurityException e) {
            JOptionPane.showMessageDialog(this, "I am sorry, but I cannot do that.");
            e.printStackTrace();
        }
    }
}

//文本框
class WordCheckTextArea extends JTextArea {
    //重新append方法,判断是否包含违禁词
    @Override
    public void append(String str) {
        WordCheckPermission p = new WordCheckPermission(str, "insert");
        //获取安全管理器,校验权限
        SecurityManager securityManager = System.getSecurityManager();
        if (securityManager != null) {
            securityManager.checkPermission(p);
        }
        super.append(str);
    }
}

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

3、用户认证

Java API 提供了一个名为Java认证和授权服务的框架,它将平台提供的认证与权限管理集成起来。

3.1、核心类及接口

3.1.1、通用
  • Subject: 认证的主体,可以代表一个人,一个角色一个进程等。认证成功之后可以从LoginContext获取Subject,代表一种身份,用户可以通过这个身份执行一些需要权限才能运行的逻辑。
  • Principals: 可认为是一种权限。一个Subject可以包含多个Principal。用户认证成功之后,把授予的Principal加入到和该用户关联的Subject中,该用户便具有了这个Principal的权限。
  • Credentials: 凭证,包括公共凭证(Public credentials,如:名称或公共密钥)和私有凭证(Private credentials,如:口令或私有密钥)。凭证并不是一个特定的类或接口,它可以是任何对象。凭证中可以包含任何特定安全系统需要的验证信息,例如标签(ticket),密钥或口令。
3.1.2、Authentication
  • LoginModule: 登录模块,不同LoginModule对应了不同的认证方式。例如:Krb5LoginModule,使用Kerberos作为认证方式,FileLoginModule使用外部保存的密码文件来认证,UsernameLoginModule通过用户名密码来认证。
  • CallbackHandler: 在某些情况下,LoginModule需要与用户通信以获取身份验证信息;LoginModule使用CallbackHandler来实现此目标。应用程序实现CallbackHandler接口并将其传递给LoginContext,LoginContext将其直接转发给底层LoginModule。LoginModule使用CallbackHandler来收集来自用户的输入(例如密码或智能卡pin号)或向用户提供信息(例如状态信息)。应用重新通过指定CallbackHandler来实现与用户交互的各种不同方式。例如,GUI应用程序CallbackHandler的实现可能会显示一个窗口来征求用户的输入。另一方面,非GUl工具的CallbackHandler的实现可能只是简单地提示用户直接从命令行输入。
  • Callback: javax.security.auth.callback包中包含了Callback接口及几个实现类。LoginModules将一个Callback数组传递给CallbackHandler的handler方法。
3.1.3、authorization
  • Policy: policy类是一个抽象类,用于表示系统范围的访问控制策略。Policy有对应的policy文件来配置访问权限。
  • AuthPermission: AuthPermission类封装了JAAS所需的基本权限;AuthPermission包含了一个名称,但不包含操作列表。
  • PrivateCredentialPermission: 用于保护对Subject私有凭证的访问。

3.2、配置文件

3.2.1、JAAS Login Configuration File

JAAS登录配置文件,主要用户用户的认证;一个配置文件中可以包含多个entry,一个entry的格式如下:

<name userd by application to refer to this entry> {
	<LoginModule> <flag> <LoginModule options>;
	<optional additional LoginModules, flags and options>;
};

一个entry的配置主要包括LoginModule、flag、LoginModule options,一个配置例子如下:

MyLogin {
	com.abc.demo.general.jaas.MyLoginModule required;
}

LoginModule:

LoginModule的类型决定了使用何种方式认证,需要配置为全限定类名。一个entry可以配置多个LoginModule,认证流程会按照顺序依次执行各个LoginModule,flag的值会影响认证流程的走向。

flag:

flag有如下取值:

  • required:此LoginModule必须成功;无论成功与否,认证流程都会继续走后面的LoginModule。
  • requisite:此LoginModule必须成功;如果成功,认证流程会继续走后面的LoginModule,如果失败,认证流程终止。
  • sufficient:此LoginModule不必成功;如果成功,认证流程终止,如果失败,认证流程会继续走后面的LoginModule。
  • optional:此LoginModule不必成功;无论成功与否,认证流程都会继续走后面的LoginModule。

整个认证流程是否成功的判定标准:

  1. 只有当所有的required和requisite LoginModule成功时,整个认证流程才成功。
  2. 如果配置了suficient LoginModule并且认证成功,在它之前所有的required和requisiet LoginModule都成功,整个认证流程才算成功。
  3. 如果没有配置任何required和requisite LoginModule,sufficient或optional LoginModule至少成功一个,整个认证流程才算成功。

options:

options为LoginModule接收的认证选项。格式为0或多个key=value,常用于传递认证附属参数。在LoginModule中使用一个Map来接收并处理这些参数。

3.2.2、Policy File

policy文件主要用来配置权限,其格式为:

grant signedBy "signer_names", codeBase "URL",
	principal principal_class_name "principal_name",
	principal principal_class_name "principal_name",
	... {
	permission permission_class_name "target_name", "action", signedBy "signer_names";
	permission permission_class_name "target_name", "action", signedBy "signer_names";
	...
};

signedBy:

针对某个签名者赋予权限。可使用jarsigner xxx.jar signer_name为jar文件签名。

codeBase:

用于为某个目录下的用户代码授权。

principal:

用于为特定的Principal授权。

permission:

permission部分为赋予的具体权限;permission_class_name表示权限名称,target_name表示权限作用的目标,action表示权限;如:

//读取d盘各级目录所有文件的权限
permission java.io.FilePermission "d:/-", "read";

//读取/设置环境变量的权限
permission java.util.PropertyPermission "os.name", "read";
permission java.util.PropertyPermission "java.security.auth.login.config", "write";

3.3、JAAS框架

Java认证和授权服务(JAAS,Java Authentication and Authorization Service)包含两部分:

  • 认证:主要负责确定程序使用者的身份
  • 授权:将各个用户映射到相应的权限

JAAS是一个可插拔的API,可以将Java应用程序与实现认证的特定技术分离开来。除此之外,JAAS还支持UNIX登录、NT登录、Kerberos认证和基于证书的认证。

一旦用户通过认证,就可以为其附加i组权限。例如,这里我们赋予Harry一个特定的权限集,,而其他用户则没有,它的语法规则如下:

grant principal com.sun.security.auth.UnixPrincipal "harry"
{
	permissiion java.util.PropertyPermission "user.*", "read";
	...
};

com.sun.security.auth.UnixPrincipal类检查运行该程序的UNIX用户的名字,它的getName方法将返回UNIX登录名,然后我们就可以检查该名称是否等于"harry"。

可以使用一个LoginContext以使得安全管理器能够检查这样的授权语句。下面是登录代码的基本轮廓:

try {
	System.setSecurityManager(new SecurityManager());
	//参数Login1是指JAAS配置文件中具有相同名字的项
	var context = new LoinContext("Login1");
	context.login();
	//subject是指已经被认证的个体
	Subject subject = context.getSubject();
	...
	content.logout();
} catch(LoginException exception) {
	exception.printStackTrace();
}

下面是一个简单的配置文件:

login1
{
	com.sun.secutiry.auth.module.UnixLoginModule required;
	com.whizzbang.auth.module.RetinaScanModule sufficient;
};

login2
{
	...
};

当然,JDK在com.sun.security.auth.module包中包含以下模块:

  • FileLoginModule
  • JndiLoginModule
  • KeyStoreLoginModule
  • Krb5LoginModule
  • LdapLoginModule
  • UnixLoginModule

在这里插入图片描述

一个登录策略由一个登录模块序列组成,每个模块被标记为required、``sufficient、requissteoptional

登录时要对登录的主体(subject)进行认证,该主体可以拥有多个特征(principal)。特征描述了主体的某些属性,比如用户名、组ID或角色等。我们在grant语句中可以看到,特征管制着各个权限。com.sun.security.auth.UnixPrincipal类描述了UNIX登录名,UnixNumericGroupPrincipal类可以用来检测用户是否归属于某个UNIX用户组。

使用下面的语法,grant语句可以对一个特征进行测试:

grant principalClass "principalName"

例如:

grant com.sun.security.auth.UnixPrincipal "harry"

当用户登录后,就会在独立的访问控制上下文中,运行要求检查用户特征的代码。使用静态的doAs或doAsPrivileged方法,启动一个新的PrivilegedAction,其run方法救护i执行这段代码。

这两个方法都可以通过使用主体特征的权限来调用某个对象的run方法去执行特定操作,而该对象必须是实现了PrivilegedAction接口的对象。

PrivilegedAction<T> action = () -> 
{
	//run with perissions of subject principals
	...
};
T result = Subject.doAs(subject, action);
//or Subject.doAsPrivileged(subject,action, null)

如果该操作会抛出受检查的异常,那么必须改为实现PrivilegedExceptionAction接口。

doAs和doAsPrivileged方法之间的区别是微小的。doAs方法开始于当前的访问控制上下文,而doAsPrivileged方法则开始于一个新的上下文。后者允许将登录代码和“业务逻辑”的权限相分离。

3.3.1、示例

在示例应用程序中,登录代码有如下权限:

permission javax.secutiry.auth.AuthPermission "createLoginContext.Login1";
permission javax.security.auth.AuthPermission "doAsPrivileged";

通过认证的用户有一个权限:

permission java.util.PropertyPermission "user.*", "read";

如果用doAs代替了doAsPrivileged,那么登录代码也需要这个权限。

AuthTest程序对用户的身份进行了认证,然后运行了一个简单的操作,以获得一个系统属性。

public class SysPropAction implements PrivilegedAction<String> {

    private String propertyName;

    public SysPropAction(String propertyName) {
        this.propertyName = propertyName;
    }

    @Override
    public String run() {
        return System.getProperty(propertyName);
    }
}
public class AuthTest {
    public static void main(String[] args) {
        System.setProperty("java.security.policy", "src/main/java/pers/zhang/jaas/auth.policy");
        System.setProperty("java.security.auth.login.config", "src/main/java/pers/zhang/jaas/jaas.config");
        System.setSecurityManager(new SecurityManager());
        try {
            LoginContext context = new LoginContext("Login1");
            context.login();
            System.out.println("Authentication successful.");
            Subject subject = context.getSubject();
            System.out.println("subject=" + subject);
            SysPropAction action = new SysPropAction("user.home");
            String result = Subject.doAsPrivileged(subject, action, null);
            System.out.println(result);
            context.logout();
        }catch (LoginException e) {
            e.printStackTrace();
        }
    }
}

权限文件auth.policy,acton_zhang是我的用户名,需要改成你自己的登录名。

grant
{
    permission javax.security.auth.AuthPermission "createLoginContext.Login1";
    permission javax.security.auth.AuthPermission "doAsPrivileged";
};

grant principal com.sun.security.auth.UnixPrincipal "acton_zhang"
{
    permission java.util.PropertyPermission "user.*", "read";
};

登录模块配置文件jaas.config

Login1
{
    com.sun.security.auth.module.UnixLoginModule required;
};

运行程序返回:

Authentication successful.
subject=主体: 
	主用户: UnixPrincipal: acton_zhang
	主用户: UnixNumericUserPrincipal: 501
	主用户: UnixNumericGroupPrincipal [主组]: 20
	主用户: UnixNumericGroupPrincipal [补充组]: 12
	主用户: UnixNumericGroupPrincipal [补充组]: 61
	主用户: UnixNumericGroupPrincipal [补充组]: 79
	主用户: UnixNumericGroupPrincipal [补充组]: 80
	主用户: UnixNumericGroupPrincipal [补充组]: 81
	主用户: UnixNumericGroupPrincipal [补充组]: 98
	主用户: UnixNumericGroupPrincipal [补充组]: 33
	主用户: UnixNumericGroupPrincipal [补充组]: 100
	主用户: UnixNumericGroupPrincipal [补充组]: 204
	主用户: UnixNumericGroupPrincipal [补充组]: 250
	主用户: UnixNumericGroupPrincipal [补充组]: 395
	主用户: UnixNumericGroupPrincipal [补充组]: 398
	主用户: UnixNumericGroupPrincipal [补充组]: 399
	主用户: UnixNumericGroupPrincipal [补充组]: 400
	主用户: UnixNumericGroupPrincipal [补充组]: 701

/Users/acton_zhang

注意:如果在Windows下运行时,请将my.policy中的UnixPricipal改为NTUser-Principal,将jaas.config中的UnixLoginModule改为NTLoginModule。

3.3.2、常用方法

javax.security.auth.login.LoginContext:

  • LoginContext(String name):创建一个登录上下文。name对应于JAAS配置文件中的登录描述符。
  • void login():建立一个登录操作,如果登录失败,则抛出一个LoginException异常。它会调用JAAS配置文件中的管理器上的login方法。
  • void logout():Subject退出登录。它会调用JAAS配置文件中的管理器上的logout方法。
  • Subject getSubject():返回认证过的Subject。

javax.security.auth.Subject:

  • Set<Principal> getPrincipals():获取该Subject的各个Principal。
  • static Object doAs(Subject subject, PrivilegedAction action)
  • static Object doAs(Subject subject, PrivilegedExceptionAction action)
  • static Object doAsPrivileged(Subject subject, PrivilegedAction action, AccessControlContext context)
  • static Object doAsPrivileged(Subject subject, PrivilegedExceptionAction action, Access ControlContext context):以subject的身份执行特许操作。它将返回run方法的返回值。doAsPrivileged方法在给定的访问控制上下文中执行该操作,你可以提供一个在前面调用静态方法
    AccessController.getContext()时所获得的“上下文快照”,或者指定为null,以便使其在一个新的上下文中执行该代码。

java.security.PrivilegedAction:

  • Object run():必须定义该方法,以执行你想要代表某个主体去执行的代码。

java.security.PrivilegedExceptionAction:

  • Object run():必须定义该方法,以执行你想要代表某个主体去执行的代码。本方法可以抛出任何受检查的异常。

java.security.Principal:

  • String getName():返回该特征的身份标识。

3.4、JAAS登录/授权

3.4.1、示例一

自定义Principal:

public class MyPrincipal implements Principal {

    private String name;

    public MyPrincipal(String name) {
        this.name = name;
    }

    @Override
    public String getName() {
        return this.name;
    }

    @Override
    public int hashCode() {
        return Objects.hashCode(name);
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null || this.getClass() != obj.getClass())
            return false;
        MyPrincipal other = (MyPrincipal)obj;
        return Objects.equals(name, other.getName());
    }

    @Override
    public String toString() {
        return "MyPrincipal{" +
                "name='" + name + '\'' +
                '}';
    }
}

自定义Callbackhandler:

public class MyCallbackHandler implements CallbackHandler {

    private String username;
    private String password;

    public MyCallbackHandler(String username, String password) {
        this.username = username;
        this.password = password;
    }

    @Override
    public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
        for (Callback callback :callbacks) {
            if (callback instanceof NameCallback) {
                ((NameCallback) callback).setName(username);
            } else if (callback instanceof PasswordCallback) {
                ((PasswordCallback) callback).setPassword(password.toCharArray());
            } else {
                throw new UnsupportedCallbackException(callback, "Callback class not supported");
            }
        }
    }
}

自定义LoginModule:

public class MyLoginModule implements LoginModule {

    private Subject subject;
    private CallbackHandler callbackHandler;
    private Principal principal;

    @Override
    public void initialize(Subject subject, CallbackHandler callbackHandler, Map<String, ?> sharedState, Map<String, ?> options) {
        this.subject = subject;
        this.callbackHandler = callbackHandler;
    }

    /**
     * 登录
     */
    @Override
    public boolean login() throws LoginException {
        try {
            NameCallback nameCallback = new NameCallback("user:");
            PasswordCallback passwordCallback = new PasswordCallback("password:", true);
            Callback[] callbacks = new Callback[]{nameCallback, passwordCallback};
            callbackHandler.handle(callbacks);
            String username = nameCallback.getName();
            String password = new String(passwordCallback.getPassword());
            return login(username, password);
        } catch (IOException e) {
            e.printStackTrace();
        } catch (UnsupportedCallbackException e) {
            e.printStackTrace();
        }
        return false;
    }

    private boolean login(String username, String password) {
        System.out.println("username=" + username + ",password=" + password);
        //实际使用中应查询数据库验证用户名密码
        if ("abc".equals(username) && "123456".equals(password)) {
            principal = new MyPrincipal(username);
            subject.getPrincipals().add(principal);
            return true;
        }
        return false;
    }

    /**
     * 登录成功后的操作
     */
    @Override
    public boolean commit() throws LoginException {
        return true;
    }

    /**
     * 中断登录
     */
    @Override
    public boolean abort() throws LoginException {
        return logout();
    }

    /**
     * 登出
     */
    @Override
    public boolean logout() throws LoginException {
        if (subject != null && principal != null) {
            subject.getPrincipals().remove(principal);
        }
        return true;
    }
}

登录模块配置文件:jaas.conf

MyLogin {
    pers.zhang.demo.MyLoginModule required;
};

授权配置文件:my.policy

//为任何主体赋权
grant
{
    //创建LoginContext的权限
   permission javax.security.auth.AuthPermission "createLoginContext.MyLogin";
   //执行特权代码的权限
   permission javax.security.auth.AuthPermission "doAsPrivileged";
   //login的时候需要将新建的principal加入subject
   permission javax.security.auth.AuthPermission "modifyPrincipals";
   //反射访问权限
   permission java.lang.reflect.ReflectPermission "suppressAccessChecks";
};

//为abc赋权
grant principal pers.zhang.demo.MyPrincipal "abc"
{
    permission java.util.PropertyPermission "user.*", "read";
};

测试:

public class JaasTest {
    public static void main(String[] args) throws LoginException {
        System.setProperty("java.security.policy", "src/main/java/pers/zhang/demo/my.policy");
        System.setProperty("java.security.auth.login.config", "src/main/java/pers/zhang/demo/jaas.conf");
        System.setSecurityManager(new SecurityManager());
        MyCallbackHandler myCallbackHandler = new MyCallbackHandler("abc", "123456");
        //使用MyLgin这个entry进行认证
        LoginContext loginContext = new LoginContext("MyLogin", myCallbackHandler);
        loginContext.login();

        Subject subject = loginContext.getSubject();
        Subject.doAsPrivileged(subject, new PrivilegedAction<Object>() {
            @Override
            public Object run() {
                String path = System.getProperty("user.home");
                System.out.println(path);
                return null;
            }
        }, null);
        loginContext.logout();
    }
}
username=abc,password=123456
/Users/acton_zhang
3.4.2、示例二

基于角色的认证与授权。

自定义Principal:

主体的特征集,如果一个登录模块支持某些角色,该模块就会添加Principal对象来描述这些角色。该类存储了一个键值对,例如role=admin。该类的getName方法用于返回该键值对,因此就可以添加基于角色的权限到策略文件中。

public class SimplePrincipal implements Principal {

    private String descr;
    private String value;


    public SimplePrincipal(String descr, String value) {
        this.descr = descr;
        this.value = value;
    }

    @Override
    public String getName() {
        return descr + "=" + value;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        SimplePrincipal other = (SimplePrincipal)obj;
        return Objects.equals(getName(), other.getName());
    }

    @Override
    public int hashCode() {
        return Objects.hashCode(getName());
    }

    @Override
    public String toString() {
        return "SimplePrincipal{" +
                "descr='" + descr + '\'' +
                ", value='" + value + '\'' +
                '}';
    }
}

登录模块:

如果匹配成功,会把用户名和角色两个SimplePrincipal添加到主体的特征集中。

public class SimpleLoginModule implements LoginModule {

    private Subject subject;
    private CallbackHandler callbackHandler;
    private Map<String, ?> optinos;

    @Override
    public void initialize(Subject subject, CallbackHandler callbackHandler, Map<String, ?> sharedState, Map<String, ?> options) {
        this.subject = subject;
        this.callbackHandler = callbackHandler;
        this.optinos = options;
    }

    @Override
    public boolean login() throws LoginException {
        if (callbackHandler == null)
            throw new LoginException("no handler");
        NameCallback nameCallback = new NameCallback("username:");
        PasswordCallback passwordCallback = new PasswordCallback("password:", false);
        try {
            callbackHandler.handle(new Callback[]{nameCallback, passwordCallback});
        } catch (UnsupportedCallbackException e) {
            LoginException ex = new LoginException("Unsupported callback");
            ex.initCause(e);
            throw ex;
        } catch (IOException e) {
            LoginException ex = new LoginException("I/O exception in callback");
            ex.initCause(e);
            throw ex;
        }

        try {
            boolean flag = checkLogin(nameCallback.getName(), new String(passwordCallback.getPassword()));
            return flag;

        } catch (IOException e) {
            LoginException ex = new LoginException();
            ex.initCause(e);
            throw ex;
        }
    }


    private boolean checkLogin(String username, String password) throws IOException {
        try (Scanner in = new Scanner(
                Paths.get("" + optinos.get("pwfile")),"UTF-8")) {
            while (in.hasNextLine()) {
                String[] inputs = in.nextLine().split("\\|");
                if (inputs[0].equals(username) && inputs[1].equals(password)) {
                    String role = inputs[2];
                    Set<Principal> principals = subject.getPrincipals();
                    principals.add(new SimplePrincipal("username", username));
                    principals.add(new SimplePrincipal("role", role));
                    return true;
                }
            }
        }
        return false;
    }

    @Override
    public boolean commit() throws LoginException {
        return true;
    }

    @Override
    public boolean abort() throws LoginException {
        return true;
    }

    @Override
    public boolean logout() throws LoginException {
        return true;
    }
}

CallBackHandler:

用于获取GUI对话框中的用户名和密码。

public class SimpleCallbackHandler implements CallbackHandler {

    private String username;
    private String password;

    public SimpleCallbackHandler(String username, String password) {
        this.username = username;
        this.password = password;
    }

    @Override
    public void handle(Callback[] callbacks) {
       for (Callback callback : callbacks) {
           if (callback instanceof NameCallback) {
               ((NameCallback)callback).setName(username);
           } else if (callback instanceof PasswordCallback) {
               ((PasswordCallback) callback).setPassword(password.toCharArray());
           }
       }
    }
}

自定义PrivilegedAction:

获取user.home

public class SysPropAction implements PrivilegedAction<String>
{
    private String propertyName;

    /**
     * Constructs an action for looking up a given property.
     * @param propertyName the property name (such as "user.home")
     */
    public SysPropAction(String propertyName)
    {
        this.propertyName = propertyName;
    }

    public String run()
    {
        return System.getProperty(propertyName);
    }
}

GUI窗体:

public class JAASFrame extends JFrame {
    private JTextField username;
    private JPasswordField password;
    private JTextField propertyName;
    private JTextField propertyValue;

    public JAASFrame() {
        username = new JTextField(20);
        password = new JPasswordField(20);
        propertyName = new JTextField("user.home");
        propertyValue = new JTextField(20);
        propertyValue.setEditable(false);

        JPanel panel = new JPanel();
        panel.setLayout(new GridLayout(0, 2));
        panel.add(new JLabel("username:"));
        panel.add(username);
        panel.add(new JLabel("password:"));
        panel.add(password);
        panel.add(propertyName);
        panel.add(propertyValue);
        add(panel, BorderLayout.CENTER);

        JButton getValueButton = new JButton("Get Value");
        getValueButton.addActionListener(event -> getValue());
        JPanel buttonPanel = new JPanel();
        buttonPanel.add(getValueButton);
        add(buttonPanel, BorderLayout.SOUTH);
        pack();
    }

    public void getValue() {
        try
        {
            LoginContext context = new LoginContext("Login1", new SimpleCallbackHandler(username.getText(), new String(password.getPassword())));
            System.out.println("Trying to log in with " + username.getText() + " and " + new String(password.getPassword()));
            context.login();
            Subject subject = context.getSubject();
            System.out.println(subject);
            //在Subject中才有Principal信息,这样就可以针对每一种用户身份制定一套权限方案。
            String result = Subject.doAsPrivileged(subject, new SysPropAction(propertyName.getText()), null);
            propertyValue.setText(result  );
            context.logout();
        }
        catch (LoginException ex)
        {
            ex.printStackTrace();
            Throwable ex2 = ex.getCause();
            if (ex2 != null) ex2.printStackTrace();
        }
    }
}

jaas登录模块配置:jaas.config

Login1
{
   pers.zhang.auth.SimpleLoginModule required pwfile="src/main/java/pers/zhang/auth/password.txt" debug=true;
};

权限策略配置:jaas.policy

grant
{
   permission java.awt.AWTPermission "showWindowWithoutWarningBanner";
   permission java.awt.AWTPermission "accessEventQueue";
   permission javax.security.auth.AuthPermission "createLoginContext.Login1";
   permission javax.security.auth.AuthPermission "doAsPrivileged";
   permission javax.security.auth.AuthPermission "modifyPrincipals";
   permission java.io.FilePermission "src/main/java/pers/zhang/auth/password.txt", "read";
   permission java.lang.reflect.ReflectPermission "suppressAccessChecks";
};

//只有admin角色才能读取
grant principal pers.zhang.auth.SimplePrincipal "role=admin"
{
    permission java.util.PropertyPermission "user.*", "read";
};

账户密码文件:password.txt

tom|123|admin
jerry|456|HR

测试

public class JAASTest {
    public static void main(String[] args) {
        System.setProperty("java.security.policy", "src/main/java/pers/zhang/auth/jaas.policy");
        System.setProperty("java.security.auth.login.config", "src/main/java/pers/zhang/auth/jaas.config");
        System.setSecurityManager(new SecurityManager());
        EventQueue.invokeLater(() -> {
            JAASFrame frame = new JAASFrame();
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.setTitle("JAAStest");
            frame.setVisible(true);
        });
    }
}

在这里插入图片描述

3.4.3、常用方法

javax.security.auth.callback.CallbackHandler

  • void handler(Callback[] callbacks):处理给定的callback,如果愿意,可以与用户进行交互,并将安全信息存储到callback对象中。

javax.security.auth.callback.NameCallback

  • NameCallback(String prompt)
  • NameCallback(String prompt, String defaultName):用给定提示符和默认的名字构建一个NameCallback。
  • String getName()
  • void setName(String name):设置或者获取该callback所收集到的名字。
  • String getPrompt():获取查询该名字时所使用的提示符。
  • String getDefaultName():获取查询该名字时所使用的默认名字。

javax.security.auth.callback.PasswordCallback

  • PasswordCallback(String prompt, boolean echoOn):用给定提示符和回显标记构建一个PasswordCallback。
  • char[] getPassword()
  • void setPassword(char[] password):设置或者获取该callback所收集到的密码。
  • String getPrompt():获取查询该密码时使用的提示符。
  • boolean isEchoOn():获取查询该密码时所使用的回显标记。

javax.security.auth.spi.LoginModule

  • void initialize(Subject subject, CallbackHandler handler, Map<String, ?> sharedState, Map<String,?> options):为了认证给定的subject,初始化该LoginModule。在登录处理期间,用给定的handler来收集登录信息:使用sharedState映射表与其他登录模块进行通信;options映射表包含该模块实例的登录配置中指定的键/值对。
  • boolean login():执行认证过程,并组装主体的特征集。如果登录成功,则返回true。
  • boolean abort():如果某一登录模块失败导致登录过程中断,就调用该方法。如果操作成功,则返回true。
  • boolean logout():注销当前的主体。如果操作成功,则返回truue。

4、数字签名

数字签名(又称公钥数字签名)是只有信息的发送者才能产生的别人无法伪造的一段数字串,这段数字串同时也是对信息的发送者发送信息真实性的一个有效证明。它是一种类似写在纸上的普通的物理签名,但是在使用了公钥加密领域的技术来实现的,用于鉴别数字信息的方法。一套数字签名通常定义两种互补的运算,一个用于签名,另一个用于验证。数字签名是非对称密钥加密技术与数字摘要技术的应用。在Java.security包中包含了许多加密算法的实现。

4.1、消息摘要(MessageDigest)

消息摘要(message digest)是数据块的数字指纹。例如,所谓的SHA1(安全散列算法)可将任何数据块,无论其数据多长,都压缩为160位(20字节)的序列。

消息摘要具有两个基本属性:

  1. 如果数据的1位或者几位改变了,那么消息摘要也将改变。
  2. 拥有给定消息的伪造者无法创建与原消息具有相同摘要的假消息。

常用的消息摘要算法:

  • SHA-1、SHA-2系列
  • MD5

MessageDigest类是用于创建了指纹算法的对象的"工厂",它的静态方法getInstance返回继承了MessageDigest类的某个类的对象。这意味着MessageDigest类能够承担下面的双重职责:

  • 作为一个工厂类
  • 作为所有消息摘要算法的超类

MessageDigest方法:

  • Object clone():如果实现是可复制的,则返回一个副本
  • byte[] digest():完成哈希计算
  • byte[] digest(byte[] inpu):使用指定的字节数组对摘要进行最后的更新,然后完成摘要计算
  • int digest(byte[] buf, in offset, int len):通过执行诸如填充之类的最终操作完成哈希计算
  • String getAlgorithm():返回标识算法的独立于实现细节的字符串
  • int getDigestLength():返回以字节为单位的摘要长度,如果提供程序不支持此操作并且实现是不可复制的,则返回0
  • static MessageDigest getInstance(String algorithm):生成实现指定摘要算法的MessageDigst对象
  • static MessageDigest getInstance(String algorithm, Provider provider):生成实现指定提供程序提供的指定算法的 MessageDigest 对象,如果该算法可从指定的提供程序得到的话
  • static MessageDigest getInstance(String algorithm, String provider):生成实现指定提供程序提供的指定算法的 MessageDigest 对象,如果该算法可从指定的提供程序得到的话
  • Provider getProvider():返回此信息摘要对象的提供程序
  • sttic boolean isEqual(byte[] digesta, byte[] digestb):比较两个摘要的相等性
  • void reset():重置摘要以供再次使用
  • String toString():返回此信息摘要对象的字符串表示形式
  • void update(byte input):使用指定的字节更新摘要
  • void update(byte[] input):使用指定的字节数组更新摘要
  • void update(byte[] input, int offset, int len):使用指定的字节数组,从指定的偏移量开始更新摘要
  • void update(ByteBuffer input):使用指定的 ByteBuffer 更新摘要

支持的算法:

public static void main(String[] args) {
    Provider[] providers = Security.getProviders();
    for (Provider p : providers) {
        System.out.println(p);
        for (Provider.Service s : p.getServices()) {
            if (s.getType().equals("MessageDigest")) {
                System.out.println("\ttype=" + s.getType() + ",algorithm=" + s.getAlgorithm());
            }
        }
    }
}
SUN version 1.8
	type=MessageDigest,algorithm=MD2
	type=MessageDigest,algorithm=MD5
	type=MessageDigest,algorithm=SHA
	type=MessageDigest,algorithm=SHA-224
	type=MessageDigest,algorithm=SHA-256
	type=MessageDigest,algorithm=SHA-384
	type=MessageDigest,algorithm=SHA-512
SunRsaSign version 1.8
SunEC version 1.8
SunJSSE version 1.8
SunJCE version 1.8
SunJGSS version 1.8
SunSASL version 1.8
XMLDSig version 1.8
SunPCSC version 1.8
Apple version 1.8

示例:

public static void main(String[] args) throws NoSuchAlgorithmException {
    String str = "hello world";

    //SHA-1
    MessageDigest instance = MessageDigest.getInstance("SHA-1");
    byte[] bytes = instance.digest(str.getBytes());
    String result = HexUtil.encodeHexStr(bytes);
    //2aae6c35c94fcfb415dbe95f408b9ce91ee846ed
    System.out.println(result);


    //MD5
    MessageDigest md5 = MessageDigest.getInstance("MD5");
    byte[] bytes1 = md5.digest(str.getBytes());
    String result1 = HexUtil.encodeHexStr(bytes1);
    //5eb63bbbe01eeed093cb22bb8f5acdc3
    System.out.println(result1);

    //SHA-512
    MessageDigest sha_512 = MessageDigest.getInstance("SHA-512");
    String salt = "123456";
    String name = salt + str;
    System.out.println("盐值:" + salt);
    System.out.println("盐值加密:" + name);

    byte[] bytes2 = sha_512.digest(name.getBytes());
    String result2 = HexUtil.encodeHexStr(bytes2);
    //343ef3bd0bc79722d96b494ff0f155cffa091cc1b06d1d8ce7139bcb69456b5f1da6170aaffdbe07e714a531d8987219daa54530a81e3b5841caba7e11cfd64c
    System.out.println(result2);
}

4.2、消息签名(Signature)

如果消息和摘要是分开发送的,那么接收者就可以检查是否被篡改过。但是,如果消息和摘要同时被截获了,对消息进行修改,再重新计算摘要,就是一个很容易的事情。毕竟,消息摘要算法是公开的,不需要使用任何密钥。在这种情况下,假消息和新摘要的接收者永远不会知道消息已经被篡改。数字签名解决了这个问题。

为了了解数字签名的工作原理,我们需要解释关于公共密钥加密技术领域中的几个概念。公共密钥加密技术是基于公共密钥和私有密钥这两个基本概念的。它的设计思想是你可以将公共密钥告诉世界上的任何人,但是,只有自己才持有私有密钥,重要的是你要保护你的私有密钥,不将它泄漏给其他任何人。这些密钥之间存在一定的数学关系,但是这种关系的具体性质对于实际的编程来说并不重要。

密钥非常长,而且很复杂。在现实中,几乎不可能用一个密钥去推算出另一个密钥。也就是说,即使每个人都知道你的公共密钥,不管它们拥有多少计算机资源,它们一辈子也无法计算出你的私有密钥。

工作机制:使用DSA进行公共密钥签名的交换

在这里插入图片描述

假设Alice想要给Bob发送一个消息,Bob想知道该消息是否来自Alice,而不是冒名顶替者。Alice写好了消息,并且用她的私有密钥对该消息摘要签名。Bob得到了她的公共密钥
的拷贝,然后Bob用公共密钥对该签名进行校验。如果通过了校验,则Bob可以确认以下两个事实:

  1. 原始消息没有被篡改过。
  2. 该消息是由Alice签名的,她是私有密钥的持有者,该私有密钥就是与Bob用于校验的公共密钥相匹配的密钥。

java中可用的签名算法:

public static void main(String[] args) {
    Provider[] providers = Security.getProviders();
    for (Provider p : providers) {
        System.out.println(p);
        for (Provider.Service s : p.getServices()) {
            if (s.getType().equals("Signature")) {
                System.out.println("\ttype=" + s.getType() + ",algorithm=" + s.getAlgorithm());
            }
        }
    }
}
SUN version 1.8
	type=Signature,algorithm=SHA1withDSA
	type=Signature,algorithm=NONEwithDSA
	type=Signature,algorithm=SHA224withDSA
	type=Signature,algorithm=SHA256withDSA
SunRsaSign version 1.8
	type=Signature,algorithm=MD2withRSA
	type=Signature,algorithm=MD5withRSA
	type=Signature,algorithm=SHA1withRSA
	type=Signature,algorithm=SHA224withRSA
	type=Signature,algorithm=SHA256withRSA
	type=Signature,algorithm=SHA384withRSA
	type=Signature,algorithm=SHA512withRSA
SunEC version 1.8
	type=Signature,algorithm=NONEwithECDSA
	type=Signature,algorithm=SHA1withECDSA
	type=Signature,algorithm=SHA224withECDSA
	type=Signature,algorithm=SHA256withECDSA
	type=Signature,algorithm=SHA384withECDSA
	type=Signature,algorithm=SHA512withECDSA
SunJSSE version 1.8
	type=Signature,algorithm=MD2withRSA
	type=Signature,algorithm=MD5withRSA
	type=Signature,algorithm=SHA1withRSA
	type=Signature,algorithm=MD5andSHA1withRSA
SunJCE version 1.8
SunJGSS version 1.8
SunSASL version 1.8
XMLDSig version 1.8
SunPCSC version 1.8
Apple version 1.8

4.3、校验签名

JDK配有一个keytool程序,该程序是一个命令行工具,用于生成和管理一组证书。keytool程序负责管理密钥库、证书数据库和私有/共有密钥对。密钥库中的每一项都有一个"别名"。

下面展示的是如何创建一个密钥库acton.certs并且用别名生成一个密钥对。

keytool -genkeypair -keystore acton.certs -alias acton

当新建或者打开一个密钥库时,系统将提示输人密钥库口令,在下面的这个例子中,口令就使用123456,如果要将keytool生成的密钥库用于重要的应用,那么需要选择一个好的口令来保护这个文件。

当生成一个密钥时,系统提示输人下面这些信息:

输入密钥库口令:  
再次输入新口令: 
您的名字与姓氏是什么?
  [Unknown]:  zhang
您的组织单位名称是什么?
  [Unknown]:  test
您的组织名称是什么?
  [Unknown]:  test
您所在的城市或区域名称是什么?
  [Unknown]:  Beijing
您所在的省//自治区名称是什么?
  [Unknown]:  Beijing
该单位的双字母国家/地区代码是什么?
  [Unknown]:  CN
CN=zhang, OU=test, O=test, L=Beijing, ST=Beijing, C=CN是否正确?
  []:  y

输入 <acton> 的密钥口令
        (如果和密钥库口令相同, 按回车):  
再次输入新口令: 

keytool工具使用X.500格式的名字,它包含常用名(CN)、机构单位(OU)、机构(O)、地点(L)、州(ST)和国别(C)等成分,以确定密钥持有者和证书发行者的身份。

最后,必须设定一个密钥口令,或者按回车键,将密钥库口令作为密钥口令来使用。

假设Alice想把她的公共密钥提供给Bob,她必须导出一个证书文件:

keytool -exportcert -keystore acton.certs -alias acton -file acton.cer

这时,Alice就可以把证书发送给Bob。当Bob收到该证书时,他就可以将证书打印出来:

keytool -printcert -file acton.cer
所有者: CN=zhang, OU=test, O=test, L=Beijing, ST=Beijing, C=CN
发布者: CN=zhang, OU=test, O=test, L=Beijing, ST=Beijing, C=CN
序列号: 6e00e6c9
有效期为 Mon Nov 13 23:05:49 CST 2023Sun Feb 11 23:05:49 CST 2024
证书指纹:
         MD5:  EC:D4:DA:A5:E0:35:58:3E:B3:2A:4D:D4:39:0E:C9:BF
         SHA1: CD:B4:ED:8B:4F:38:94:51:88:0E:FD:22:C3:0D:88:F4:B3:6F:FA:55
         SHA256: 45:41:DC:75:60:73:59:CE:0E:89:EF:BC:41:E2:09:1E:45:4B:5E:CB:C1:AF:8B:BB:50:60:F1:47:B5:36:73:CF
签名算法名称: SHA256withDSA
主体公共密钥算法: 2048DSA 密钥
版本: 3

扩展: 

#1: ObjectId: 2.5.29.14 Criticality=false
SubjectKeyIdentifier [
KeyIdentifier [
0000: 30 69 CA E7 FA BE 25 E1   5D 74 8D 13 50 AD 8B 1E  0i....%.]t..P...
0010: A6 2C 58 0D                                        .,X.
]
]

现在Alice就可以给Bob发送签过名的文档了。

jarsigner工具负责对jar文件进行签名和校验,Alice只需要将文档添加到要签名的JAR文件中。

jar cvf document.jar document.txt

然后使用jarsigner工具将签名添加到文件中,她必须指定要使用的密钥库、JAR文件和密钥的别名。

jarsigner -keystore acton.certs document.jar acton

当Bob收到JAR文件时,他可以使用jarsigner程序的-verify选项,对文件进行校验。

jarsigner -verify -keystore bob.certs document.jar

Bob不需要设定密钥别名。jarsigner程序会在数字签名中找到密钥所有者的X.509名字,并在密钥库中搜寻匹配的证书。

如果JAR文件没有收到破坏而且签名匹配,那么jarsigner程序将打印:

jar verified.

否则,程序将显示一个出错消息:

4.4、认证问题

假设你从朋友Alice那接收到一个消息,该消息是Alice用她的私有密钥签名的,使用的签名方法就是我们刚刚介绍的方法。你可能已经有了她的公共密钥,或者你能够容易地获得她的公共密钥,比如问她要一个密钥拷贝,或者从她的Web页中获得密钥。这时,你就可以校验该消息是否是Aic签过名的,并且有没有被破坏过。现在,假设你从一个声称代表某著名软件公司的陌生人那里获得了一个消息,他要求你运行消息附带的程序。这个陌生人甚至将他的公共密钥的拷贝发送给你,以便让你校验他是否是该消息的作者。你检查后会发现该签名是有效的,这就证明该消息是用匹配的私有密钥签名的,并且没有遭到破坏。

此时你要小心:你仍然不清楚谁写的这条消息。任何人都可以生成一对公共密钥和私有密钥,再用私有密钥对消息进行签名,然后把签名好的消息和公共密钥发送给你。这种确定发送者身份的问题称为“认证问题”。

解决这个认证问题的通常做法是比较简单的。假设陌生人和你有一个你们俩都值得信赖的共同熟人。假设陌生人亲自约见了该熟人,将包含公共密钥的磁盘交给了他。后来,你的熟人与你见面,向你担保他与该陌生人见了面,并且该陌生人确实在那家著名的软件公司工作,然后将磁盘交给你。这样一来,你的熟人就证明了陌生人身份的真
实性。

在这里插入图片描述

事实上,你的熟人并不需要与你见面。取而代之的是,他可以将他的私有签名应用于陌生人的公共密钥文件之上即可。

当你拿到公共密钥文件之后,就可以检验你的熟人的签名是否真实,由于你信任他,因此你确信他在添加他的签名之前,确实核实了陌生人的身份。

然而,你们之间可能没有共同的熟人。有些信任模型假设你们之间总是存在一个“信任链”一即一个共同熟人的链路一这样你就可以信任该链中的每个成员。当然,实际情况并不总是这样。你可能信任你的熟人Alice,而且你知道Alice信任Bob,但是你不了解Bob,因此你没有把握究竟是不是该信任他。其他的信任模型则假设有一个我们大家都信任的慈善大佬,即一家我们大家都信任的公司。在这样的公司中,如雷贯耳的有DigiCert、GlobalSign和Entrust,它们都提供认证服务。

在这里插入图片描述

你常常会遇到由负责担保他人身份的一个或多个实体签署的数字签名,你必须评估一下究竟能够在多大程度上信任这些身份认证人。你可能非常信赖某种特定的证书授权,因为也许你在许多网页中都看到过他们的标志,或者你曾经听说过,每当有新的万能密钥产生时,他们就会要求在一个非常保密的会议室中聚集众多揣着黑色公文包的人进行磋商。

然而,对于实际被认证的对象,你应该抱有一个符合实际的期望:直接在Wb页面上填一份表格,并支付少量的费用,就可以获得一个“第一类”(class1)ID,包含在证书中的密
钥将被发送到指定的邮件地址。因此,你有理由相信该电子邮件是真实的,但是密钥申请人也可能填入任意名字和机构。还有其他对身份信息的检验更加严格的D类别。例如,如果是“第三类”(class3)ID,证书授权将要求密钥申请人必须进行身份公证,公证机构将要核实企业申请者的财务信用资质。其他认证机构将采用不同的认证程序。因此,当你收到一条经过认证的消息时,重要的是你应该明白它实际上认证了什么。

4.5、证书签名

假设Alice想要给同事Cindy发送一条经过签名的消息,但是Cindy并不希望因为要校验许多签名指纹而受到困扰。因此,假设有一个Cidy信任的实体来校验这些签名。在这个
例子中,Cindy信任ACME软件公司的信息资源部。

这个部门负责证书授权(CA)的运作。ACME的每个人在其密钥库中都有CA的公共密钥,这是由一个专门负责详细核查密钥指纹的系统管理员安装的。CA对ACME雇员的密钥
进行签名,当他们在安装彼此的密钥时,密钥库将隐含地信任这些密钥,因为它们是由一个可信任的密钥签名的。

下面展示了可以如何模仿这个过程。首先需要创建一个密钥库acmesoft.Certs,生成一个密钥对并导出公共密钥。

keytool -genkeypair -keystore acmesoft.certs -alias acmeroot
keytool -exportcert -keystore acmesoft.certs a-lias acmeroot -file acmeroot.cer

其中的公共密钥被导入到了一个自签名的证书中,然后将其添加到每个雇员的密钥库中:

keytool -importcert -keystore cindy.certs -alias acmeroot -file acmeroot.cer

如果Alice要发送消息给Cidy以及ACME软件公司的其他任何人,她需要将她自己的证书签名——并提交给信息资源部。但是,这个功能在keytool程序中是缺失的。

在本书附带的代码中,我们提供了一个CertificateSigner类来弥补这个问题。ACME软件公司的授权机构成员将负责核实Alice的身份,并且生成如下的签名证书:

java CertificateSigner keystore acmesoft.certs -alias acmeroot -infile aclice.cer -outfile alice_signedby_acmeroot.cer

证书签名器程序必须拥有对ACME软件公司密钥库的访问权限,并且该公司成员必须知道密钥库的口令,显然这是一项敏感的操作。

现在Alice将文件alice signedby acmeroot.cert交给Cindy和ACME软件公司的其他任何人。或者,ACME软件公司直接将该文件存储在公司的目录中。请记住,该文件包含了
Alice的公共密钥和ACME软件公司的声明,证明该密钥确实属于Alice

现在,Cindy将签名的证书导入到她的密钥库中:

keytool -importcert -keystore cindy.certs -alias alice -file alice_signedby_acmeroot.cer

密钥库要进行校验,以确定该密钥是由密钥库中已有的受信任的根密钥签过名的。Cidy就不必对证书的指纹进行校验了。

一旦Cidy添加了根证书和经常给她发送文档的人的证书后,她就再也不用担心密钥库了。

4.6、证书请求

在上一节中,我们用密钥库和CertificateSigner工具模拟了一个CA。但是,大多数CA都运行着更加复杂的软件管理证书,并且使用的证书格式也略有不同。

用OpenSSL软件包作为实例。许多Linux系统和Mac OS X都预装这个软件,并且用于Windows的Cygwin端口也可用这个软件。

为了创建一个CA,需要运行CA脚本,其确切依赖于你的熬做系统。在Ubuntu上,运行:

/usr/lib/sll/misc/CA.pl -newca

这个脚本会在当前目录中创建一个demoCA子目录,这个目录包含了一个根密钥对,并存储了证书与证书撤销列表。

你希望将这个公共密钥导入到所有雇员的Java密钥库中,但是它的格式是隐私增强型邮件(PEM)格式,而不是密钥库更容易接受的DER格式。将文件demoCA/cacert.penm复制成文件acmeroot.pem,然后在文本编辑器中打开这个文件。

移除下面这行之前的所有内容:

-----BEGIN CERTIFICATE-----

以及下面这行之后的所有内容:

-----END CERTIFICATE-----

现在可以按照通常的方式将acmeroot.pem导入到每个密钥库中了:

keytool -importcert -keystore cindy.certs -alias alice -file acmeroot.pem

这看起来有点不可思议,keytool竟然不能自己去执行这种编辑操作。要对Alice的公共密钥签名,需要生成一个证书请求,它包含这个PEM格式的证书:

keytool -certreq -keystore alice.store -alias alice -file alice.pem

要签名这个证书,需要运行:

openssl ca -in alice.pem -out alice_signedy_acmeroot.pem

与前面一样,在alice signedby acmeroot.penm中切除BEGIN CERTIFICATE/END CERTIFICATE标记之
外的所有内容。然后,将其导入到密钥库中:

keytool -importcert -keystore cindy.certs -alias alice -file alice signedby acmeroot.pem

你可以使用相同的步骤,使一个证书得到公共证书权威机构的签名。

示例:mac openssl 生成自签名证书

生成私钥(key文件)
openssl genrsa -out client.key 4096

生成签名请求(csr文件)
openssl req -new -key client.key -out client.csr

签发证书
openssl x509 -req -days 36500 -in client.csr -signkey client.key -out client.crt

一键生成自签名证书
openssl req -new -x509 -newkey rsa:4096 -keyout test.key -out test.crt

crt证书转换为pem格式
openssl x509 -in test.crt -out test.pem

关于证书的理解:
public.pem:仅包含public证书
public.chain.crt:仅包含全部上级签发者public证书
public.crt:public.pem+public.chain.crt 组成,有先后顺序

4.7、示例

示例一:使用KeyPairGenerator生成密钥并签名:

public class SignatureTest {
    public static void main(String[] args) throws Exception {
        String text = "hello world";

        //生成密钥对
        KeyPair keyPair = generateKeyPair();

        //获取私钥和公钥
        PrivateKey privateKey = keyPair.getPrivate();
        PublicKey publicKey = keyPair.getPublic();
        System.out.println(privateKey);
        System.out.println(publicKey);

        //使用私钥进行签名
        byte[] signature = sign(text, privateKey);
        System.out.println(HexUtil.encodeHexStr(signature));


        //使用公钥验证签名
        boolean isVerified = verify(text, signature, publicKey);
        System.out.println(isVerified);
    }

    public static KeyPair generateKeyPair() throws Exception {
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("DSA");
        keyPairGenerator.initialize(1024);//设置密钥长度
        return keyPairGenerator.generateKeyPair();
    }

    public static byte[] sign(String text, PrivateKey privateKey) throws Exception {
        Signature signature = Signature.getInstance("SHA256withDSA");
        signature.initSign(privateKey);
        signature.update(text.getBytes());
        return signature.sign();
    }

    public static boolean verify(String text, byte[] signature, PublicKey publicKey) throws Exception {
        Signature verifier = Signature.getInstance("SHA256withDSA");
        verifier.initVerify(publicKey);
        verifier.update(text.getBytes());
        return verifier.verify(signature);
    }
}

示例二:使用keytool生成的证书签名

public class Signatrue2Test {

    public static final String KEY_STORE = "JKS";
    public static final String X509 = "X.509";
    public static final String KEY_STORE_PATH = "src/main/java/pers/zhang/signature/acton.certs";
    public static final String KEY_STORE_PASSWORD = "123456";
    public static final String CERTIFICATE_PATH = "src/main/java/pers/zhang/signature/acton.cer";
    public static final String CERTIFICATE_ALIAS = "acton";


    public static void main(String[] args) throws Exception {

        String text = "hello world";

        PublicKey publicKey = getPublicKey(CERTIFICATE_PATH);
        PrivateKey privateKey = getPrivateKey(KEY_STORE_PATH, CERTIFICATE_ALIAS, KEY_STORE_PASSWORD);

        //证书
        X509CertImpl certificate = (X509CertImpl) getCertificate(CERTIFICATE_PATH);

        //私钥签名
        Signature signature = Signature.getInstance(certificate.getSigAlgName());
        signature.initSign(privateKey);
        signature.update(text.getBytes());
        byte[] bytes = signature.sign();

        //公钥验证
        signature.initVerify(publicKey);
        signature.update(text.getBytes());
        boolean flag = signature.verify(bytes);
        System.out.println(flag);
    }

    /**
     * 根据文件获取密钥库
     * @param keyStorePath
     * @param password
     * @return
     * @throws Exception
     */
    public static KeyStore getKeyStore(String keyStorePath, String password) throws Exception {
        //读取密钥库文件
        FileInputStream fis = new FileInputStream(keyStorePath);
        KeyStore keyStore = KeyStore.getInstance(KEY_STORE);
        //解析
        keyStore.load(fis, password.toCharArray());
        fis.close();
        return keyStore;
    }

    /**
     * 根据证书文件获取证书
     * @param certificatePath
     * @return
     * @throws Exception
     */
    public static Certificate getCertificate(String certificatePath) throws Exception {
        //读取证书文件
        FileInputStream fis = new FileInputStream(certificatePath);
        //创建证书工厂
        CertificateFactory instance = CertificateFactory.getInstance(X509);
        //解析
        Certificate certificate = instance.generateCertificate(fis);
        fis.close();
        return certificate;
    }

    /**
     * 根据密钥库和别名获取证书
     * @param keyStorePath
     * @param alias
     * @param password
     * @return
     * @throws Exception
     */
    public static Certificate getCertificate(String keyStorePath, String alias, String password) throws Exception {
        KeyStore keyStore = getKeyStore(keyStorePath, password);
        Certificate certificate = keyStore.getCertificate(alias);
        return certificate;
    }

    /**
     * 根据证书获取公钥
     * @param certificatePath
     * @return
     * @throws Exception
     */
    public static PublicKey getPublicKey(String certificatePath) throws Exception{
        Certificate certificate = getCertificate(certificatePath);
        PublicKey publicKey = certificate.getPublicKey();
        return publicKey;
    }

    public static PrivateKey getPrivateKey(String keyStorePath, String alias, String password) throws Exception {
        KeyStore keyStore = getKeyStore(keyStorePath, password);
        PrivateKey privateKey = (PrivateKey) keyStore.getKey(alias, password.toCharArray());
        return privateKey;
    }

}

5、加密

java支持的加密算法:

public static void main(String[] args) {
   Provider[] providers = Security.getProviders();
    for (Provider p : providers) {
        System.out.println(p);
        for (Provider.Service s : p.getServices()) {
            if (s.getType().equals("Cipher")) {
                System.out.println("\ttype=" + s.getType() + ",algorithm=" + s.getAlgorithm());
            }
        }
    }
}
SUN version 1.8
SunRsaSign version 1.8
SunEC version 1.8
SunJSSE version 1.8
SunJCE version 1.8
	type=Cipher,algorithm=RSA
	type=Cipher,algorithm=DES
	type=Cipher,algorithm=DESede
	type=Cipher,algorithm=DESedeWrap
	type=Cipher,algorithm=PBEWithMD5AndDES
	type=Cipher,algorithm=PBEWithMD5AndTripleDES
	type=Cipher,algorithm=PBEWithSHA1AndDESede
	type=Cipher,algorithm=PBEWithSHA1AndRC2_40
	type=Cipher,algorithm=PBEWithSHA1AndRC2_128
	type=Cipher,algorithm=PBEWithSHA1AndRC4_40
	type=Cipher,algorithm=PBEWithSHA1AndRC4_128
	type=Cipher,algorithm=PBEWithHmacSHA1AndAES_128
	type=Cipher,algorithm=PBEWithHmacSHA224AndAES_128
	type=Cipher,algorithm=PBEWithHmacSHA256AndAES_128
	type=Cipher,algorithm=PBEWithHmacSHA384AndAES_128
	type=Cipher,algorithm=PBEWithHmacSHA512AndAES_128
	type=Cipher,algorithm=PBEWithHmacSHA1AndAES_256
	type=Cipher,algorithm=PBEWithHmacSHA224AndAES_256
	type=Cipher,algorithm=PBEWithHmacSHA256AndAES_256
	type=Cipher,algorithm=PBEWithHmacSHA384AndAES_256
	type=Cipher,algorithm=PBEWithHmacSHA512AndAES_256
	type=Cipher,algorithm=Blowfish
	type=Cipher,algorithm=AES
	type=Cipher,algorithm=AES_128/ECB/NoPadding
	type=Cipher,algorithm=AES_128/CBC/NoPadding
	type=Cipher,algorithm=AES_128/OFB/NoPadding
	type=Cipher,algorithm=AES_128/CFB/NoPadding
	type=Cipher,algorithm=AES_128/GCM/NoPadding
	type=Cipher,algorithm=AES_192/ECB/NoPadding
	type=Cipher,algorithm=AES_192/CBC/NoPadding
	type=Cipher,algorithm=AES_192/OFB/NoPadding
	type=Cipher,algorithm=AES_192/CFB/NoPadding
	type=Cipher,algorithm=AES_192/GCM/NoPadding
	type=Cipher,algorithm=AES_256/ECB/NoPadding
	type=Cipher,algorithm=AES_256/CBC/NoPadding
	type=Cipher,algorithm=AES_256/OFB/NoPadding
	type=Cipher,algorithm=AES_256/CFB/NoPadding
	type=Cipher,algorithm=AES_256/GCM/NoPadding
	type=Cipher,algorithm=AESWrap
	type=Cipher,algorithm=AESWrap_128
	type=Cipher,algorithm=AESWrap_192
	type=Cipher,algorithm=AESWrap_256
	type=Cipher,algorithm=RC2
	type=Cipher,algorithm=ARCFOUR
SunJGSS version 1.8
SunSASL version 1.8
XMLDSig version 1.8
SunPCSC version 1.8
Apple version 1.8

5.1、对称密码

"Java密码扩展"包含了一个Cipher类,该类是所有加密算法的超类。通过下面的getInstance方法可以获得一个密码对象:

Cipher cipher = Cipher.getInstance(algorithmName);

或者调用下面这个方法:

Cipher cipher = Cipher.getInstance(algoithmName, providerName);

JDK中是由名为“SunJCE”的提供商提供密码的,如果没有指定其他提供商,则会默认为该提供商。如果要使用特定的算法,而对该算法Oracle公司没有提供支持,那么也可以指定其他的提供商。

算法名称是一个字符串,比如“AES”或者“DES/CBC/PKCS5 Padding”。

DES,即数据加密标准,是一个密钥长度为56位的古老的分组密码。DES加密算法在现在看来已经是过时了,因为可以用穷举法将它破译。更好的选择是采用它的后续版本,即高级加密标准(AES)。

一旦获得了一个密码对象,就可以通过设置模式和密钥来对它初始化:

int mode = ...;
Key key = ...;
ciper.init(mode, key);

Cipher的模式有一下几种:

  • ENCRYPT_MODE:整型值1,加密模式,用于Cipher的初始化。
  • DECRYPT_MODE:整型值2,解密模式,用于Cipher的初始化。
  • WRAP_MODE:整型值3,包装密钥模式,用于Cipher的初始化。
  • UNWRAP_MODE:整型值4,解包装密钥模式,用于Cipher的初始化。

然后可以反复调用update方法来对数据块进行加密:

int blockSize = cipher.getBlockSize();
var intBytes = new byte[blockSize];
//read inBytes
...
int outputSize = cipher.getOutputSize(blockSize);
var outBytes = new byte(outputSize);
int outLength = cipher.update(inBytes, 0, outputSize, outBytes);
//write outBytes
...

完成上述操作后,还必须调用一次doFinal方法。如果还有最后一个输入数据块(其字节数小于blockSize),那么就要调用:

outBytes = cipher.doFinal(inBytes, 0, inLength);

如果所有的输入数据都已经加密,则用下面的方法调用来代替:

outBytes = cipher.doFinal();

对doFinal的调用是必需的,因为它会对最后的块进行填充。就拿DES密码来说,它的数据块大小是8字节。假设输入数据最后一耳光数据库少于8字节,当然我们可以将其余的字节全部用0填充,从而得到一个9字节的最终数据块,然后对它进行加密。但是,当对数据块进行解密时,数据块的结尾会附加若干个0字节,因此它与原始输入文件之间会略有不同。

这肯定是个问题,所以常用的填充方案是RSA Security公司在公共密钥密码标准# 5中描述的方案。

在该方案中,最后一个数据块不是全部用填充值0进行填充,而是用扥古填充字节数量的值作为填充值进行填充。换句话说,如果L是最后一个(不完整的)数据块,那么它将按如下方式进行填充:

//if length(L) = 7
L 01    			
//if length(L) = 6
L 02 02
//if length(L) = 5
L 03 03 03
...
//if length(L) = 1
L 07 07 07 07 07 07 07

最后,如果输入的数据长度确实能被8整出,那么就会将下面这个数据块:

08 08 08 08 08 08 08 08

附加到数据块后,并进行加密。在解密时,明文的最后一个字节就是要丢弃的填充字符数。

5.2、密钥生成

为了加密,我们需要生成密钥。每个密码都有不同的用于密钥的格式,我们需要确保密钥的生成是随机的。这需要遵循下面的步骤:

  1. 为加密算法获取KeyGenerator。
  2. 用随机源来初始化密钥发生器。如果密码块的长度是可变的,还需要指定期望的密码块长度。
  3. 调用generateKey方法。

例如,下面是如何生成AES密钥的方法:

KeyGenerator keygen = KeyGenerator.getInstance("AES");
var random = new SecureRandome();
kengen.init(random);
Key key = keygen.generateKey();

或者,可以从一组固定的原生数据(也许是由口令或者随机击键产生的)中生成一个密钥,这时可以使用如下的SecretKeyFactory:

byte[] keyData = ....;//16 bytes for AES
var key = new SecreKeySpec(keyData, "AES");

如果要生成密钥,必须使用“真正的随机”数。例如,在Random类中的常规的随机数发生器。是根据当前的日期和时间来产生随机数的,因此它不够随机。假设计算机时钟可以精确到1/10秒,那么,每天最多存在864000个种子。如果攻击者知道发布密钥的日期(通常可以由消息日期或证书有效日期推算出来),那么就可以很容易地生成那一天所有可能的种子。

SecureRandom类产生的随机数,远比由Random类产生的那些数字安全得多。你仍然需要提供一个种子,以便在一个随机点上开始生成数字序列。要这样做,最好的方法是从一个诸如白噪声发生器之类的硬件设备那里获取输入。另一个合理的随机输入源是请用户在键盘上进行随心所欲的盲打,但是每次敲击键盘只为随机种子提供1位或者2位。一旦你在字节数组中收集到这种随机位后,就可以将它传递给setSeed方法。

var secrand = new SecureRandom();
var b = new byte[20];
//fill with truly random bits
secrand.setSeed(b);

如果没有为随机数发生器提供种子,那么它将通过启动线程,使它们睡眠,然后测量它们被唤醒的准确时间,以此来计算自己的20个字节的种子。

5.3、密码流

JCE库提供了一组使用便捷的流类,用于对流数据进行加密或解密。

例如,下面是对文件数据进行加密的方法:

Cipher cipher = ...;
cipher.init(Cipher.ENCRYPT_MODE, key);
var out = new CipherOutputStream(new FileOutputStream(outputFileName), cipher);
var bytes = new byte[BLOCKSIZE];
int inLength = getData(bytes);
while(inLength != -1) {
	out.write(bytes, 0, inLength);
	inLength = getData(bytes);
}
out.flush();

同样地,可以使用CipherIntStream,对于文件的数据进行读取和解密:

Cipher cipher = ...;
cipher.init(Cipher.DECRYPT_MODE, key);
var in = new CpherInputStream(new FileInputStream(inputFileName), cipher);
var bytes = new byte[BLOCKSIZE];
int intLength = in.read(bytes);
while(inLength != -1) {
	putData(bytes, inLength);
	inLength = in.read(bytes);
}

密码流类能够透明地调用update和doFinal方法,非常方便。

5.4、公共密钥密码

AES密码是一种对称密码,加密和解密都使用相同的密钥。对称密码的致命缺点在于密码的分发。如果Alice给Bob发送了一个加密的方法,那么Bob需要使用与Alice相同的密钥。如果Alice修改了密钥,那么她必须在给Bob发送信息的同时,还要通过安全信道发送新的密钥,但是也许她并没有到达Bob的安全信道,这也正是她必须对她发送给Bob的信息进行加密的原因。

公共密钥密码技术解决了这个问题。在公共密钥密码中,Bob拥有一个密钥对,包括个公共密钥和一个相匹配的私有密钥。Bob可以在任何地方发布公共密钥,但是他必须严格
保守他的私有密钥。Alice只需要使用公共密钥对她发送给Bob的信息进行加密即可。

实际上,加密过程并没有那么简单。所有已知的公共密钥算法的操作速度都比对称密钥算法(比如DES或AES等)慢得多,使用公共密钥算法对大量的信息进行加密是不切实际的。但是,如果像下面这样,将公共密钥密码与快速的对称密码结合起来,这个问题就可以得到解决:

  1. Alice生成一个随机对称加密密钥,她用该密钥对明文进行加密。
  2. ALice用Bob的公共密钥给对称密钥进行加密
  3. Alice将加密后的对称密钥和加密后的明文同时发给Bob
  4. Bob用他的私有密钥给对称密钥解密
  5. Bob用解密后的对称密钥给信息解密

除了Bob之外,其他人无法给对称密钥进行解密,因为只有Bob拥有解密的私有密钥。这样,昂贵的公共密钥加密技术就可以只应用于少量的关键数据的加密。

最常见的公共密钥算法是Rivest、Shamir和Adleman发明的RSA算法。

如果要使用RSA算法,就需要一对公共/私有密钥。你可以按如下方法使用Key-Pair-Generator来获得:

KeyPirGenerator pairgen = KeyPairGenerator.getInstance("RSA");
var random = new SecureRandom();
pairgen.initialize(KEYSIZE, random);
KeyPair keyPair = pairgen.generateKeyPair();
Key publicKey = keyPair.getPublic();
key privateKey = keyPair.getPrivate();

5.5、常用方法

javax.crypto.Cipher:

  • static Cipher getInstance(String algorithmName)
  • static Cipher getInstance(String algorithmName, String providerName):返回实现了指定加密算法的Cipher对象。如果未提供该算法,则抛出一个NoSuchAlgorithmException异常。
  • int getBlockSize():返回密码块的大小,如果该密码不是一个分组密码,则返回0.
  • int getOutputSize(int inputLength):如果下一个输入数据块拥有给定的字节数,则返回所需的输出缓冲区的大小。本方法的运行要考虑到密码对象中所有已缓冲的字节数量。
  • void init(int mode, Key key):对加密算法对象初始化。Mode是ENCRYPT_MODE,DECRYPT_MODE,WRAP_MODE,UNWRAPMODE之一。
  • byte[] update(byte[] in)
  • byte[] update(byte[] in, int offest, int length)
  • int update(byte[] in, int offset, int length, byte[] out):对输入数据块进行转换。前两个方法返回输出,第三个方法返回放入out的字节数。
  • byte[] doFinal()
  • byte[] doFinal(byte[] in)
  • byte[] doFinal(byte[] in, int offset, int length)
  • int doFinal(byte[] in, int offset, int length, byte[] out):转换输入的最后一个数据块,并刷新该加密算法对象的缓冲。前三个方法返回输出,第四个方法返回放入out的字节数。

javax.crypto.KeyGenerator:

  • static KeyGenerator getInstance(String algorithmName):返回实现指定加密算法的KeyGenerator对象。如果未提供该加密算法,则抛出一个NoSuchAlgorithmException异常。
  • void init(SecureRandom random)
  • void init(int keySize, SecureRandom random):对密钥生成器进行初始化。
  • SecreKey generateKey():生成一个新的密钥。

javax.crypto.spec.SecreKeySpec:

  • ScretKeySpec(byte[] key, String algorithmName):创建一个密钥描述规格说明。

javax.crypto.CipherInputStream:

  • CipherInputStream(InputStream in, Cipher cipher):构建一个输入流,以读取in中的书,并且使用指定的密码对数据进行解密和加密。
  • int read()
  • int read(byte[] b, int off, int len):读取输入流中的数据,该数据会被自动解密和加密。

javax.crypto.CipherOutputStream:

  • CipherOutputStream(OutputStream out, Cipher cipher):构建一个输出流,以便将数据写入out,并且使用指定的密码对数据进行加密的解密。
  • void write(int ch)
  • void wirte(byte[] b, int off, int len):将数据写入输出流,该数据会被自动加密和解密。
  • void flush():刷新密码缓冲区,如果需要的话,执行填充操作。

5.6、示例

5.6.1、对称密码加密
//生成密钥,保存在secret.key中
java AESTtest -genkey secret.key

//加密
java AESTest -encrypt plaintextFile encryptedFile secret.key

//解密
java AESTest -decrypt encrypTedFile decryptedFile secret.key
public class AESTest {
    public static void main(String[] args) throws Exception {
        if (args[0].equals("-genkey")) {
            KeyGenerator keygen = KeyGenerator.getInstance("AES");
            SecureRandom secureRandom = new SecureRandom();
            keygen.init(secureRandom);
            SecretKey key = keygen.generateKey();
            try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(args[1]))) {
                out.writeObject(key);
            }
        } else{
            int mode;
            if (args[0].equals("-encrypt")) {
                mode = Cipher.ENCRYPT_MODE;
            } else {
                mode = Cipher.DECRYPT_MODE;
            }

            try (ObjectInputStream keyIn = new ObjectInputStream(new FileInputStream(args[3]));
                 FileInputStream in = new FileInputStream(args[1]);
                 FileOutputStream out = new FileOutputStream(args[2])
                ) {
                Key key = (Key) keyIn.readObject();
                Cipher cipher = Cipher.getInstance("AES");
                cipher.init(mode, key);
                Util.crypt(in, out, cipher);
            }
        }
    }
}

class Util{
    public static void crypt(InputStream in, OutputStream out, Cipher cipher) throws Exception {
        int blockSize = cipher.getBlockSize();
        int outputSize = cipher.getOutputSize(blockSize);
        byte[] inBytes = new byte[blockSize];
        byte[] outBytes = new byte[outputSize];

        int inLength = 0;
        boolean done = false;
        while (!done) {
            inLength = in.read(inBytes);
            if (inLength == blockSize) {
                int outLength = cipher.update(inBytes, 0, blockSize, outBytes);
                out.write(outBytes, 0, outLength);
            } else {
                done = true;
            }
        }
        if (inLength > 0) {
            outBytes = cipher.doFinal(inBytes, 0, inLength);
        } else {
            outBytes = cipher.doFinal();
        }
        out.write(outBytes);
    }
}
5.6.2、对称密码加密(密码流)

使用方式和上面一致:

public class AESStreamTest {

    //数据块大小
    public static final int BLOCK_SIZE = 1024;

    public static void main(String[] args) throws Exception {
        if (args[0].equals("-genkey")) {
            KeyGenerator keygen = KeyGenerator.getInstance("AES");
            SecureRandom secureRandom = new SecureRandom();
            keygen.init(secureRandom);
            SecretKey key = keygen.generateKey();
            try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(args[1]))) {
                out.writeObject(key);
            }
        } else if (args[0].equals("-encrypt")) {
            Cipher cipher = Cipher.getInstance("AES");
            //读取密钥文件
            ObjectInputStream keyIn = new ObjectInputStream(new FileInputStream(args[3]));
            Key key = (Key) keyIn.readObject();
            cipher.init(Cipher.DECRYPT_MODE, key);
            keyIn.close();
            try (FileInputStream in = new FileInputStream(args[1]);
                 CipherOutputStream out = new CipherOutputStream(new FileOutputStream(args[2]), cipher)
            ) {
                byte[] bytes = new byte[BLOCK_SIZE];
                int inLength = in.read(bytes);
                while (-1 != inLength) {
                    out.write(bytes);
                    inLength = in.read(bytes);
                }
                out.flush();
            }
        } else if (args[0].equals("-decrypt")) {
            Cipher cipher = Cipher.getInstance("AES");
            //读取密钥文件
            ObjectInputStream keyIn = new ObjectInputStream(new FileInputStream(args[3]));
            Key key = (Key) keyIn.readObject();
            cipher.init(Cipher.DECRYPT_MODE, key);
            keyIn.close();
            try (OutputStream out = new FileOutputStream(args[2]);
                 CipherInputStream in = new CipherInputStream(new FileInputStream(args[1]), cipher)
            ) {
                byte[] bytes = new byte[BLOCK_SIZE];
                int inLength = in.read(bytes);
                while (-1 != inLength) {
                    out.write(bytes);
                    inLength = in.read(bytes);
                }
                out.flush();
            }
        } else {
            throw new Exception("unknow args...");
        }
    }
}
5.6.3、公共密钥加密
//生成RSA密钥对
java RSATest -genkey public.key private.key

//加密
java RSATest -encrypt content.txt rsaContent.txt public.key

//解密
-decrypt rsaContent.txt newcontent.txt private.key
public class RSATest {

    private static final int KEY_SIZE = 512;

    public static void main(String[] args) throws Exception{
        if (args[0].equals("-genkey")) {
            //生成RSA密钥对
            KeyPairGenerator pairgen = KeyPairGenerator.getInstance("RSA");
            SecureRandom random = new SecureRandom();
            pairgen.initialize(KEY_SIZE, random);
            KeyPair keyPair = pairgen.generateKeyPair();
            //写入文件
            try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(args[1]))) {
                out.writeObject(keyPair.getPublic());
            }
            try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(args[2]))) {
                out.writeObject(keyPair.getPrivate());
            }
        } else if (args[0].equals("-encrypt")) {
            //生成AES对称密钥
            KeyGenerator keygen = KeyGenerator.getInstance("AES");
            SecureRandom random = new SecureRandom();
            keygen.init(random);
            SecretKey key = keygen.generateKey();
            //wrap with RSA public key
            try(ObjectInputStream keyIn = new ObjectInputStream(new FileInputStream(args[3]));
                DataOutputStream out = new DataOutputStream(new FileOutputStream(args[2]));
                FileInputStream in = new FileInputStream(args[1])
            ) {
                //读取RSA公钥
                Key publicKey = (Key) keyIn.readObject();
                //使用RSA公钥给AES密钥加密
                Cipher cipher = Cipher.getInstance("RSA");
                cipher.init(Cipher.WRAP_MODE, publicKey);
                byte[] wrappedKey = cipher.wrap(key);
                //写入文件头部
                out.writeInt(wrappedKey.length);
                out.write(wrappedKey);
                //使用AES密钥对明文加密
                cipher = Cipher.getInstance("AES");
                cipher.init(Cipher.ENCRYPT_MODE,key);
                Util.crypt(in, out, cipher);
            }
        } else if (args[0].equals("-decrypt")) {
            try(DataInputStream in = new DataInputStream(new FileInputStream(args[1]));
                ObjectInputStream keyIn = new ObjectInputStream(new FileInputStream(args[3]));
                FileOutputStream out = new FileOutputStream(args[2])
            ) {
                //从加密文件头部读取被RSA加密后的AES密钥
                int length = in.readInt();
                byte[] wrappedKey = new byte[length];
                in.read(wrappedKey, 0, length);

                //unwrap with RSA private key
                //从文件读取RSA私钥
                Key privateKey = (Key)keyIn.readObject();
                Cipher cipher = Cipher.getInstance("RSA");
                //用RSA私钥解密AES密钥
                cipher.init(Cipher.UNWRAP_MODE, privateKey);
                Key key = cipher.unwrap(wrappedKey, "AES", Cipher.SECRET_KEY);
                //使用解密后的AES密钥解密铭文
                cipher = Cipher.getInstance("AES");
                cipher.init(Cipher.DECRYPT_MODE, key);
                Util.crypt(in, out, cipher);
            }
        } else {
            throw new Exception("unknow args...");
        }
    }
}
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Java反射机制是指在运行时动态地获取一个类的信息,并可以操作类的属性、方法和构造器等。Java反射机制可以使程序员在运行时动态地调用类的方法和属性,扩展类的功能,并可以实现注解、工厂模式以及框架开发等。 Java反射机制的原理如下:首先,Java编译器将Java源代码编译为字节码文件,字节码文件中包含着类的信息,这些信息包括类的名称、方法、属性和构造器等等。接着,Java虚拟机将字节码文件加载到内存中,然后通过类加载器将类加载到内存中形成一个类对象,这个类对象可以操作字节码文件中的信息。 使用Java反射机制的过程如下:首先获取类对象,通过类对象来获取类的构造器、属性、方法等信息,然后调用构造器来创建对象,通过属性获取和设置类的成员属性,通过方法调用类的方法等。 Java反射机制的优点是可以在运行时动态地得到类的信息,使得程序员在程序运行时能够对类进行更加灵活的操作,并可以使得程序更加通用化,同时也存在着一定的性能问题,因为Java反射机制需要Java虚拟机进行一定的额外处理,所以在程序运行时需要进行额外的时间和资源消耗。 总之,Java反射机制Java语言的一项重要特性,在Java开发中广泛应用,在代码编写、框架开发以及API开发中具有重要作用。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值