JavaSE(十一)加密与安全

加密算法

  加密分为可逆加密(双向加密)和不可逆加密(单向加密),可逆的加密可以由明文得到密文,也可以由密文得到明文,而不可逆的加密只能由明文得到密文。
  加密算法也可以分为无密钥的算法和有密钥的算法,无密钥的算法只有在算法保密的前提下才是安全的,有密钥的算法只有算法和密钥同时泄密才会变得不安全。其实不可逆加密md5和可逆加密base64这些算法都是无密钥的,如果只有通信双方才知道这两种算法,那么信息可以说是安全的,但这些算法却是公开的,但这也可以有一些特定的用处(如md5计算指纹,验证原文是否被改动,base64显示不可显示的字节数组),有密钥的算法,如不可逆加密HMAC和可逆加密AES等,通常算法是公开的,而通信双方只需要保密密钥。
  常用的不可逆的加密算法就是消息摘要算法和HMAC算法,消息摘要算法能从任意长字符串的原文中获取较短长度字符串的消息摘要(一般用特定算法获取的消息摘要是定长的),且相同原文获得的消息摘要是相同的,而不同原文获得的消息摘要我们认为是不同的(即不发生碰撞,但由于长串的排列形式比短串多,所以理论上很多原文的消息摘要是相同的,但实际上很难遇见),所以我们也可以把消息摘要看成是原文的指纹,通过对比消息摘要来评定原文是否被改动过,另外保存的密码内容也通常为其消息摘要,这样既可以验证输入密码的正确性,也可以在第三方盗取了密码时,无法根据消息摘要获取真实密码,最常见的消息摘要算法有MD和SHA等。HMAC算法是根据一个密钥和给定的消息摘要算法从原文中提取消息摘要的算法。
  常用的可逆加密算法分为对称加密(又叫私钥加密)和非对称加密(又叫公钥加密),对称加密加解密使用相同的密钥,非对称加密加解密使用不同的密钥。通常对称加密的效率要比非对称加密的效率高得多,但密钥只有一份,安全存疑。对称加密和非对称加密的算法通常是公开的,它们通过保密密钥来保密原文。最常见的对称加密算法有DES、3DES、AES等,非对称加密有RSA、DSA等。
  所有密钥的顶层接口为Key,它有3个要素,分别是算法、编码形式和基本编码格式,算法是该密钥支持的算法如des,通过getAlgorithm()方法获取;编码形式其实就是密钥用字节数组的表现形式,通过它来保存和传递,通过getEncoded()方法获取;基本编码格式值如X.509、PKCS#8、RAW,通过getFormat()获取。Key有三个最直接的子接口,SecretKey、PublicKey、PrivateKey分别对应对称加密的密钥和非对称加密的公私钥,另外有类KeyPair是一对PublicKey和PrivateKey的封装。KeySpi是密钥材料,用于KeyFactory和SecretKeyFactory来生成密钥。
  密钥的生成方式有多种,不同的加密算法支持不同的密钥生成方式,单密钥可以通过KeyGenerator或SecretKeyFactory生成密钥,也可以通过SecretKeySpec直接new出来。KeyGenerator的静态方法getInstance(String algorithm)获取密钥生成器的实例,然后其init方法可以用来初始化生成的密钥大小(编码形式的长度)和随机源,很多算法其密钥大小是固定的,也是密钥生成器默认的,可以不调用init方法,而有些算法可能有多种密钥长度,这就需要调用init方法,最后调用generateKey()得到一个SecretKey实例,通过KeyGenerator得到的密钥的编码形式是随机的,所以此方法可用于生成随机编码形式的密钥,而不能指定密钥的编码形式,该方式适合DESede、DES、AES、ARCFOUR、Blowfish、RC2、HmacMD5、HmacSHA224等算法。SecretKeyFactory的静态方法getInstance(String algorithm)获取一个密钥生成工厂,然后调用generateSecret(KeySpec keySpec)得到一个SecretKey实例,而keySpec是生成密钥的材料,其具体实现类如SecretKeySpec、DESKeySpec等,该方式适合DESede、DES、AES和ARCFOUR算法。SecretKeySpec即是KeySpec的子类,又是SecretKey的子类,也就是说它既是密钥也是密钥材料,通过new就可以得到一个密钥,它支持算法有hmac系列、DES、DESede、AES。密钥对也有多种生成方式,可以通过KeyPairGenerator或KeyFactory生成,KeyPairGenerator通过其静态方法getInstance(String algorithm)获取生成器实例,然后调用其generateKeyPair()生成一个KeyPair对象,支持DiffieHellman、DSA、RSA、EC算法;KeyFactory先通过其静态方法getInstance(String algorithm)获取工厂实例,然后根据密钥材料通过generatePublic(KeySpec keySpec)或generatePrivate(KeySpec keySpec)生成公钥或私钥,密钥材料类视算法而定,如RSA生成公钥的密钥材料为X509EncodedKeySpec类型,私钥的密钥材料为PKCS8EncodedKeySpec类型,支持DiffieHellman、DSA、RSA、EC算法。
  可逆且不需要密钥的算法有Base64以及用16进制显示字节数组。Base64有一个基本表由64个长度为1byte的单元组成,被加密的字节数组以bit为单位,6bit分为一个组,如果长度不足就补位,这样每一个组有64种位组合,这种位组合的每一种都刚好对应基本表中的一个字节,这就得到加密后的结果,由于6bit就对应了一个byte,所以base64加密后的byte长度会变为原来的8/6,由于基本表的单元组成不同,就有多种Base64的加密方法,基本的Base64这基本表是有定义的,但其中有的单元byte用在url中有特殊的含义,所以把基本表中的这些单元换掉,就得到了针对URL加密的Base64加密方法,我们自己可以通过定义基本表来写自己的Base64算法,我们也可以把n位为一个分组来写自己的Base2的n次方加密算法。16进制显示字节数组的加解密过程和Base64一样,只是分组变为了4bit,而基本表的单元则是16进制的基本单元,这样nbyte的数组无需补位就可以得到2nbyte的密文,所以我们可以把他叫着base16加密算法。java中有现成的Base64加解密类。Base16算法java并没有实现,但我们自己可以轻易实现,循环被加密字节数组的每一个byte,并做如下操作miwen += strDigits[b >>> 4] + strDigits[b & 0x0f](其中strDigits是个字符串数组,元素依为"0",“1”…“F”,当然可以是大写或者小写)。java已经实现了Base64类,其有两个内部类分别为Encoder和Decoder,它们分别对应加密和解密器,Base64只有6个静态方法,它们分成三组,每一组有两个方法,一个获取加密器,一个获取解密器,这三组加解密器分别对应三组基本表,分别对应普通表、url表和mime类型表,加密和解密器对应的encode和decode方法就可以实现加解密了,如Base64.getEncode().encode(“str”.getBytes(“utf-8”))。
  消息摘要算法主要有MD(消息摘要的缩写)和SHA(安全散列算法的缩写)算法,他们都是在MD4的基础上进行的改进,SHA比MD5更安全,得到的指纹更难被模拟,但MD5更高效,在标准java中,消息原文和摘要都是字节数组,现支持消息摘要算法有MD2(16byte的摘要长度)、MD5(16byte)、SHA-1(20byte)、SHA-224(28byte)、SHA-256(32byte)、SHA-384(48byte)、SHA-512(64byte)。获取一段文字的消息摘要,首先需要通过消息摘要算法工厂类(MessageDigest即是消息摘要算法的工厂类,又是消息摘要算法的父类)的静态方法得到一个消息摘要算法的对象MessageDigest md=MessageDigest.getInstance(String algorithm),其参数是算法的名称,如"SHA-1",获得的消息摘要算法对象的原文是长度为0的字节数组,可以通过update(byte[] input)追加原文,因为是追加,可以多次调用update方法,得到的原文就是各个输入字节数组的依次连接,当最后通过调用digest()方法返回的字节数组就是原文的消息摘要,并且调用disgest方法后,消息摘要算法回到初始化状态,消息原文又变为长度为0的字节数组,也可以通过reset方法将消息原文变为长度为0的字节数组。消息摘要算法得到的是一个字节数组,如果将字节数组直接转换为字符串,极有可能会以乱码显示,为了沟通方便,我们通常把字节数组通过base64或base16等算法得到可显示的字符串,通过不同的方式将字节数组转化为字符串得到的字符串是不一样的,一般习惯用base16。
  散列消息鉴别码Hmac算法是一个密钥结合消息摘要算法(MD5、SHA等)来获取指纹的算法,它比直接的消息摘要算法更安全,java实现了该算法,先通过Mac类的静态方法getInstance()获取一个Mac实例,然后通过init(key)方法设置密钥,再通过update()追加原文,最后通过doFinal()返回mac值,hmac的密钥可以通过KeyGenerator生成,也可以通过直接new SecretKeySpec获取密钥。
  对称加密主要有DES、AES,对称加密算法首先需要通过加密器Cipher的静态方法getInstance(String transformation)获取一个加密器实例,其中参数transformation由三部分组成,之间通过/隔开如DES/CBC/PKCS5Padding,第一部分为算法,第二部分为工作模式,第三部分为填充模式,transformation也可以只有算法如DES,其他两个部分根据算法会有默认值;然后调用init(int opmode, Key key, IvParameterSpec iv)方法对加密器进行初始化,其中opmode为加密或者解密模式,取Cipher.ENCRYPT_MODE或Cipher.DECRYPT_MODE,key为密钥,iv为初始化向量(初始化向量可以通过new IvParameterSpec(byte[] byteS)得到);最后通过doFinal(byte[] src)返回加密/解密后的字节数组。
  非对称加密算法主要有RSA、DSA等,其中RSA、DSA可以用公钥加密私钥解密,也可以私钥加密公钥解密,但并不是所有的非对称加密都能如此。非对称加密的加解密代码和对称加解密一样,只是init方法的密钥是通过KeyPair获取的公钥或私钥。
  密钥交换算法DH并不用于加密,而是在不传递密钥的基础上(不会被第三方监听到密钥),让通信双方都能够知晓密钥,主要过程为通信方A将f(x, C)的值Fx以及C传给通信方B,通信方B将g(y, C)的值Gy传给通信方A,通信方B处理h(Fx, y, C)得到值M,通信方A使用k(Gy, x, C)得到N,当M=N时(即h(f(x, c), y, C) = k(g(y, c), x, C)),那么M或N就是协定好的密钥。函数f、g、h、k为算法公开的函数,监听的第三方只知道Fx、Fy、C的值,只要无法根据Fx以及Fy推导出x与y的值,第三方便无法获取密钥。

// 密钥顶级父类,有子类PublicKey和PrivateKey。
public interface Key extends java.io.Serializable {
  String getAlgorithm(); // 获取密钥对应的算法。
  String getFormat(); // 
  byte[] getEncoded();
}
  
// 封装的一对密钥,一个公钥一个私钥。
public final class KeyPair implements java.io.Serializable {
  PublicKey getPublic();
  PrivateKey getPrivate();
}

// 生产随机的对称密钥,支持算法有hmac系列算法、AES、ARCFOUR、Blowfish、DES、DESede、RC2。
public class KeyGenerator {
  SecretKey generateKey();
  String getAlgorithm();
  static KeyGenerator getInstance(String algorithm);
  static KeyGenerator getInstance(String algorithm, Provider provider); // 指定算法提供者,这一般是针对java没有集成的算法,由java核心包以外的包提供的算法。
  void init(int keysize, SecureRandom random); 
  void init(AlgorithmParameterSpec params); // 不同密钥生成算法的额外参数。
}
  
// 根据密钥材料生产对称密钥,支持的密钥算法有AES、DES、DESede、ARCFOUR、PBE。
public class SecretKeyFactory {
  static SecretKeyFactory getInstance(String algorithm);
  SecretKeyFactory getInstance(String algorithm, Provider provider);
  SecretKey generateSecret(KeySpec keySpec); // 根据密钥材料生产密钥。
}
  
// 既是密钥材料也是密钥,支持算法有hmac系列、DES、DESede。
public class SecretKeySpec implements KeySpec, SecretKey {
  SecretKeySpec(byte[] key, String algorithm);  
}
  
// 生成对称密钥。
public abstract class KeyPairGenerator extends KeyPairGeneratorSpi {
  static KeyPairGenerator getInstance(String algorithm);
  void initialize(int keysize);
  void initialize(int keysize, SecureRandom random);
  KeyPair generateKeyPair(); 
}
   
// 生成对称密钥。
public class KeyFactory {
  static KeyFactory getInstance(String algorithm);
  final PublicKey generatePublic(KeySpec keySpec);
  final PublicKey generatePrivate(KeySpec keySpec);
}

// Base64加解密器的工厂,而且是获取单例的工厂。
public class Base64 {
  public static Encoder getEncoder/getUrlEncoder/getMimeEncoder(); // 对应不同的基本表。
  public static Encoder getDecoder/getUrlDecoder/getMimeDecoder();  
}

// Base64的加密器。
public static class Encoder {
  public byte[] encode(byte[] src);
  public int encode(byte[] src, byte[] dst); // 将src加密后写入dst,返回写入dst的长度。
  public ByteBuffer encode(ByteBuffer buffer);
  public Encoder withoutPadding(); // 返回一个新的加密器,但此加密器不会填充=,默认的加密器是会填充的。
    // 原文nbyte=8nbit,6bit一组,最后可能不能刚好是6bit,那么就在后面补齐0,而这不一定是3byte的倍数,base64要求得到的byte数是3的倍数,如果不是需要补=号,此方法得到的加密器不会补=。
  public OutputStream wrap(OutputStream os); // 将一个输出流转换为另一个输出流,从新输出流写入的数据经过加密后写入原来的输出流。
}
  
// Base64的解密器。
public static class Decoder {
  public byte[] decode(byte[] src);
  public int decode(byte[] src, byte[] dst); // 将src解密后写入dst,返回写入dst的长度。
  public ByteBuffer decode(ByteBuffer buffer);  
  public InputStream wrap(InputStream is); // 将一个输入流转换为另一个输入流,从新输入流输出的数据都是经过原来输入流输出后解密过的数据。
}

// 消息摘要算法。
public abstract class MessageDigest extends MessageDigestSpi {
  public static MessageDigest getInstance(String algorithm); // 根据算法名如md5、SHA-1等获取一个消息摘要算法实体,并初始化原文为长度为0的字节数组。
  public final String getAlgorithm(); // 获取算法名称,如md5,sha-1。
  public void update(byte input); // 在原文上追加一个字节。
  public void update(byte[] input); // 在原文上追加多个字节。
  public final void update(ByteBuffer input); 
  public void update(byte[] input, int offset, int len); // 在原文上追加input从offset开始的len个字节。
  public byte[] digest(); // 返回消息摘要并将原文长度设置为0。
  public byte[] digest(byte[] input); // 等于update(input); digest();
  public int digest(byte[] buf, int offset, int len); // 将消息摘要放在buf中offset开始的地方,返回消息摘要的数据长度,可能抛异常。
  public void reset(); // 将原文长度设置为0。
}

// 散列消息鉴别码,mac算法由原文、key和消息摘要算法一起得到密文。
public class Mac implements Cloneable {
  public static final Mac getInstance(String algorithm); // 根据算法名如hmacmd5、hmacsha1等获取一个Mac算法实体,并初始化原文为长度为0的字节数组。
  public final String getAlgorithm(); // 获取算法名称,如hmacmd5、hmacsha1、hmacsha256。
  public final void init(Key key); // 初始化Key并将原文长度设置为0。
  public final void init(Key key, AlgorithmParameterSpec params); // 初始化Key并将原文长度设置为0,有的算法需要参数通过params传入。
  public final void update(byte input); // 追加原文。
  public final void update(byte[] input); // 
  public final void update(ByteBuffer input); // 
  public final void update(byte[] input, int offset, int len); // 
  public final byte[] doFinal(); // 返回mac值并将原文长度设置为0。
  public final byte[] doFinal(byte[] input); // 等于update(input); digest();
  public final void doFinal(byte[] output, int outOffset); // 将mac值存储在output中从outOffset开始的地方。
  public final void reset(); // 将原文长度置为0。
}

// 加密器,进行对称加密或非对称加密。
public class Cipher {
  static Cipher getInstance(String transformation);
  byte[] getIV();
  void init(int opmode, Key key); // init一系列方法进行初始化。
  byte[] update(byte[] input); // update一系列方法。
  byte[] doFinal();
  byte[] doFinal(byte[] bytes);  
}  

数字证书

  数据的散列值就像数是据的指纹一样,不同的数据我们认为其散列值不同。如果A用自己的私钥对数据的散列值进行加密,那么加密的结果被认为是A对原数据的数字签名。当接收方接收到数据和A的签名时,就可以用A的公钥来解密签名,从而验证数据的来源是否真的是A且未经过修改。数字签名包括两个算法,一是散列算法如md5,二是非对称加密算法。数字证书相关文件格式及后缀如下:

  • 数字证书,.cer / .crt / .pem
  • 证书链,.p7b
  • Keystore,PKCS#12格式后缀.pfx / .p12
  • 证书请求,.csr / .p10
  • 请求回复,.p7r,其实就是一个证书链,但只用于导入

数字证书

  数字证书是由权威的CA机构签发给申请机构的电子文档。申请机构将自己的机构信息、公钥等信息提供给CA机构,CA机构对申请机构经过严格的线下考核后,用CA机构自己的私钥对申请机构的信息、申请机构的公钥、CA机构的信息以及证书有效期等进行签名,将被签名的信息与数字签名一起组成的电子文档叫着数字证书
  一般用ASN.1来描述证书内容(ASN.1是一种抽象的语法规则,就像xml与json一样可以对一个对象进行描述),BER定义了如何把ASN.1类型的值编码为字节流,通常每个值不止有一种的BER编码方法,而DER就是BER中的一种编码方法,DER可以把ASN.1类型的值编码成唯一确定的字节流。X.509证书保存时一般有两种方式,一种是直接将DER编码的字节流以二进制方式存储;另一种是先将DER编码的字节流进行Base64格式编码,再在编码后的数据前加上一行-----BEGIN CERTIFICATE-----,在编码后的数据后加上一行-----END CERTIFICATE-----,然后进行保存,这添加的两行不能有任何变动,行首和行尾也不能添加任何字符(空白符也不行),但在begin行前的其他行以及end行后的其他行可以随意添加内容。一个证书文件中可以包含多个证书,其内容就是多个证书内容直接连接而成,不用添加其他任何成分X.509是最最常用的证书类型,其结构如下

-- 用来表示一个X.509的证书结构
Certificate  ::=  SEQUENCE  {
	tbsCertificate       TBSCertificate, -- 表示证书实际内容
	signatureAlgorithm   AlgorithmIdentifier, -- 发布方进行签名的算法
	signature            BIT STRING  -- 发布方对证书的签名值
}

TBSCertificate  ::=  SEQUENCE  {
	version         [0]  EXPLICIT Version DEFAULT v1, -- X.509证书的版本(现在只有v1、v2、v3),Version类型算是个整型(实际上用012表示v1、v2、v3)
	serialNumber         CertificateSerialNumber, -- 其实就是一个整型,同一个发布方签发的不同证书该序列号不同
	signature            AlgorithmIdentifier, -- 证书签名算法
	issuer               Name, -- 证书发布方相关信息
	validity             Validity, -- 证书有效期
	subject              Name, -- 证书主体方相关信息
	subjectPublicKeyInfo SubjectPublicKeyInfo,  -- 证书的公钥(主体方的公钥)
	issuerUniqueID  [1]  IMPLICIT UniqueIdentifier OPTIONAL, -- 证书发布方ID(可选),只在证书版本23中才有,UniqueIdentifier其实就是一个BIT STRING
	subjectUniqueID [2]  IMPLICIT UniqueIdentifier OPTIONAL, -- 证书主体方ID(可选),只在证书版本23中才有,UniqueIdentifier其实就是一个BIT STRING
	extensions      [3]  EXPLICIT Extensions OPTIONAL -- 证书扩展段(可选),由多个证书扩展项组成,只在证书版本3中才有
}

AlgorithmIdentifier ::= SEQUENCE {
	algorithm     OBJECT IDENTIFIER(简称OID类型), -- 签名算法,OID类型由一组句点分隔的非负整数组成,它与算法具有一一对应的关系,如1.2.840.10040.4.3代表SHA1withDSA算法
	parameters    ANY DEFINED BY algorithm OPTIONAL -- 算法相关的参数,不同算法其结构不同
}

Validity ::= SEQUENCE {
	notBefore      Time,  -- 证书有效期起始时间
	notAfter       Time  -- 证书有效期终止时间
 }

SubjectPublicKeyInfo ::= SEQUENCE {
        algorithm            AlgorithmIdentifier, -- 公钥算法
        subjectPublicKey     BIT STRING -- 公钥值
}

Extension ::= SEQUENCE {
	extnID      OBJECT IDENTIFIER,
	critical    BOOLEAN DEFAULT FALSE, -- 是否是关键扩展
	extnValue   OCTET STRING
}

// 数字证书
public abstract class Certificate implements java.io.Serializable {
    public final String getType(); // 证书类型,如X.509、PGP或SDSI
    public abstract byte[] getEncoded(); // 返回此证书的编码形式
    public abstract void verify(PublicKey key); // 验证是否已使用与指定公钥相应的私钥对此证书进行了签名,此公钥应该是发布方的公钥
    public abstract PublicKey getPublicKey(); // 获取证书中的公钥,此公钥是主体方的公钥   
}

// X509数字证书
public abstract class X509Certificate extends Certificate implements X509Extension {
	public final String getType(); // 证书类型为X.509
    public abstract int getVersion(); // 获取X.509证书版本
    public abstract BigInteger getSerialNumber(); // 获取证书的序列号,同一个CA机构签发的不同证书其序列号不同 
    
    public X500Principal getIssuerX500Principal(); // 以X500Principal的形式返回证书的发布方
    public X500Principal getSubjectX500Principal(); // 以X500Principal的形式返回证书的主体方    
    public abstract boolean[] getIssuerUniqueID(); // 获取证书发布方的ID,发布方ID是一个位序列,返回的boolean数组就相当于一个位序列,为ture的元素对应的位就是1,为false的元素对应的位就是0
    public abstract boolean[] getSubjectUniqueID(); // 获取证书主体方的ID,证书主体方的ID与发布方的ID格式一样
    
    public abstract Date getNotBefore/getNotAfter(); // 返回证书有效期的起止时间
    public abstract void checkValidity(Date date); // 检查证书在指定日期是否有效
    public abstract void checkValidity(); // 检查证书目前是否有效    
    
    public abstract String getSigAlgName(); // 获取证书签名算法的签名算法名,如SHA1withDSA
    public abstract String getSigAlgOID(); // 获取证书的签名算法OID字符串,OID字符串与签名算法名具有一一对应的关系
    public abstract byte[] getSigAlgParams(); // 获取算法参数
    public abstract byte[] getTBSCertificate(); // 返回TBSCertificate的编码形式,也就是除了签名部分的编码形式,签名便是以此为基础数据进行的签名
    public abstract byte[] getSignature(); // 获取证书的签名数据
    
    public Set<String> getCriticalExtensionOIDs(); // 获取该证书中所有关键扩展(critical字段为true的扩展)的OID集合
    public Set<String> getNonCriticalExtensionOIDs(); // 获取该证书中所有非关键扩展(critical字段为false的扩展)的OID集合 
    public byte[] getExtensionValue(String oid); // 获取指定OID的证书扩展的extnValue的BER编码值
    public abstract boolean[] getKeyUsage(); // 获取证书用途(对应OID为2.5.29.15的扩展项,不存在该OID的扩展项时返回null),证书用途只有一些预定义的用途,返回值的每一个元素都对应一个预定义的用途 ,返回值中为true的元素对应用途的组合就是证书用途
    public List<String> getExtendedKeyUsage(); // 获取证书的额外用途(对应OID为2.5.29.37的扩展项,不存在该OID的扩展项时返回nul)
    public abstract int getBasicConstraints(); // 对应OID为2.5.29.19的扩展项,如果证书主体方为一个证书颁发机构,返回通过该CA的认证路径深度的约束,否则返回-1
    public Collection<List<?>> getSubjectAlternativeNames(); // 对应OID为2.5.29.17的扩展项
    public Collection<List<?>> getIssuerAlternativeNames(); // 对应OID为2.5.29.18的扩展项
}

// 证书工厂可以根据输入流解析出证书、证书链 (CertPath) 和证书吊销列表
public class CertificateFactory {
    public static final CertificateFactory getInstance(String type); // 获取一个指定类型的证书工厂,如X.509类型的证书工厂
    public final String getType(); // 证书工厂类型,如X.509
    public final Certificate generateCertificate(InputStream inStream);  // 解析输入流中的第一个数字证书
    public final Collection<? extends Certificate> generateCertificates(InputStream inStream); // 解析输入流中的所有数字证书
    public final Iterator<String> getCertPathEncodings(); // 获取证书链支持的编码方式,如X509CertPath.PKIPATH_ENCODING、X509CertPath.PKCS7_ENCODING
    public final CertPath generateCertPath(InputStream inStream);  // 解析输入流为一个证书链
    public final CertPath generateCertPath(InputStream inStream, String encoding); // encoding表示解析流的编码方式,如X509CertPath.PKIPATH_ENCODING、X509CertPath.PKCS7_ENCODING
    public final CertPath generateCertPath(List<? extends Certificate> certificates); // 解析为一个证书链
    public final CRL generateCRL(InputStream inStream); // 解析输入流中的第一个证书吊销列表
    public final Collection<? extends CRL> generateCRLs(InputStream inStream); // 解析输入流中的所有证书吊销列表
}

证书吊销信息分发

  证书都是有有效期的,但如果在有效期内需要注销证书(如私钥泄漏、业务停止等),就需要依赖证书吊销信息分发机制。在X.509标准中采纳的是基于证书吊销列表(Certificate Revocation List,CRL)和基于在线证书状态协议(Online Certificate Status Protocol,OCSP)两种机制来分发证书吊销信息。
  CRL中包含了所有已吊销的证书信息(随着时间的推移,CRL会越来越大,所以CRL发布方会对CRL做定期清理,如果当前时间不在某个已吊销证书的有效期内,那么这个已吊销的证书信息就可以从CRL中移除)。CRL中包含了本次更新和下次更新时间,其间的时间差叫着吊销延迟,当通过CRL检查证书有效性时,先检查本地缓存的CRL是否有效,如果当前时间在下次更新时间之前,就使用本地缓存的CRL,否则重新获取CRL(证书中的扩展字段会包含如何获取CRL的信息)。CRL存在着固有的缺点,首先是随着时间的推进,CRL会越来越大,会消耗较大网络资源;存在吊销延迟,减小吊销延迟与网络消耗总是相互制约。
  OCSP服务器会应答客户端的证书状态查询请求。请求内容主要包括需要查询的证书的序列号,响应内容为该序列号对应的证书状态(有效、过期、未知)。OCSP解决了吊销延迟问题,但每一次验证证书链都需要去请求OCSP服务器,OCSP服务器抗压能力需要足够强。

-- 用来表示一个X.509的证书吊销列表
CertificateList  ::=  SEQUENCE  {
	tbsCertList          TBSCertList, -- 表示证书吊销列表实际内容
	signatureAlgorithm   AlgorithmIdentifier, -- 发布方进行签名的算法
	signature            BIT STRING   -- 发布方对证书的签名值
}

TBSCertList  ::=  SEQUENCE  {
	version                 Version OPTIONAL, -- X.509证书的版本(现在只有v1、v2、v3)
	signature               AlgorithmIdentifier, -- CRL签名算法
	issuer                  Name, -- CRL发布方相关信息
	thisUpdate              ChoiceOfTime, -- CRL发行日期
	nextUpdate              ChoiceOfTime OPTIONAL, -- CRL下次发行日期
	revokedCertificates     SEQUENCE OF SEQUENCE  { -- 所有已经吊销的证书列表
		userCertificate         CertificateSerialNumber, -- 证书序列号
		revocationDate          ChoiceOfTime, -- 证书吊销时间
		crlEntryExtensions      Extensions OPTIONAL -- 扩展端(可选)
	} OPTIONAL,
	crlExtensions           [0]  EXPLICIT Extensions OPTIONAL -- CRL扩展段(可选)
}

// 证书吊销列表
public abstract class CRL {
    public final String getType(); // 证书吊销列表的类型,如X.509
    public abstract boolean isRevoked(Certificate cert); // 检查指定的数字证书是否在当前证书吊销列表中
}

// X509证书吊销列表
public abstract class X509CRL extends CRL implements X509Extension {
    public abstract byte[] getEncoded(); // 返回此证书的编码形式
	public final String getType(); // 证书类型为X.509
    public abstract int getVersion(); // 获取X.509证书版本
    public abstract Date getThisUpdate(); // CRL发行日期
    public abstract Date getNextUpdate(); // CRL下次发行日期
    public X500Principal getIssuerX500Principal(); // 获取CRL发布方信息
    
    public abstract void verify(PublicKey key); // 当前证书吊销列表如果不是由与指定公钥配对的私钥签发的,抛出异常
	public abstract String getSigAlgName(); // 获取吊销列表签名算法的签名算法名,如SHA1withDSA
    public abstract String getSigAlgOID(); // 获取吊销列表签名算法OID字符串,OID字符串与签名算法名具有一一对应的关系
    public abstract byte[] getSigAlgParams(); // 获取算法参数
    public abstract byte[] getTBSCertList(); // 获取CRL实际内容的编码形式,不包含签名信息,签名信息由此数据签名而来
    public abstract byte[] getSignature(); // 获取CRL的签名信息    
    
    public abstract Set<? extends X509CRLEntry> getRevokedCertificates(); // 获取CRL吊销的证书条目
    public abstract X509CRLEntry getRevokedCertificate(BigInteger serialNumber); // 根据证书序列号获取证书条目
    public X509CRLEntry getRevokedCertificate(X509Certificate certificate); // 根据证书获取证书条目
}

证书链(证书路径)

  证书链是由一系列证书构成的一个链,链首证书为目标证书(又叫终端证书)。目标证书受信任的证书链应该具备如下特征:

  • 在证书链上除最后一个证书外,证书颁发者等于其后一个证书的主体方
  • 在证书链上除最后一个证书,每个证书都是由其后一个证书对应的私钥签名的
  • 在证书链上最后一个证书的发布方必须是最受信任的CA(如果是自签名证书,发布方就是自己,那么自己必须是受信任的CA)

  在Java中用CertPath来表示一个证书链,通过CertPathValidator来验证目标证书链是否受信任。

// 证书链
public abstract class CertPath implements Serializable {
    public String getType(); // 获取证书链类型,如X.509
    public abstract Iterator<String> getEncodings(); // 获取证书链支持的编码方式,如X509CertPath.PKIPATH_ENCODING、X509CertPath.PKCS7_ENCODING
    public abstract byte[] getEncoded(); // 通过默认编码方式得到证书链的字节流
    public abstract byte[] getEncoded(String encoding); // 通过指定编码方式得到证书链的字节流
    public abstract List<? extends Certificate> getCertificates(); // 获取证书列表
}

// X.509证书链
public class X509CertPath extends CertPath {
    public X509CertPath(List<? extends Certificate> certificates); // 通过参数构建证书链
    public X509CertPath(InputStream var1); // 采用默认编码方式解析输入流,构建证书链
    public X509CertPath(InputStream var1, String encoding); // 采用指定编码方式解析输入流,构建证书链
    public static Iterator<String> getEncodingsStatic(); // 与getEncodings()等效的静态方法
}

// 用于验证证书链
public class CertPathValidator {
    public final static String getDefaultType(); // 返回Java安全属性文件中所指定的默认CertPathValidator类型,如果没有此属性,则返回PKIX
    
    public static CertPathValidator getInstance(String algorithm); // 根据校验算法获取获取校验器实例,算法如PKIX
    public final String getAlgorithm(); // 返回当前校验器的校验算法
    
    public final CertPathValidatorResult validate(CertPath certPath, CertPathParameters params); // 根据校验参数对证书链进行校验,校验未通过时抛出异常,校验通过时会返回一些相关信息
}

// 对证书链进行校验的校验参数
public class PKIXParameters implements CertPathParameters {
    public PKIXParameters(Set<TrustAnchor> trustAnchors); // 创建校验参数并将trustAnchors设置为最受信任的CA
    public PKIXParameters(KeyStore keystore); // 创建校验参数并将keystore中的所有证书条目设置为最受信任的CA

	// 最受信任的CA集合,证书链链尾证书的发布方必须在最受信任的CA集合中
    public Set<TrustAnchor> getTrustAnchors(); 
    public void setTrustAnchors(Set<TrustAnchor> trustAnchors);
    
    // 校验的时间,必须在有效期内
    public Date getDate(); // 默认返回当前时间
    public void setDate(Date date);
    
    // 是否启用URL校验
    public boolean isRevocationEnabled();
    public void setRevocationEnabled(boolean val);
    
    // 如果需要对证书链上的证书进行额外的检查,可以添加CertPathCheckers,在检查证书链时会为证书链上的每一个证书调用每一个CertPathCheckers的check方法
    public List<PKIXCertPathChecker> getCertPathCheckers();
    public void setCertPathCheckers(List<PKIXCertPathChecker> checkers);
    public void addCertPathChecker(PKIXCertPathChecker checker);
}

// 最受信任的CA,它负责检验证书链末尾的证书,由于本身不需要被检验,所以不需要自己被签名的信息
public class TrustAnchor {
    public TrustAnchor(X509Certificate trustedCert, byte[] nameConstraints); // nameConstraints为额外限定条件,不需要时设置为null
    public TrustAnchor(X500Principal caPrincipal, PublicKey pubKey, byte[] nameConstraints);
    public TrustAnchor(String caName, PublicKey pubKey, byte[] nameConstraints);
}

// 该虚拟类的定义是个SPI定义,会被框架回调
public abstract class PKIXCertPathChecker implements CertPathChecker, Cloneable {
    public abstract void init(boolean forward); // 进行初始化设置,forwad表示是否为正向检查

	// 正向检查会从目标证书开始依次调用check方法,反向检查会从链尾证书开始
    public abstract boolean isForwardCheckingSupported(); // 是否支持正向检查(并不代表是否使用正向检查,具体使用正向检查还是反向检查由上层框架调用init方法时决定)
    
    public abstract Set<String> getSupportedExtensions(); // 返回该checker支持的证书扩展项的OID集合
    public abstract void check(Certificate cert, Collection<String> unresolvedCritExts); // 对指定的证书定义check处理,如果check失败抛出Check异常。unresolvedCritExts表示该checker支持的还未处理的证书扩展项的OID集合
    public void check(Certificate cert) // check(cert, Collections.<String>emptySet());
}

密钥库

  密钥库KeyStore用来管理加密密钥和证书,它有多种格式,如JKS(默认类型)、PKCS12(常用后缀p12,行业标准模式,推荐类型)、JCEKS(支持SecretKeyEntry类型条目)。密钥库主要由许多条目Entry组成,每一个条目由其别名标识。KeyStore的条目有三种类型:

  • KeyStore.PrivateKeyEntry:包含一个非对称加密的私钥及其相关联的证书链,条目可以被保护(如设置密码)。
  • KeyStore.SecretKeyEntry:包含一个对称密钥SecretKey,条目可以被保护(如设置密码)。JKS与PKCS12类型的KeyStore都不支持该类型的条目,但JCEKS类型的KeyStore支持。
  • KeyStore.TrustedCertificateEntry:包含一个数字证书(只存在公钥),条目不可以被保护。

  密钥库可以持久化并可以设置密码,一般保存为一个密钥库文件,也可以持久化到智能卡或其他集成的加密引擎

// ProtectionParameter是用于安全保护相关的参数,而PasswordProtection作为其实现类,为安全保护提供密码参数
public static class PasswordProtection implements ProtectionParameter, javax.security.auth.Destroyable {
    public PasswordProtection(char[] password);
    public synchronized char[] getPassword();
}

// 代表一个密钥库条目,所有条目都包含一个键值对属性
public static interface Entry {
    public default Set<Attribute> getAttributes(); // Attribute是个键值对接口,一个name,一个value,Attribute有实现类PKCS12Attribute
}        

public static final class SecretKeyEntry implements Entry {
    public SecretKeyEntry(SecretKey secretKey);
    public SecretKeyEntry(SecretKey secretKey, Set<Attribute> attributes);
    public SecretKey getSecretKey();
}

public static final class PrivateKeyEntry implements Entry {
    public PrivateKeyEntry(PrivateKey privateKey, Certificate[] chain);
    public PrivateKeyEntry(PrivateKey privateKey, Certificate[] chain, Set<Attribute> attributes);
    public PrivateKey getPrivateKey();
    public Certificate[] getCertificateChain();
    public Certificate getCertificate();
}

public static final class TrustedCertificateEntry implements Entry {
    public TrustedCertificateEntry(Certificate trustedCert);
    public TrustedCertificateEntry(Certificate trustedCert, Set<Attribute> attributes);
    public Certificate getTrustedCertificate();
}
    
// 密钥库
public class KeyStore {
    public final static String getDefaultType(); // 获取默认的密钥库类型
    
    public static KeyStore getInstance(String type); // 生成指定类型的空密钥库,须要调用load进行初始化
    public static KeyStore getInstance​(File file, char[] password); // 从java9开始新增的接口,从密钥库文件中加载密钥库,需要密钥库文件的密码    
    public final void load(InputStream stream, char[] password); // 从指定流中加载密钥库,如果只是为了初始化,参数全为null
    public final void store(OutputStream stream, char[] password); // 将密钥库持久化到流中
    
    public final String getType(); // 获取当前密钥库类型
    public final int size(); // 密钥库的条目数量
    public final Enumeration<String> aliases(); // 获取所有条目的别名
    public final boolean containsAlias(String alias); // 是否包含指定别名的条目
    public final boolean isKeyEntry(String alias); // 指定别名的条目是否是PrivateKeyEntry或者SecretKeyEntry类型
    public final boolean isCertificateEntry(String alias); // 指定别名的条目是否是TrustedCertificateEntry类型
    public final boolean entryInstanceOf(String alias, Class<? extends KeyStore.Entry> entryClass); // 指定别名的条目是否是某个类型的条目(entryClass类或其子类)
    
    public final void deleteEntry(String alias); // 删除条目
    public final Entry getEntry(String alias, ProtectionParameter protParam); // 获取指定别名的对应条目,如果条目为不需要被保护的条目类型( 如TrustedCertificateEntry条目),protParam设置为null
    public final void setEntry(String alias, Entry entry, ProtectionParameter protParam);  // 不存在alias相应条目时添加条目,存在时替换原条目(与原条目类型必须相同,否则抛出异常)。protParam用于保护条目,如果条目为不需要被保护的条目类型( 如TrustedCertificateEntry条目),设置为null    
    public final Key getKey(String alias, char[] password); // alias对应着SecretKeyEntry返回其SecretKey,alias对应着PrivateKeyEntry返回其PrivateKey,alias对应着TrustedCertificateEntry返回null
    public final Certificate getCertificate(String alias); // alias对应着TrustedCertificateEntry返回其证书,alias对应着PrivateKeyEntry返回其终端证书(证书链下标为0的证书),alias对应着SecretKeyEntry返回null
    public final Certificate[] getCertificateChain(String alias); // alias对应着PrivateKeyEntry返回其证书链,alias对应着SecretKeyEntry或TrustedCertificateEntry返回null
    public final Date getCreationDate(String alias); // 条目创建或更改时间
    public final void setKeyEntry(String alias, Key key, char[] password, Certificate[] chain); // 如同setEntry方法,但只针对PrivateKeyEntry与SecretKeyEntry,对于SecretKeyEntry参数chain为null 
    public final void setCertificateEntry(String alias, Certificate cert); // 如同setEntry方法,但只针对CertificateEntry
}

数字证书的生成

  前面数节,不是从keystore中获取Certificate,就是从流中获取Certificate,实际上都是将已经存在的证书解析为Certificate对象而已,如何在Java中新建一个Certificate对象呢?手动编写一个符合asn.1格式且用DER编码的字节流,然后以此流来构建Certificate对象,虽然理论上可行,但不可取,正如一般不会手动编写字节码一样,可以通过rt.jar中sun.security.tools.keytool包下面的类构建一个Certificate对象(Java9开始不兼容此包,所以无法在Java9+中使用该方法),另外一种方式是使用第三方的bouncycastle包来构建Certificate对象。

管理工具

  JRE中提供了keytool来进行密钥和证书的管理,但最常用、功能最强大的工具是openssl。keytool基于Java实现,openssl基于C语言实现。

keytool

  keytool是一个集合了多个命令的工具,通过keytool来执行命令的格式为:keytool -command options。通过keytool -help可以查看keytool支持的各种命令,通过keytool -command -help可以查看指定命令的详细用法,所有命令如下:

  • genkeypair,用于生成密钥对条目,条目包括密钥对及其自签名证书。如:keytool -genkeypair -alias www.bo.org -keyalg RSA -keystore d:/keystore/bo.keystore -storetype pkcs12
  • genseckey,生成对称密钥条目。如:keytool -genseckey -alias test -keystore d:/keystore/bo.keystore
  • certreq,生成证书请求。如:keytool -certreq -alias www.bo.org -keystore d:/keystore/bo.keystore -file d:/keystore/cert.csr
  • gencert,根据证书请求生成证书。如:keytool -gencert -infile d:\keystore\cert.csr -outfile d:\keystore\test_to_bo.crt -alias test -keystore d:\keystore\test.keystore
  • importcert,导入证书或证书链。如:keytool -importcert -file d:/keystore/test.crt -alias test -keystore d:/keystore/bo.keystore。**在指定的keystore中,如果不存在指定别名的条目,会新建一个受信任的证书条目;如果已经存在指定别名的条目且条目为密钥对条目,同时密钥对条目的公钥与导入的证书公钥相同,而且导入的证书能被其他受信任的证书条目所验证,形成证书链,那么就用该证书链替换原来密钥对条目中的证书链,否则报错。
  • importkeystore,从另一个密钥库导入一个或多个条目。如:keytool -importkeystore -srckeystore d:/keystore/src.keystore -srcalias srcalias -destkeystore d:/keystore/dest.keystore -destalias destalias
  • exportcert,导出证书。如:keytool -exportcert -keystore d:/keystore/bo.keystore -alias www.bo.org -file d:/keystore/bo.crt
  • list,列出密钥库中所有条目。如:keytool -list -v -keystore d:/keystore/bo.keystore
  • printcert,打印证书内容。如:keytool -printcert -v -file d:\keystore\bo.crt
  • printcertreq,打印证书请求内容。如:keytool -printcertreq -v -file d:\keystore\bo.csr
  • printcrl,打印crl文件内容。如:keytool -printcrl -v -file d:/keystore/bo.crl
  • delete,删除条目。如:keytool -delete -keystore d:/keystore/bo.keystore -alias test
  • changealias,更改条目别名。如:keytool -changealias -destalias newalias -alias oldalias -keystore d:/keystore/bo.keystore
  • keypasswd,更改条目密钥口令。如:keytool -keypasswd -new newpasswd -alias test -keystore d:/keystore/bo.keystore
  • storepasswd,更改密钥库存储口令。如:keytool -storepasswd -new newpasswd -keystore d:/keystore/bo.keystore

openssl

  OpenSSL是一个基于C语言的开源项目,其组成主要包括一下三个组件:

  • openssl,多用途的命令行工具,类似keytool,但却比keytool常用且功能更完善。
  • libcrypto:加密算法库。
  • libssl:加密模块应用库,实现了ssl及tls。

  openssl的许多命令都需要指定配置文件,下载的openssl中有默认的配置文件,位于share目录下的openssl.cnf。openssl配置文件是由若干键值对组成的,有些键值对表示的是一些基本文件,必须在使用openssl命令之前保证这些文件已经存在且得到了初始化,常用的有健database对应的文件,new_certs_dir对应的目录,serial对应的文件(并且在该文件中初始化写入01)。
  以下命令展示了证书签发的过程,更多OpenSSL的知识需要更大的篇幅学习,这里不再讲解。

  1. 生成根CA并自签(根CA机构完成)
      # 生成根CA的密钥对
      openssl genrsa -des3 -out e:/pki/keys/RootCA.key 2048
      # 生成自签名证书,如果没有-x509选项生成的是证书请求
      openssl req -new -x509 -days 3650 -key e:/pki/keys/RootCA.key -out e:/pki/keys/RootCA.crt -config …/share/openssl.cnf
  2. 生成二级CA(二级CA机构与根CA机构共同完成)
      # 生成二级CA的密钥对
      openssl genrsa -des3 -out e:/pki/keys/secondCA.key 2048
      # 去除.key文件的密码,因为nginx配置密钥文件时,不支持带密码的密钥文件
      openssl rsa -in e:/pki/keys/secondCA.key -out e:/pki/keys/secondCA.key
      # 生成证书请求,如果添加-x509选项生成的就是自签名证书
      openssl req -new -days 3650 -key e:/pki/keys/secondCA.key -out e:/pki/keys/secondCA.csr -config …/share/openssl.cnf
      # 数字签名,这一步由根CA完成,-extensions v3_ca会生成扩展表示本次签发的证书归属于一个CA机构,该CA机构可以继续签发下级证书
      openssl ca -extensions v3_ca -in e:/pki/keys/secondCA.csr -days 3650 -out e:/pki/keys/secondCA.crt -cert e:/pki/keys/RootCA.crt -keyfile e:/pki/keys/RootCA.key -config …/share/openssl.cnf
  3. 使用二级CA签发服务器证书
      # 生成服务器的密钥对
      openssl genrsa -des3 -out e:/pki/keys/server.key 2048
      # 去除.key文件的密码
      openssl rsa -in e:/pki/keys/server.key -out e:/pki/keys/server.key
      # 生成证书请求
      openssl req -new -days 3650 -key e:/pki/keys/server.key -out e:/pki/keys/server.csr -config …/share/openssl.cnf
      # 数字签名,这一步由二级CA完成
      openssl ca -in e:/pki/keys/server.csr -days 3650 -out e:/pki/keys/server.crt -cert e:/pki/keys/secondCA.crt -keyfile e:/pki/keys/secondCA.key -config …/share/openssl.cnf

沙箱机制

  沙箱机制把代码隔绝在一定权限内运行,仿佛放在一个箱子中,只具有箱子内的功能权限而不具有箱子外的功能权限。默认情况下,沙箱机制通过安全管理器来检查是否对某个权限放行,而代码拥有的所有权限通过策略文件来配置。

安全管理器

  安全管理器SecurityManager是一个负责控制具体操作是否允许执行的类,其方法checkPermission(Permission perm)用来检查是否具有perm权限,当检查不通过时抛出SecurityException,否则正常返回。在执行敏感操作之前,可以用安全管理器的checkPermission方法来检查当前程序是否具有执行该敏感操作的权限(一般先判断是否开启了安全管理器功能,如果未开启就不做权限检查,如果开启了才调用checkPermission方法),但权限检查是在执行敏感操作前由程序员编写,所以程序员只要不做检测就不会对敏感操作做检查。这样将主动权交到访问者手里似乎是没有起到任何资源保护的效果,所以对资源的访问应该提供完备的API,在API中实现安全检查功能,只要访问程序调用了这些API,就附带地进行了安全检查,这需要防止访问程序绕过资源访问定义好的API,用其他方式去访问资源,在java的核心包中也的确提供了这样的机制,比如进行文件访问时,在调用对流的读写等API时,这些API中就进行了安全检查功能。
  一个虚拟机可以安装一个安全管理器(安装了安全管理器就相当于开启了安全管理器功能,默认启动Java程序是没有开启安全管理功能的),通过System的静态方法setSecurityManager(final SecurityManager s)设置或者在启动JVM时添加-Djava.security.manager=包含包名的安全管理器类来设置,如果-Djava.security.manager后面跟任何内容,那么使用默认的安全管理器。
  通过系统类的System.getSecurityManager()方法可以获得系统的安全管理器,默认情况下,系统类是没有安全管理器的,所以返回null,在进行安全检查时,一般先判断系统是否注册了安全管理器,如果有就进行安全检查,否则不做检查
  安全管理器SecurityManager实际上是通过AccessControllerContext的checkPermision方法来进行权限控制的(SecurityManager的checkPermission方法直接调用了AccessControllerrContext的checkPermssion方法)。当然,我们也可以继承SecurityManager类重写checkPermission方法来实现自己的安全控制手段,比如我们不允许任何的文件写操作,只需要重写checkWrite方法,让其直接抛出SecurityException异常。
  AccessControllerContext主要包含一个保护域集合,其权限检查过程实际上是对该保护域集合中的所有保护域进行权限检查,只要有一个保护域权限检查失败,那么AccessControllerrContext的权限检查就失败
  AccessController的静态方法getContext()可用来构建AccessControllerrContext,该方法会将当前线程的栈顶(当前线程此时的栈顶就是AccessController的getContext()方法对应的栈帧)到栈底(如果某个栈帧是AccessController.doPrivileged(PrivilegedAction action)方法对应的栈帧,那么就到该方法接近底的下一帧为止,也就是调用该方法的方法对应的栈帧,不用继续到栈底)的所有栈帧对应方法所属类的保护域组成的集合来初始化AccessControllerContext
  SecurityManager检查权限,一般只需要调用checkPermission(Permission perm)方法,但如果某个线程发送事件到另一个线程进行事件处理,对于事件进行处理的线程希望发送事件的线程有相应权限时(而不是处理线程有这个权限),可以要求事件发送者同时发送其上下文快照(在两个线程并行执行过程中,事件发送线程的栈帧会不断被创建和销毁,而发送给事件处理线程的上下文是发送时的上下文,并且不应该随着事件发送线程的执行而改变),获取上下文快照只需要在发送线程调用SecurityManager.getSecurityContext()获取。

public class SecurityManager {
    public Object getSecurityContext(); // 获取默认的securityContex,实际上调用了AccessController.getContext(),所以实际返回的就是AccessControlContext类型
    public void checkPermission(Permission perm, Object context); // context虽然可以是任何类型,但实际中使用AccessControlContext类型    
    public void checkPermission(Permission perm); // 其实就是调用checkPermission(perm, getSecurityContext())
    public void checkXxx(...); // 一系列的权限检查方法,实现上都是根据该方法自身的意义构建相应类型的Permission并调用checkPermission方法,如果找不到对应的checkXxx方法来检查权限,那么就直接调用checkPermission方法。
}

权限、保护域和策略

  权限类Permission是一个虚拟类,表示的就是一类权限,其主要成员应该有两个,一个是名name,一个是动作actions,名指示了权限的主体,动作指示了主体的权限,可以通过getName和getActions方法获得,一般权限类都应该有以这两个成员作为参数的构造方法,在策略配置文件中可以配置这两个成员。方法public abstract boolean implies(Permission permission)是权限类最重要的方法,他表示如果具有了当前权限,是否具有permission权限,这样可以判断一段代码拥有了一个权限,那么是否能够访问受另一个权限保护的资源,比如AllPermission实现本方法的时候就直接返回true,也就是拥有AllPermission权限的代码可以访问受任何权限保护的资源,再比如具有FilePermission("/-",“read”)权限一定具有FilePermission("/any/any…", “read”)权限,这也是由其implies方法定义的。下面为JRE中预定义的一部分权限说明,当需要使用其他权限时可以查看相关手册或直接分析其Permission实现类的implies方法

  • FilePermission的name表示文件名,"<<ALL FILES>>“表示所有文件,”/dir…/*“表示指定目录下的所有文件和目录,以”/dir…/-“表示递归指定目录下的所有文件和目录,”*“表示当前目录下的所有文件和目录,”-“表示递归当前目录下的所有文件和目录;actions可以是"read”、“write”、“execute”、“delete"或"readlink”。
  • SocketPermission的name为"服务器:端口号",服务器可以是域名或ip地址,当为域名时,可以使用"*“代替靠左的多级域名,如*.sina.cn,端口号可以直接一个数字表示端口号,或x-y表示x到y端口号,-y表示0-y,x-表示大于x的端口号;actions可以是"accept”、“connect”、“listen"或"resolve”,多个actions可以用“,”分割。

  权限集合PermissionCollection就是由一系列Permission组合而成,其方法isReadOnly()返回是否可以往集合里面添加新的权限,方法add(Permission permission)就是往集合里面添加权限,而implies(Permission permission)方法就是检查该权限集合是否具有某个权限,只要权限集合中有一个权限的implies(某个权限)返回true,那么就返回true,否则返回false
  保护域ProtectionDomain主要由一个代码源和一个权限集合组成,类加载器在加载类时会为每一个加载的类指定一个保护域,权限控制器就是通过线程栈的各个帧的保护域来验证权限,只有线程栈的各个帧所属类的保护域都具有某个权限时,代码才具有相应权限,而ProtectionDomain的implies方法就是调用的其权限集合的implies方法。
  保护域是在类加载的过程中指定给被加载的类的,默认情况下,指定保护域的时候用到了策略Policy,策略也是系统的一个单例。策略Policy由很多的grant组成,每一个grant都对应一个key和一个权限集合,当加载器加载类时,会读取类的来源和授权信息,并将它们组合成一个代码源CodeSource,而代码源的每一个来源和授权信息都可能从Policy中找到一个对应key,根据这些key找到对应的grant的权限集合,这些权限集合的权限并集组成一个新的权限集合,这个新的权限集合和这个代码源就组成了类的保护域。可以通过Policy的getPolicy和setPolicy来获取和注册系统的单例策略,习惯性通过安全配置文件(jre/lib/security/java.security文件,是Java安全框架的配置文件)来配置策略(在java.security文件中通过关键字policy.provider来指定策略实现类)。默认的安全配置文件中部分配置如下:

policy.provider=sun.security.provider.PolicyFile # 指定了默认使用的安全策略
policy.url.1=file:\${java.home}/lib/security/java.policy # 与用户无关的配置
policy.url.2=file:\${user.home}/.java.policy # 用户不同,其配置不同

  PolicyFile为默认使用的策略,它通过策略配置文件来实现安全策略,策略配置文件的位置在安全配置文件中指定(注意安全配置文件与策略配置文件不同)。PolicyFile策略会从头扫描java.security文件中的policy.url.n的配置(n从1开始,依次递增,直到找不到配置为止,所以如果没有配置policy.url.2直接配置policy.url.3不会扫描到policy.url.3)并将扫描到的配置文件加载;也可以通过-Djava.security.policy=或==指定要加载的策略文件(=会在加载完指定文件后继续加载java.security里面配置的文件,而==不会)。
  策略配置文件主要包含授权项列表,其中可能包含密钥仓库项keystore以及零个或多个授权项grant。其格式如下:

keystore "some_keystore_url", "keystore_type";

grant [signedBy "signer_names"][, codeBase "url"] [, principal Principal实现类名(含包如a.MyPrincipal) "Principal实现类构造函数参数"] {
    permission package.PermissionClassName ["permissionName"][, “permissionAction"][, signedBy “signer_names" ];
    permission ...
};

   如上例,keystore 是存放私钥及相关数字证书的数据库grant表示一个授权项,codeBase表示指定位置的类可以获得此授权项(如"file:/c:/dir/*"、“file:/c:/dir/-”),signedBy表示用keystore中指定别名的证书签名的类可以获得此授权项principal表示具有某个身份的使用者可以获得该授权项(详见JAAS),多个grant项表示符合任一grant的代码都具有该授权项下的所有权限,如果同一个grant中有多个配置用“,”隔开,表示同时符合这几个配置的代码才能获取该授权项下的权限。 默认的jre/lib/security/java.policy文件中部分配置如下:

grant codeBase "file:${{java.ext.dirs}}/*" {
    permission java.security.AllPermission;
};

grant {
    permission java.lang.RuntimePermission "stopThread";        
    permission java.net.SocketPermission "localhost:0", "listen";
    ...
};

JAAS

  JAAS是Java Authentication and Authorization Service(Java用户认证和授权服务)的缩写,是对用户的认证和授权,而原沙箱机制是对代码的授权,这里的用户是一个抽象的概念,其实JAAS是在在原沙箱机制的基础上做出的扩展,它必须依赖于原沙箱机制。
  认证主要负责确定程序使用者(用户,认证主体)的身份(角色),授权将各个身份赋予相应的权限。在JAAS中,类Subject代表用户,而类Principal代表身份,一个用户可以有多个身份。Subject的权限检查依然在AccessControllerContext的checkPermision方法中完成,所以JAAS是在原沙箱机制的基础上进行的检查。通过Subject的静态方法doAs或doAsPrivileged方法可以对封装的代码赋予使用者Subject(使用者Subject有多个身份Principal,身份被赋予了授权项grant)。
  JAAS的授权是对原沙箱机制的补充,在原沙箱机制基于codeBase与signedBy的基础上添加了principal。每一个grant,可能包括codeBase、singedBy和principal中的0个或多个条件。当grant一个条件都不包括时,所有AccessControlContext都拥有此grant;当包含多个时条件时,只有同时满足这些条件的AccessControlContext才能拥有此grant。doAs就是对代码进行使用者Subject授权,如果在doAs外执行代码,那么就不存在Subject,这时所有的包含principal条件的grant都不会被授权。
  在进行授权操作时,需要Subject对象,可以直接new一个Subject对象然后进行身份设置,不过JAAS框架提供了认证框架来获取Subject。针对每一个Principal需要创建一个LoginContext实例,调用LoginContext实例的login()方法可以进行登录操作(会创建Principal并赋予身份,如果失败抛出异常),调用logout()可以进行注销操作(主要是身份清零),通过getPrincipal()方法就可以获取Principal。LoginContext用起来极其简单,那是因为LoginContext大量的底层工作是通过回调LoginModule来完成的,所以最关键的部分是编写LoginModule。
  与原沙箱机制的Policy类似,在JAAS框架中也有一个系统单例Configuration(对应类javax.security.auth.login.Configuration), Configuration由许多配置单元组成,每一个配置单元包含一个名称与一个配置条目的有序列表(配置条目对应类AppConfigurationEntry),每一个配置条目包含了一个LoginModule实现类全名、一个控制标识和若干个额外参数。LoginContext通过配置单元的名称与配置单元绑定,从而根据绑定的配置单元的配置条目列表对LoginModule进行回调。
  控制标识包括required、requisite、sufficient与optional。required要求LoginModule成功,无论成功失败都继续下一个配置条目;requisite要求LoginModule成功,成功继续下一个条目,失败不在继续;sufficient不要求LoginModule成功,失败继续下一个条目,成功不在继续;optional不要求LoginModule成功,无论成功失败都继续下一个配置条目。
  ConfigFile为默认的Configuration,它通过配置文件来创建Configuration(在安全配置文件java.security中指定了默认的Configuration,配置为login.configuration.provider=sun.security.provider.ConfigFile,而login.config.url.1=file:${user.home}/.java.login.config指定了ConfigFile的配置文件)。配置文件格式如下:

AppName1 {
	ModuleClass Flag ModuleOptions; // 如com.java.test.common.abc.DemoLoginModule required debug=true myExt=extValue;
	ModuleClass2 Flag ModuleOptions;	
};

AppName2 {
	ModuleClass Flag ModuleOptions;
};

// 代表一个身份(角色),在实现该接口时通常需要一个包含name参数的构造函数
public interface Principal {
    public String getName(); // 获取身份名字
    public boolean equals(Object another); // 判断两个principal是否相等,一般判断getName()是否相等
    public int hashCode(); // 一般可以取getName().hashCode()
    
    // 判断使用者Subject是否拥有当前身份Principal
    // 默认实现为subject.getPrincipals().contains(this); 由于默认方法的存在,所以该方法可以不被重写,转而实现contains所依赖的方法equals
    // 框架会回调该函数,当前的Principal对象是根据grant的principal条件生成的Principal实例,而参数subject是doAs传入的subject
    public default boolean implies(Subject subject); 
}

// 代表一个程序使用者(用户,认证主体)
public final class Subject implements java.io.Serializable {
    public Subject();
    
    public Set<Principal> getPrincipals(); 
    public <T extends Principal> Set<T> getPrincipals(Class<T> c); // 返回c及其子类型的Principal

	// PrivilegedAction就包含一个run方法,run及其中调用的方法的代码使用者都为指定的subject
    public static <T> T doAs(final Subject subject, final java.security.PrivilegedAction<T> action);
    public static <T> T doAs(final Subject subject, final java.security.PrivilegedExceptionAction<T> action);
    public static <T> T doAsPrivileged(final Subject subject, final java.security.PrivilegedAction<T> action, final java.security.AccessControlContext acc);
    public static <T> T doAsPrivileged(final Subject subject, final java.security.PrivilegedExceptionAction<T> action, final java.security.AccessControlContext acc);
    
    ...
}

// 登录上下文,会回调name对应的所有LoginModule(受控制标识控制)
public class LoginContext {
	// 不带subjcet参数时会创建一个subject,不带Configuration时默认使用Configuration.getConfiguration()
	public LoginContext(String name) throws LoginException;
    public LoginContext(String name, Subject subject) throws LoginException;
    public LoginContext(String name, CallbackHandler callbackHandler) throws LoginException;
    public LoginContext(String name, Subject subject, CallbackHandler callbackHandler) throws LoginException;
    public LoginContext(String name, Subject subject, CallbackHandler callbackHandler, Configuration config) throws LoginException;
    
    public void login() throws LoginException;
    public void logout() throws LoginException;
    public Subject getSubject();
}

// 用户认证的真正实现,被LoginContext回调
// LoginContext实例在调用其login()时,会先调用LoginModule.login()(如果LoginContext实例是第一次调用login(),那么还会在调用LoginModule.login()前调用LoginModule.initialize()),接着会调用LoginModule.commit()
// 当LoginModule.login()或LoginModule.commit()返回false或抛出异常时会回调LoginModule.abort()
// LoginContext实例在调用其logout()时,会调用LoginModule.logout()
public interface LoginModule {
    void initialize(Subject subject, CallbackHandler callbackHandler, Map<String,?> sharedState, Map<String,?> options); // subject与callbackHandler对应LoginContext的构造函数参数,options对应Configuration相应条目的额外参数
    boolean login() throws LoginException; // 用于认证过程,如校验用户名和密码
    boolean commit() throws LoginException;  // 一般用于为已认证的Subject赋予Principal
    boolean abort() throws LoginException; // 一般用于释放资源,如去除Subject的Principal
    boolean logout() throws LoginException; // 一般和abort实现相同,通常abort可以直接调用logout
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值