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
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章