Java深度歷險(九)Java安全

安全性是Java應用程序的非功能性需求的重要組成部分,如同其它的非功能性需求一樣,安全性很容易被開發人員所忽略。當然,對於Java EE的開發人員來說,安全性的話題可能沒那麼陌生,用戶認證和授權可能是絕大部分Web應用都有的功能。類似Spring Security這樣的框架,也使得開發變得更加簡單。本文並不會討論Web應用的安全性,而是介紹Java安全一些底層和基本的內容。

認證

用戶認證是應用安全性的重要組成部分,其目的是確保應用的使用者具有合法的身份。 Java安全中使用術語主體(Subject)來表示訪問請求的來源。一個主體可以是任何的實體。一個主體可以有多個不同的身份標識(Principal)。比如一個應用的用戶這類主體,就可以有用戶名、身份證號碼和手機號碼等多種身份標識。除了身份標識之外,一個主體還可以有公開或是私有的安全相關的憑證(Credential),包括密碼和密鑰等。

典型的用戶認證過程是通過登錄操作來完成的。在登錄成功之後,一個主體中就具備了相應的身份標識。Java提供了一個可擴展的登錄框架,使得應用開發人員可以很容易的定製和擴展與登錄相關的邏輯。登錄的過程由LoginContext啓動。在創建LoginContext的時候需要指定一個登錄配置(Configuration)的名稱。該登錄配置中包含了登錄所需的多個LoginModule的信息。每個LoginModule實現了一種登錄方式。當調用LoginContextlogin方法的時候,所配置的每個LoginModule會被調用來執行登錄操作。如果整個登錄過程成功,則通過getSubject方法就可以獲取到包含了身份標識信息的主體。開發人員可以實現自己的LoginModule來定製不同的登錄邏輯。

每個LoginModule的登錄方式由兩個階段組成。第一個階段是在login方法的實現中。這個階段用來進行必要的身份認證,可能需要獲取用戶的輸入,以及通過數據庫、網絡操作或其它方式來完成認證。當認證成功之後,把必要的信息保存起來。如果認證失敗,則拋出相關的異常。第二階段是在commitabort法中。由於一個登錄過程可能涉及到多個LoginModuleLoginContext會根據每個LoginModule的認證結果以及相關的配置信息來確定本次登錄是否成功。LoginContext用來判斷的依據是每個LoginModule對整個登錄過程的必要性,分成必需、必要、充分和可選這四種情況。如果登錄成功,則每個LoginModulecommit方法會被調用,用來把身份標識關聯到主體上。如果登錄失敗,則LoginModule abort方法會被調用,用來清除之前保存的認證相關信息。

LoginModule進行認證的過程中,如果需要獲取用戶的輸入,可以通過CallbackHandler和對應的Callback來完成。每個Callback可以用來進行必要的數據傳遞。典型的啓動登錄的過程如下:

public Subject login() throws LoginException {    
    TextInputCallbackHandler callbackHandler = new TextInputCallbackHandler();    
    LoginContext lc = new LoginContext("SmsApp", callbackHandler);    
    lc.login();    
    return lc.getSubject();
} 


這裏的SmsApp是登錄配置的名稱,可以在配置文件中找到。該配置文件的內容也很簡單。

SmsApp {    
   security.login.SmsLoginModule required;
};


這裏聲明瞭使用security.login.SmsLoginModule這個登錄模塊,而且該模塊是必需的。配置文件可以通過啓動程序時的參數 java.security.auth.login.config來指定,或修改JVM的默認設置。下面看看SmsLoginModule的核心方法 logincommit

public boolean login() throws LoginException {
<span style="white-space:pre">		</span>TextInputCallback phoneInputCallback = new TextInputCallback(
<span style="white-space:pre">				</span>"Phonenumber: ");
<span style="white-space:pre">		</span>TextInputCallback smsInputCallback = new TextInputCallback("Code:");
<span style="white-space:pre">		</span>try {
<span style="white-space:pre">			</span>handler.handle(new Callback[] { phoneInputCallback,
<span style="white-space:pre">					</span>smsInputCallback });
<span style="white-space:pre">		</span>} catch (Exception e) {
<span style="white-space:pre">			</span>throw new LoginException(e.getMessage());
<span style="white-space:pre">		</span>}
<span style="white-space:pre">		</span>String code = smsInputCallback.getText();
<span style="white-space:pre">		</span>booleanisValid = code.length() > 3; // 此處只是簡單的進行驗證。
<span style="white-space:pre">		</span>if (isValid) {
<span style="white-space:pre">			</span>phoneNumber = phoneInputCallback.getText();
<span style="white-space:pre">		</span>}
<span style="white-space:pre">		</span>return isValid;
<span style="white-space:pre">	</span>}


<span style="white-space:pre">	</span>publicboolean commit() throws LoginException {
<span style="white-space:pre">		</span>if (phoneNumber != null) {
<span style="white-space:pre">			</span>subject.getPrincipals().add(new PhonePrincipal(phoneNumber));
<span style="white-space:pre">			</span>return true;
<span style="white-space:pre">		</span>}
<span style="white-space:pre">		</span>return false;
<span style="white-space:pre">	</span>}


  

 

這裏使用了兩個TextInputCallback來獲取用戶的輸入。當用戶輸入的編碼有效的時候,就把相關的信息記錄下來,此處是用戶的手機號碼。在commit方法中,就把該手機號碼作爲用戶的身份標識與主體關聯起來。

權限控制

在驗證了訪問請求來源的合法身份之後,另一項工作是驗證其是否具有相應的權限。權限由Permission及其子類來表示。每個權限都有一個名稱,該名稱的含義與權限類型相關。某些權限有與之對應的動作列表。比較典型的是文件操作權限FilePermission,它的名稱是文件的路徑,而它的動作列表則包括讀取、寫入和執行等。Permission類中最重要的是implies方法,它定義了權限之間的包含關係,是進行驗證的基礎。

權限控制包括管理和驗證兩個部分。管理指的是定義應用中的權限控制策略,而驗證指的則是在運行時刻根據策略來判斷某次請求是否合法。策略可以與主體關聯,也可以沒有關聯。策略由Policy來表示,JDK提供了基於文件存儲的基本實現。開發人員也可以提供自己的實現。在應用運行過程中,只可能有一個Policy處於生效的狀態。驗證部分的具體執行者是AccessController,其中的checkPermission方法用來驗證給定的權限是否被允許。在應用中執行相關的訪問請求之前,都需要調用checkPermission方法來進行驗證。如果驗證失敗的話,該方法會拋出AccessControlException異常。 JVM中內置提供了一些對訪問關鍵部分內容的訪問控制檢查,不過只有在啓動應用的時通過參數-Djava.security.manager啓用了安全管理器之後才能生效,並與策略相配合。

與訪問控制相關的另外一個概念是特權動作。特權動作只關心動作本身所要求的權限是否具備,而並不關心調用者是誰。比如一個寫入文件的特權動作,它只要求對該文件有寫入權限即可,並不關心是誰要求它執行這樣的動作。特權動作根據是否拋出受檢異常,分爲PrivilegedActionPrivilegedExceptionAction。這兩個接口都只有一個run方法用來執行相關的動作,也可以向調用者返回結果。通過AccessControllerdoPrivileged方法就可以執行特權動作。

Java安全使用了保護域的概念。每個保護域都包含一組類、身份標識和權限,其意義是在當訪問請求的來源是這些身份標識的時候,這些類的實例就自動具有給定的這些權限。保護域的權限既可以是固定,也可以根據策略來動態變化。ProtectionDomain用來表示保護域,它的兩個構造方法分別用來支持靜態和動態的權限。一般來說,應用程序通常會涉及到系統保護域和應用保護域。不少的方法調用可能會跨越多個保護域的邊界。因此,在AccessController進行訪問控制驗證的時候,需要考慮當前操作的調用上下文,主要指的是方法調用棧上不同方法所屬於的不同保護域。這個調用上下文一般是與當前線程綁定在一起的。通過AccessControllergetContext方法可以獲取到表示調用上下文的AccessControlContext對象,相當於訪問控制驗證所需的調用棧的一個快照。在有些情況下,會需要傳遞此對象以方便在其它線程中進行訪問控制驗證。

考慮下面的權限驗證代碼:

Subject subject = new Subject(); 
	ViewerPrincipal principal = new ViewerPrincipal("Alex"); 
	subject.getPrincipals().add(principal); 
	Subject.doAsPrivileged(subject, new PrivilegedAction<Object>() {         
		public Object run() {                
			new Viewer().view();                
			return null;        
			} }, null); 


 

這裏創建了一個新的Subject對象並關聯上身份標識。通常來說,這個過程是由登錄操作來完成的。通過Subject doAsPrivileged方法就可以執行一個特權動作。Viewer對象的view方法會使用AccessController來檢查是否具有相應的權限。策略配置文件的內容也比較簡單,在啓動程序的時候通過參數java.security.auth.policy指定文件路徑即可。

grant Principal security.access.ViewerPrincipal "Alex" {     permission security.access.ViewPermission "CONFIDENTIAL"; }; //這裏把名稱爲CONFIDENTIAL的ViewPermission授權給了身份標識爲Alex的主體。

 

加密、解密與簽名

構建安全的Java應用離不開加密和解密。Java的密碼框架採用了常見的服務提供者架構,以提供所需的可擴展性和互操作性。該密碼框架提供了一系列常用的服務,包括加密、數字簽名和報文摘要等。這些服務都有服務提供者接口(SPI),服務的實現者只需要實現這些接口,並註冊到密碼框架中即可。比如加密服務CipherSPI接口就是CipherSpi。每個服務都可以有不同的算法來實現。密碼框架也提供了相應的工廠方法用來獲取到服務的實例。比如想使用採用MD5算法的報文摘要服務,只需要調用MessageDigest.getInstance("MD5")即可。

加密和解密過程中並不可少的就是密鑰(Key)。加密算法一般分成對稱和非對稱兩種。對稱加密算法使用同一個密鑰進行加密和解密;而非對稱加密算法使用一對公鑰和私鑰,一個加密的時候,另外一個就用來解密。不同的加密算法,有不同的密鑰。對稱加密算法使用的是SecretKey,而非對稱加密算法則使用PublicKeyPrivateKey。與密鑰Key對應的另一個接口是KeySpec,用來描述不同算法的密鑰的具體內容。比如一個典型的使用對稱加密的方式如下:

KeyGenerator generator = KeyGenerator.getInstance("DES"); 
SecretKey key = generator.generateKey();saveFile("key.data", key.getEncoded()); 
Cipher cipher = Cipher.getInstance("DES"); cipher.init(Cipher.ENCRYPT_MODE, key); 
String text = "Hello World"; byte[] encrypted = cipher.doFinal(text.getBytes()); 
saveFile("encrypted.bin", encrypted);


 

加密的時候首先要生成一個密鑰,再由Cipher服務來完成。可以把密鑰的內容保存起來,方便傳遞給需要解密的程序。

byte[] keyData =getData("key.data");
SecretKeySpec keySpec = new SecretKeySpec(keyData, "DES");
Cipher cipher = Cipher.getInstance("DES");
cipher.init(Cipher.DECRYPT_MODE, keySpec);
byte[] data = getData("encrypted.bin");
byte[] result = cipher.doFinal(data);


解密的時候先從保存的文件中得到密鑰編碼之後的內容,再通過SecretKeySpec獲取到密鑰本身的內容,再進行解密。

報文摘要的目的在於防止信息被有意或無意的修改。通過對原始數據應用某些算法,可以得到一個校驗碼。當收到數據之後,只需要應用同樣的算法,再比較校驗碼是否一致,就可以判斷數據是否被修改過。相對原始數據來說,校驗碼長度更小,更容易進行比較。消息認證碼(MessageAuthentication Code)與報文摘要類似,不同的是計算的過程中加入了密鑰,只有掌握了密鑰的接收者才能驗證數據的完整性。

使用公鑰和私鑰就可以實現數字簽名的功能。某個發送者使用私鑰對消息進行加密,接收者使用公鑰進行解密。由於私鑰只有發送者知道,當接收者使用公鑰解密成功之後,就可以判定消息的來源肯定是特定的發送者。這就相當於發送者對消息進行了簽名。數字簽名由Signature服務提供,簽名和驗證的過程都比較直接。

Signature signature = Signature.getInstance("SHA1withDSA");
KeyPairGenerator keyGenerator = KeyPairGenerator.getInstance("DSA");
KeyPair keyPair = keyGenerator.generateKeyPair();
PrivateKey privateKey = keyPair.getPrivate();
signature.initSign(privateKey);
byte[] data = "HelloWorld".getBytes();
signature.update(data);
byte[] signatureData = signature.sign(); //得到簽名
PublicKey publicKey = keyPair.getPublic();
signature.initVerify(publicKey);
signature.update(data);
boolean result = signature.verify(signatureData); //進行驗證


驗證數字簽名使用的公鑰可以通過文件或證書的方式來進行發佈。

安全套接字連接

在各種數據傳輸方式中,網絡傳輸目前使用較廣,但是安全隱患也更多。安全套接字連接指的是對套接字連接進行加密。加密的時候可以選擇對稱加密算法。但是如何在發送者和接收者之間安全的共享密鑰,是個很麻煩的問題。如果再用加密算法來加密密鑰,則成爲了一個循環問題。非對稱加密算法則適合於這種情況。私鑰自己保管,公鑰則公開出去。發送數據的時候,用私鑰加密,接收者用公開的公鑰解密;接收數據的時候,則正好相反。這種做法解決了共享密鑰的問題,但是另外的一個問題是如何確保接收者所得到的公鑰確實來自所聲明的發送者,而不是僞造的。爲此,又引入了證書的概念。證書中包含了身份標識和對應的公鑰。證書由用戶所信任的機構簽發,並用該機構的私鑰來加密。在有些情況下,某個證書籤發機構的真實性會需要由另外一個機構的證書來證明。通過這種證明關係,會形成一個證書的鏈條。而鏈條的根則是公認的值得信任的機構。只有當證書鏈條上的所有證書都被信任的時候,才能信任證書中所給出的公鑰。

日常開發中比較常接觸的就是HTTPS即安全的HTTP連接。大部分用Java程序訪問採用HTTPS網站時出現的錯誤都與證書鏈條相關。有些網站採用的不是由正規安全機構簽發的證書,或是證書已經過期。如果必須訪問這樣的HTTPS網站的話,可以提供自己的套接字工廠和主機名驗證類來繞過去。另外一種做法是通過keytool工具把證書導入到系統的信任證書庫之中。

URL url = new URL("https://localhost:8443");
SSLContext context = SSLContext.getInstance("TLS");
context.init(new KeyManager[] {}, new TrustManager[] {new MyTrustManager()}, new SecureRandom());HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
connection.setSSLSocketFactory(context.getSocketFactory());
connection.setHostnameVerifier(new MyHostnameVerifier());


這裏的MyTrustManager實現了X509TrustManager接口,但是所有方法都是默認實現。而MyHostnameVerifier實現了HostnameVerifier接口,其中的verify方法總是返回true

 

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