侧边栏壁纸
  • 累计撰写 57 篇文章
  • 累计创建 10 个标签
  • 累计收到 2 条评论

目 录CONTENT

文章目录

9、安全

yilee
2023-04-04 / 0 评论 / 0 点赞 / 102 阅读 / 0 字 / 正在检测是否收录...
温馨提示:
本文最后更新于2024-05-31,若内容或图片失效,请留言反馈。 部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

九、安全

9.1 类加载器

  • 虚拟机代码存储在以 .class 为扩展名的类文件中,每个类文件都包含某个类或者接口的定义和实现代码。这些类文件必须由一个程序进行解释,该程序能够将虚拟机的指令集翻译成目标机器的机器语言。
  1. 类加载过程

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

      1. 虚拟机有一个用于加载类文件的机制;它使用该机制来加载 MyProgram 类文件中的内容。
      2. 如果 MyProgram 类拥有类型为另一个类的域,或者是拥有超类,那么这些类文件也会被加载。(加载某个类所依赖的所有类的过程称为类的解析。)
      3. 接着,虚拟机执行 MyProgram 中的main 方法(它是静态的,无需创建类的实例) 。
      4. 如果 main 方法或者main 调用的方法要用到更多的类,那么接下来就会加载这些类。
    • 类加载机制并非只使用单个的类加载器。每个Java 程序至少拥有三个类加载器,分别是

      • 引导类加载器

      • 扩展类加载器

      • 系统类加载器(有时也称为应用类加载器)

    • 引导类加载器负责加载系统类(通常从 JAR 文件rt.jar 中进行加载) 。它是虚拟机不可分割的一部分,而且通常是用C 语言来实现的。引导类加载器没有对应的 Classloader 对象。

      String.class.getClassLoader()
      // 上述方法将返回null 。
      
    • 扩展类加载器用于从 jre/lib/ext 目录加载“标准的扩展” 。可以将JAR 文件放入该目录,这样即使没有任何类路径,扩展类加载器也可以找到其中的各个类。

    • 系统类加载器用于加载应用类。它在由 CLASSPATH 环境变量或者 -classpath 命令行选项设置的类路径中的目录里或者是 JAR/ZIP 文件里查找这些类。

  2. 类加载器的层次结构

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

      例子:当要求系统类加载器加载一个系统类(比如,java.util.Arraylist) 时,它首先要求扩展类加载器进行加载,该扩展类加载器则首先要求引导类加载器进行加载。

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

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

      因为在 URLClassLoader 构造器中没有指定父类加载器,因此 pluginloader 的父亲就是系统类加载器。层次结构如下:

      5a5caa4adeb84adfb9a7f9718a0fdf28

    • 类是由于其他的类需要它而被加载的,而这个过程是透明的。每个线程都有一个对类加载器的引用,称为上下文类加载器。主线程的上下文类加载器是系统类加载器。当新线程创建时,它的上下文类加载器会被设置成为创建该线程的上下文类加载器。因此,如果你不做任何特殊的操作,那么所有线程就都会将它们的上下文类加载器设置为系统类加载器。

      • 我们也可以通过下面的调用将其设置成为任何类加载器。

        Thread t = Thread.currentThread();
        t.setContextClassLoader(loader);
        
      • 助手方法可以获取这个上下文类加载器

        Thread t = Thread.currentThread();
        ClassLoader loader = t.getContextClassLoader();
        Class cl = loader.loadClass(className);
        

      如果你编写了一个按名字来加栽类的方法,那么让调用者在传递显式的类加栽器和使用上下文类加栽器之间进行选择就是一种好的做法。不要直接使用该方法所属的类的类加栽器。

  3. 将类加载器作为命名空间

    • 包的命名是为了消除名字冲突。在标准类库中,有两个名为Date 的类,它们的实际名字分别为 java.util.Datejava.sql.Date 。在一个正在执行的程序中,所有的类名都包含它们的包名。

    • 在同一个虚拟机中,可以有两个类,它们的类名和包名都是相同的。类是由它的全名和类加载器
      来确定的。这项技术在加载来自多处的代码时很有用。例如:浏览器为每一个Web页都使用了一个独立的applet 类加载器类的实例。虚拟机就能区分来自不同Web 页的各个类,而不用管它们的名字是什么。

      cc7f1eb565fd43c78ba677df0a909cb7

  4. 编写你自己的类加载器

    • 编写自己的类加载器,只需要继承 Classloader 类,覆盖 findClass(String className) 类即可。要实现该方法,必须做到以下几点:

      • 为来自本地文件系统或者其他来源的类加载其字节码。
      • 调用Classloader 超类的 defineClass方法,向虚拟机提供字节码。
    • Classloader 超类的 loadClass 方法用千将类的加载操作委托给其父类加载器去进行,只有当该类尚未加载并且父类加载器也无法加载该类时,才调用 findClass 方法。

      public class MyClassLoader extends ClassLoader {
          static ArrayList<String> paths = new ArrayList<>();
      
          static {
              // 重固定的路径
              paths.add("C:\\Users\\Galaxy\\Desktop\\");
          }
      
          @Override
          protected Class<?> findClass(String name) throws ClassNotFoundException {
              // 拼接路径
              File file = null;
              String namePath = name.replaceAll("\\.", "/") + ".class";
              for (String path : paths) {
                  file = Paths.get(path, namePath).toFile();
                  if (file.exists()) {
                      break;
                  }
              }
              if (file == null || !file.exists()) {
                  throw new ClassNotFoundException("类不存在");
              }
      
      
              try (InputStream in = new FileInputStream(file.getAbsolutePath())) {
                  // 一般这个大小够了,这里是为了方便读取
                  // 文件的内容,所以一次性读入,并且将大小返回
                  // 用于defineClass
                  byte[] b = new byte[1024 * 1024 * 10];
                  int classSize = in.read(b);
                  return this.defineClass(name, b, 0, classSize);
              } catch (IOException ignored) {
              }
              throw new ClassNotFoundException("类加载错误");
          }
      
          public static void main(String[] args) throws ClassNotFoundException {
              Class<?> cls = new MyClassLoader().loadClass("top.liheji.test0.Point");
              System.out.println(cls);
              // 项目中不能存在top.liheji.test0.Point 类,否则会使用AppLoader加载
              System.out.println(cls.getClassLoader());
          }
      }
      
  5. 字节码校验

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

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

      校验器总是在防范被故意篡改的类文件,而不仅仅只是检查编译器产生的类文件。

9.2 安全管理器与访问权限

  1. 权限检查

    • 安全管理器是一个负责控制具体操作是否允许执行的类。运行的Java 应用程序默认的设置是不安装安全管理器的,这样所有的操作都是允许的。安全管理器负责检查的操作包括以下内容:

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

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

  2. Java 平台安全性

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

      1931bf035e90424cbf6f9ebb950916db

      • 代码来源( code source) 是由一个代码位置和一个证书集指定的。代码位置指定了代码的来源。证书的目的是要由某一方来保障代码没有被篡改过。

      • 权限( permissi on ) 是指由安全管理器负责检查的任何属性。Java 平台支待许多访问权限类,每个类都封装了特定权限的详细信息。Policy 类的默认实现可从访问权限文件中读取权限。例如,实例new FilePermission("/tmp/*", "read,write"); 表示:允许在/tmp 目录下读取和写入任何文件。权限层次结构如下。

        8b414dd5788b41cfa2df91f2b0f824de

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

      为什么在调用堆栈上的所有方法都必须允许某个特定的操作呢?

      • 一个applet 的init 方法想要打开一个文件,它可能会调用下面的语句:

        Reader in= new FileReader(name);
        
      • FileReader 构造器调用FilelnputStream 构造器,而FilelnputStream 构造器调用安全管理器的checkRead 方法,安全管理器最后用 FilePermission(name , “read”) 对象调用 checkPermission。调用堆栈如下。

        c99c83a4b25346c9a3b579e9518e62b3

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

  3. 安全策略文件

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

      // 该文件给所有下载自http://www.horstmann.com/classes 的代码授予在/tmp目录下读取和写入文件的权限。
      grant codeBase "http://www.horstmann.com/classes" {
          permission java.io.FilePermission "/tmp/*", "read,write";
      }
      
    • 可以将策略文件安装在标准位置上。默认情况下,有两个位置可以安装策略文件:

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

      ​ 可以在java . security 配置文件中修改这些文件的位置,默认位置设定为:

      policy.url.1=file:${java.home}/lib/security/java.policy
      policy.url.2=file:${user.home}/.java.policy
      

      ​ 系统管理员可以修改 java.security 文件,并可以指定驻留在另外一台服务器上并且用户无法修改的策略URL 。策略文件中允许存放任何数量的策略URL (这些URL 带有连续的编号) 。所有文件的权限都被组合了在一起。

      ​ 如果你想将策略文件存储到文件系统之外,那么可以去实现Policy 类的一个子类,让其去收集所允许的权限。然后在 java.security 配置文件中更改下面这行:

      policy.provider=sun.security.provider.PolicyFile
      
    • 一个策略文件包含一系列grant 项。每一项都具有以下的形式:

      grant codesource {
          permisszon1;
      	permission2;
          ...
      }
      

      codesource 代码来源包含一个代码基和值得信赖的用户特征( principal ) 与证书签名者的名字

      • 代码基可以设定为 code Base “url”
      • URL 以“/"结束,那么它是一个目录。否则,它将被视为一个JAR 文件的名字
      • 代码基是一个URL 并且总是以斜杠作为文件分隔符,即使是Windows 中的文件URL,也是如此。

      策略文件阅读器接受两种格式的file URL,即即 file://localFilefile:localFile 。 Windows 驱动器名前面的斜杠是可有可无的。

      权限采用下面的结构:

      permission className targetName, actionList;
      
      • 类名是权限类的全称类名(比如java.io.FilePermission ) 。
      • 目标名是个与权限相关的值,例如,文件权限中的目录名或者文件名,或者是socket 权限中的主机和端口。
      • 操作列表同样是与权限相关的,它是一个操作方式的列表,比如read 或connect 等操作,逗号分隔。标准的权限和它们执行的操作如下图。大部分权限只允许执行某种特定的行为。可以将这些行为视为带有一个隐含操作“permit" 的目标。这些权限类都继承自BasicPermission 类

      有些权限类并不需要目标名和操作列表。

      e57898ccae4c4bf69d4dad52c67638f5

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

      - -
      file 文件
      directory/ 目录
      directory/* 目录中的所有文件
      * 当前目录中的所有文件
      directory/- 目录和其子目录中的所有文件
      - 当然目录和其子目录中所有文件
      <> 文件系统中的所有文件
      // 赋予对/myapp 目录和它的子目录中的所有文件的访问权限。
      permission java.io.FilePermission "/myapp/-", "read,write,delete";
      // 使用 \\ 转义字符序列来表示Window 文件名中的反斜杠。
      permission java.io.FilePermission "c:\\myap\\-", "read,write,delete";
      
    • Socket 权限的目标由主机和端口范围组成。对主机的描述具有下面几种形式:

      - -
      hostname 或IP address 单个主机
      local host 或空字符串 本地主机
      •.domainSuffix 以给定后缀结尾的域中所有的主机
      * 所有主机

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

      - -
      :n 单个端口
      :n- 编号大于等于n 的所有端口
      :-n 编号小于等于n 的所有端口
      :nl-n2 位于给定范围内的所有端口
      permission java.net.SocketPermission "*.horstmann.com:8000-8999","connect";
      
    • 属性权限的目标可以采用下面两种形式之一:

      - -
      property 一个具体的屈性
      propertyPrefix. * 带有给定前缀的所有属性vfd
      // 允许程序读取以java.vm 开头的所有属性。
      permission java.util.PropertyPermission "java.vm.*", "read";
      
    • 可以在策略文件中使用系统属性,其中的${property} 标记会被属性值替代,例如,${user.home} 会被用户主目录替代。为了创建平台无关的策略文件,使用 file.separator 属性而不是使用显式的 / 或
      者\\分隔符绝对是个好主意。

      // \${property}
      permission java.io.FilePermission "${user.home}", "read,write";
      // 平台无关
      permission java.io.FilePermission "${user.home}${/}-", "read,write";
      
  4. 定制权限

    • 把自己的权限类提供给用户,以使得他们可以在策略文件中引用这些权限类。要实现自己的权限类,可以继承Permission 类,并提供以下方法:

      • 带有两个String 参数的构造器,这两个参数分别是目标和操作列表
      • String getAct ions()
      • boolean equals(Object other)
      • int hashCode()
      • boolean implies(Permission other)
    • 权限有一个排序,其中更加泛化的权限隐含了更加具体的权限。

      // 允许读写 /tmp 目录以及子目录中的任何文件。
      pl = new FilePermission("/tmp/-", "read,write");
      // 该权限隐含了其他更加具体的权限如下
      p2 = new FilePermission("/tmp/-", "read");
      p3 = new FilePermission("/tmp/aFile", "read,write");
      p4 = new FilePermission("/tmp/aDirectory/-", "write");
      // 即实现权限后需要实现implies方法,查询其隐含的权限并检查
      
  5. 实现权限类(P426)

9.3 用户认证

  1. JAAS 框架(Java 认证和授权服务)

    • “认证” 部分主要负责确定程序使用者的身份,而“授权”将各个用户映射到相应的权限。一旦用户通过认证,就可以为其附加一组权限。

      grant principal com.sun.security.auth.UnixPrincipal "harry" {
      	permission java.util.PropertyPermission "user.*", "read";
      	...
      }
      //  com.sun.security.auth.UnixPrincipal 类检查运行该程序的UNIX用户的名字,它的getName 方法将返回UNIX登录名,然后我们就可以检查该名称是否等于" harry " 。
      
    • 可以使用一个LoginContext 以使得安全管理器能够检查这样的授权语句。下面是登录代码的基本轮廓:

      try{
          System.setSecurityManager(new SecurityManager());
          // defined in JAAS configu rati on file
          LoginContext context = new LoginContext("Login1");  
          // "Login1" 是指JAAS 配置文件中具有相同名字的项。
          context.login() ;
          // get the authenticated Subject
          Subject subject = context.getSubject();
          // subject 是指已经被认证的个体。
          ...
          context.logout() ;
      } catch (LoginException exception) { // th rown if 1 ogi n was not successfu 1
      	exception.printStackTrace();
      }
      
    • 配置文件

      Login1 {
          com.sun.security.auth.module.UnixloginModule required;
          com.whizzbang.auth.module.RetinaScanModule sufficient;
      };
      
      Login2 {
          ...
      };
      
    • JDK 在com.sun.security.auth.module 包中包含以下模块:UnixloginModule、NTLoginModule、KrbSLoginModule、JndiLoginModule、KeyStoreloginModule。一个登录策略由一个登录模块序列组成, 每个模块被标记为required 、sufficient , requisite 或optional。登录算法如下:

      1. 各个模块依次执行, 直到有一个sufficient 的模块认证成功,或者有一个requisite 的模块认证失败,或者已经执行到最后一个模块时才停止。
      2. 当标记为required 和requisite 的所有模块都认证成功,或者它们都没有被执行,但至少有一个sufficient 或optional 的模块认证成功时,这次认证就成功了。
    • 登录时要对登录的主体(subject) 进行认证,该主体可以拥有多个特征(principal) 。特征描述了主体的某些属性,比如用户名、组ID 或角色等。

    • 使用grant 语句可以对一个特征进行测试格式如下,当用户登录后,就会在独立的访问控制上下文中,运行要求检查用户特征的代码。使用静态的doAs 或doAsPrivileged 方法,启动一个新的PrivilegedAction, 其run 方法就会执行这段代码。

      • doAs 方法开始千当前的访问控制
      • oAsPrivileged 方法则开始于一个新的上下文。允许将登录代码和“业务逻辑"的权限相分离。
      grant principalClass "principalName"
      // grant com.sun.security.auth.UnixPrincipal "harry"
      
  2. JAAS 登录模块(P437)

9.4 数字签名

主要介绍消息摘要是如何检测数据文件中的变化的,以及数字签名是如何证明签名者的身份的。

  1. 消息摘要

    • 消息摘要(message digest) 是数据块的数字指纹。消息摘要具有两个基本属性:

      • 如果数据的1 位或者几位改变了,那么消息摘要也将改变。
      • 拥有给定消息的伪造者不能创建与原消息具有相同摘要的假消息。
    • Java 编程语言已经实现了MD5 、SHA-I, SHA-256 , SHA-3 84 和SHA-512 。MessageDigest类是用千创建封装了指纹算法的对象的“工厂" , 它的静态方法get Instance 返回继承了MessageDigest 类的某个类的对象。MessageDigest 类能够承担下面的双重职责:在获取MessageDigest 对象之后,可以通过反复调用update 方法,将信息中的所有字节提供给该对象。

      • 作为一个工厂类。
      • 作为所有消息摘要算法的超类。
    • 使用流程

      • 获取一个能够计算SHA 指纹的对象的方法

        MessageDigest alg = MessageDigest.getInstance("SHA-1");
        
      • 将文件中的所有字节传给上面创建的alg 对象,以执行指纹算法:

        InputStream in = ...
        int ch;
        while ((ch = in.read()) != -1)
            alg.update((byte) ch);
        
      • 调用digest 方法返回消息摘要(该方法按照指纹算法的要求补齐输入,并且进行相应的计算,然后以字节数组的形式返回消息摘要)。

        byte[] hash = alg.digest();
        
  2. 消息签名

    • 如果消息和指纹同时被截获了,对消息进行修改, 再重新计算指纹,就是一件很容易的事情。数字签名解决了这个问题。

    • 公共密钥加密技术是基千公共密钥和私有密钥这两个基本概念的。它的设计思想是你可以将公共密钥告诉世界上的任何人,但是,只有自己才待有私有密钥,重要的是你要保护你的私有密钥,不将它泄漏给其他任何人。

    • 使用DSA 进行公共密钥签名的交换

      4638601d24bd410da5b008f2ef2dd998

  3. 校验签名

    • JDK 配有一个keytool 程序,该程序是一个命令行工具,用千生成和管理一组证书。

    • 校验流程(Alice 是如何对一个文档进行签名并且将它发送给Bob 的,而Bob 又是如何校验该文档确实是由Alice 签名,而不是冒名顶替的)。

      • Alice 创建一个密钥库 alice.certs 并且用别名生成一个密钥对。keytool 工具使用X. 500 格式的名字,它包含常用名( CN)、机构单位( OU) 、机构( O ) 、地点(L)、州(ST) 和国别( C ) 等成分,以确定密钥持有者和证书发行者的身份。最后,必须设定一个密钥口令,或者按回车键,将密钥库口令作为密钥口令来使用。

        keytool -genkeypair -keystore alice.certs -alias alice
        # 输入相关信息
        
      • Alice导出一个证书文件, 把她的公共密钥提供给Bob。

        # 导出一个证书文件
        keytool -exportcert -keystore alice.certs -alias alice -file alice.cer
        # Alice 把证书发送给Bob
        keytool -printcert -file alice.cer
        
      • 现在 Alice 就可以给Bob 发送签过名的文档了。jarsigner 工具负责对JAR 文件进行签名和校验, Alice 只需要将文档添加到要签名的 JAR 文件中。使用jarsigner 工具将签名添加到文件

        jar cvf document.jar document.txt
        # 将签名添加到文件
        jarsigner -keystore alice.certs document.jar alice
        
      • 当Bob 收到JAR 文件时,他可以使用j arsigner 程序的 -verify 选项,对文件进行校验。Bob 不需要设定密钥别名。jarsigner 程序会在数字签名中找到密钥所有者的X.500 名字,并在密钥库中搜寻匹配的证书。

        jarsigner -verify -keystore bob.certs document.jar
        
      • 如果JAR 文件没有受到破坏而且签名匹配,那么jarsigner 程序将打印:jar verified. 。否则,程序将显示一个出错消息。

  4. 认证问题

    • 任何人都可以生成一对公共密钥和私有密钥,再用私有密钥对消息进行签名,然后把签名好的消息和公共密钥发送给你。这种确定发送者身份的问题称为"认证问题” 。

    • 解决方法(1):通过一个值得信赖的中间人进行认证

      8584297ebfde443e955492cf4522af71

    • 解决方法(2):通过受信赖的中间人的签名进行认证

      979ce69cee0042eb8545f79d3962a850

  5. 证书签名(P454)

  6. 证书请求(P454)

  7. 代码签名(P455)

9.5 加密

  1. 对称密码

    • "Java 密码扩展“包含了一个Cipher 类,该类是所有加密算法的超类。调用Cipher.getinstance(algorithName, providerName); 可以获得一个密码对象

      • 算法名称是一个字符串,比如“ AES" 或者“ DES/CBC/PKCSSPadding ” 。
      • JDK 中是由名为“SunJCE" 的提供商提供密码的,如果没有指定其他提供商,则会默认为该提供商。如果要使用特定的算法,而对该算法Oracle 公司没有提供支持,那么也可以指定其他的提供商。
    • DES, 即数据加密标准,是一个密钥长度为56 位的古老的分组密码。DES 加密算法在现在看来已经是过时了,因为可以用穷举法将它破译。更好的选择是采用它的后续版本,即高级加密标准(AES)。

    • 使用方法

      • 获得一个密码对象

        Cipher cipher = Cipher.getInstance(algorithName);
        // 或者
        Cipher cipher = Cipher.getInstance(algorithName, providerName);
        
      • 设置模式和密钥来对它初始化。

        int mode =
        Key key = ...;
        cipher.init(mode, key);
        // 模式为枚举类型
        Cipher.ENCRYPT_MODE
        Cipher.DECRYPT_MODE
        Cipher.WRAP_MODE
        Cipher.UNWRAP_MODE
        
      • 反复调用update 方法来对数据块进行加密。

        int blockSize = cipher.getBlockSize();
        byte[] inBytes = new byte[blockSize];
        ... // read inBytes
        int outputSize= cipher.getOutputSize(blockSize);
        byte[] outBytes = new byte[outputSize];
        int outLength = cipher.update(inBytes, 0, outputSize, outBytes);
        ... // write outBytes
        
      • 完成上述操作后,还必须调用一次doFinal 方法。对doFinal 的调用是必需的,因为它会对最后的块进行“填充" 。

        // 如果还有最后一个输入数据块(其字节数小于blockSize ) ,那么就要调用:
        outBytes = cipher.doFinal(inBytes, 0, inlength);
        // 如果所有的输入数据都已经加密,则用下面的方法调用来代替
        outBytes = cipher.doFinal();
        
  2. 密钥生成

    • 每个密码都有不同的用于密钥的格式,我们需要确保密钥的生成是随机的。

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

      KeyGenerator keygen = KeyGenerator.getInstance("AES");
      SecureRandom random = new SecureRandom(); // see below
      keygen.init(random);
      Key key = keygen.generateKey();
      // 从一组固定的原生数据中生成一个密钥
      byte[] keyData = ... ; // 16 bytes for AES
      SecretKey key = new SecretKeySpec(keyData, "AES");
      

      SecureRandom 类产生的随机数,远比由Random 类产生的那些数字安全得多。

    • 一旦你在字节数组中收集到这种随机位后,就可以将它传递给setSeed 方法。

      SecureRandom secrand = new SecureRandom();
      byte[] b = new byte (20] ;
      // fill wi th truly random bi ts
      secrand.setSeed(b);
      
  3. 密码流

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

    • 文件数据进行加密

      Cipher cipher =.. . ;
      cipher.init(Cipher.ENCRYPT_MODE, key);
      CipherOutputStream out = new CipherOutputStream(new FileOutputStream(outputFileName), cipher);
      byte[] bytes = new byte [BLOCKSIZE] ;
      int inLength = getData(bytes);  // get data from data source
      while (inLength != -1) {
          out.write (bytes, 0, inLength) ;
          inLength = getData(bytes); // get more data from data source
      }
      out. flush();
      
    • 使用CipherlnputStream , 对文件的数据进行读取和解密:

      Cipher cipher = ...;
      cipher.init(Cipher.ENCRYPT_MODE, key);
      CipherInputStream in = new CipherInputStream(new FileInputStream(inputFileName), cipher);
      byte[] bytes = new byte[BLOCKSIZE];
      int inLength = in.read(bytes);
      while (inLength != -1) {
          putData(bytes, inLength) ;  // put data to destination
          inLength = in.read(bytes);
      }
      
  4. 公共密钥密码

    • 所有已知的公共密钥算法的操作速度都比对称密钥算法(比如DES 或AES 等)慢得多,使用公共密钥算法对大量的信息进行加密是不切实际的。

    • 将公共密钥密码与快速的对称密码结合起来,如下:

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

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

    • 最常见的公共密钥算法是Rivest 、Shamir 和Adleman 发明的RSA 算法。使用RSA 算法,就需要一对公共/私有密钥。

      // 使用Key-PairGenerator 来获得公共/私有密钥
      KeyPairGenerator pairgen = KeyPairGenerator.getInstance("RSA");
      SecureRandom random = new SecureRandom();
      pairgen.initialize(KEYSIZE, random);
      KeyPair keyPair = pairgen.generateKeyPair();
      Key publicKey = keyPair.getPublic();
      Key privateKey = keyPair.getPrivate();
      

API注释

9.1 .3 将类加载器作为命名空间

java.lang.Class 1.0

ClassLoader getClassLoader()
// 获取加载该类的类加载器。

java.lang.ClassLoader 1.0

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

java.net.URLClassLoader 1.2

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

java.lang.Thread 1.0

ClassLoader getContextClassLoader() 1.2
// 获取类加载器,该线程的创建者将其指定为执行该线程时最适合使用的类加载器。
void setContextClassLoader(ClassLoader loader) 1.2
// 为该线程中的代码设置一个类加载器,以获取要加载的类。如果在启动一个线程时没有显式地设置上下文类加载器,则使用父线程的上下文类加载器。

9.2.2 Java 平台安全性

java.lang.SecurityManager 1.0

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

java.lang.Class 1.0

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

java.security. ProtectionDomain 1.2

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

java.security.CodeSource 1.2

Certificate[] getCertificates()
// 获取与该代码来源相关联的用千类文件签名的证书链。
URL getlocation()
// 获取与该代码来源相关联的类文件代码位置。

9.2.5 实现权限类

java.security.Permission 1.2

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

9.3.1 JAAS 框架

javax.security.auth.login.LoginContext 1.4

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

javax.security. auth.Subject 1.4

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, AccessControlContext context)
//以subj ect 的身份执行特许操作。它将返回run 方法的返回值。doAsPrivileged 方法在给定的访问控制上下文中执行该操作,你可以提供一个在前面调用静态方法AccessController.getContext() 时所获得的“上下文快照“,或者指定为null ,以便使其在一个新的上下文中执行该代码。

java.security. PrivilegedAction 1.4

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

java.security. PrivilegedExceptionAction 1.4

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

java.security.Principal 1.1

String get Name()
// 返回该特征的身份标识。

9.3.2 JAAS 登录模块

javax.security auth.callback.CallbackHandler 1.4

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

javax.security.auth.callback.NameCallback 1.4

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 1.4

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

javax.security.auth.spi.LoginModule 1.4

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

9.4.1 消息摘要

java.security. MessageDigest 1.1

static MessageDigest getInstance(String algorithName)
// 返回实现指定算法的MessageDigest 对象。如果没有提供该算法,则抛出一个NoSuchAlgorithmException 异常。
void update(byte input)
void update(byte[] input)
void update(byte[] input, int offset, int len)
// 使用指定的字节来更新摘要。
byte[] digest()
// 完成散列计算,返回计算所得的摘要,并复位算法对象。
void reset()
// 重置摘要。

9.5.2 密钥生成

javax.crypto.Cipher 1.4

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 ,或者UNWRAP_MODE 之一。
byte[] update(byte[] in)
byte[] update(byte[] in, int offset, 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 1.4

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

javax.crypto.spec.SecretKeySpec 1.4

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

9.5.3 密码流

javax.crypto.CipherlnputStream 1.4

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

javax.crypto.CipherOutputStream 1.4

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

    qrcode alipay
  2. 微信打赏

    qrcode weixin

评论区