原文地址:http://www.infoq.com/cn/articles/cf-java-securityhtml
安全性是Java應用程序的非功能性需求的重要組成部分,如同其它的非功能性需求同樣,安全性很容易被開發人員所忽略。固然,對於Java EE的開發人員來講,安全性的話題可能沒那麼陌生,用戶認證和受權多是絕大部分Web應用都有的功能。相似Spring Security這樣的框架,也使得開發變得更加簡單。本文並不會討論Web應用的安全性,而是介紹Java安全一些底層和基本的內容。java
用戶認證是應用安全性的重要組成部分,其目的是確保應用的使用者具備合法的身份。 Java安全中使用術語主體(Subject)來表示訪問請求的來源。一個主體能夠是任何的實體。一個主體能夠有多個不一樣的身份標識(Principal)。好比一個應用的用戶這類主體,就能夠有用戶名、身份證號碼和手機號碼等多種身份標識。除了身份標識以外,一個主體還能夠有公開或是私有的安全相關的憑證(Credential),包括密碼和密鑰等。算法
典型的用戶認證過程是經過登陸操做來完成的。在登陸成功以後,一個主體中就具有了相應的身份標識。Java提供了一個可擴展的登陸框架,使得應用開發人員能夠很容易的定製和擴展與登陸相關的邏輯。登陸的過程由LoginContext啓動。在建立LoginContext的時候須要指定一個登陸配置(Configuration)的名稱。該登陸配置中包含了登陸所需的多個LoginModule的信息。每一個LoginModule實現了一種登陸方式。當調用LoginContext的login方法的時候,所配置的每一個LoginModule會被調用來執行登陸操做。若是整個登陸過程成功,則經過getSubject方法就能夠獲取到包含了身份標識信息的主體。開發人員能夠實現本身的LoginModule來定製不一樣的登陸邏輯。spring
每一個LoginModule的登陸方式由兩個階段組成。第一個階段是在login方法的實現中。這個階段用來進行必要的身份認證,可能須要獲取用戶的輸入,以及經過數據庫、網絡操做或其它方式來完成認證。當認證成功以後,把必要的信息保存起來。若是認證失敗,則拋出相關的異常。第二階段是在commit或abort方法中。因爲一個登陸過程可能涉及到多個LoginModule。LoginContext會根據每一個LoginModule的認證結果以及相關的配置信息來肯定本次登陸是否成功。LoginContext用來判斷的依據是每一個LoginModule對整個登陸過程的必要性,分紅必需、必要、充分和可選這四種狀況。若是登陸成功,則每一個LoginModule的commit方法會被調用,用來把身份標識關聯到主體上。若是登陸失敗,則LoginModule 的abort方法會被調用,用來清除以前保存的認證相關信息。數據庫
在LoginModule進行認證的過程當中,若是須要獲取用戶的輸入,能夠經過CallbackHandler和對應的Callback來完成。每一個Callback能夠用來進行必要的數據傳遞。典型的啓動登陸的過程以下:api
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的核心方法login和commit。網絡
public boolean login() throws LoginException { TextInputCallback phoneInputCallback = new TextInputCallback("Phone number: "); TextInputCallback smsInputCallback = new TextInputCallback("Code: "); try { handler.handle(new Callback[] {phoneInputCallback, smsInputCallback}); } catch (Exception e) { throw new LoginException(e.getMessage()); } String code = smsInputCallback.getText(); boolean isValid = code.length() > 3; //此處只是簡單的進行驗證。 if (isValid) { phoneNumber = phoneInputCallback.getText(); } return isValid; } public boolean commit() throws LoginException { if (phoneNumber != null) { subject.getPrincipals().add(new PhonePrincipal(phoneNumber)); return true; } return false; }
這裏使用了兩個TextInputCallback來獲取用戶的輸入。當用戶輸入的編碼有效的時候,就把相關的信息記錄下來,此處是用戶的手機號碼。在commit方法中,就把該手機號碼做爲用戶的身份標識與主體關聯起來。架構
在驗證了訪問請求來源的合法身份以後,另外一項工做是驗證其是否具備相應的權限。權限由Permission及其子類來表示。每一個權限都有一個名稱,該名稱的含義與權限類型相關。某些權限有與之對應的動做列表。比較典型的是文件操做權限FilePermission,它的名稱是文件的路徑,而它的動做列表則包括讀取、寫入和執行等。Permission類中最重要的是implies方法,它定義了權限之間的包含關係,是進行驗證的基礎。oracle
權限控制包括管理和驗證兩個部分。管理指的是定義應用中的權限控制策略,而驗證指的則是在運行時刻根據策略來判斷某次請求是否合法。策略能夠與主體關聯,也能夠沒有關聯。策略由Policy來表示,JDK提供了基於文件存儲的基本實現。開發人員也能夠提供本身的實現。在應用運行過程當中,只可能有一個Policy處於生效的狀態。驗證部分的具體執行者是AccessController,其中的checkPermission方法用來驗證給定的權限是否被容許。在應用中執行相關的訪問請求以前,都須要調用checkPermission方法來進行驗證。若是驗證失敗的話,該方法會拋出AccessControlException異常。 JVM中內置提供了一些對訪問關鍵部份內容的訪問控制檢查,不過只有在啓動應用的時經過參數-Djava.security.manager啓用了安全管理器以後才能生效,並與策略相配合。
與訪問控制相關的另一個概念是特權動做。特權動做只關心動做自己所要求的權限是否具有,而並不關心調用者是誰。好比一個寫入文件的特權動做,它只要求對該文件有寫入權限便可,並不關心是誰要求它執行這樣的動做。特權動做根據是否拋出受檢異常,分爲PrivilegedAction和PrivilegedExceptionAction。這兩個接口都只有一個run方法用來執行相關的動做,也能夠向調用者返回結果。經過AccessController的doPrivileged方法就能夠執行特權動做。
Java安全使用了保護域的概念。每一個保護域都包含一組類、身份標識和權限,其意義是在當訪問請求的來源是這些身份標識的時候,這些類的實例就自動具備給定的這些權限。保護域的權限既能夠是固定,也能夠根據策略來動態變化。ProtectionDomain類用來表示保護域,它的兩個構造方法分別用來支持靜態和動態的權限。通常來講,應用程序一般會涉及到系統保護域和應用保護域。很多的方法調用可能會跨越多個保護域的邊界。所以,在AccessController進行訪問控制驗證的時候,須要考慮當前操做的調用上下文,主要指的是方法調用棧上不一樣方法所屬於的不一樣保護域。這個調用上下文通常是與當前線程綁定在一塊兒的。經過AccessController的getContext方法能夠獲取到表示調用上下文的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),服務的實現者只須要實現這些接口,並註冊到密碼框架中便可。好比加密服務Cipher的SPI接口就是CipherSpi。每一個服務均可以有不一樣的算法來實現。密碼框架也提供了相應的工廠方法用來獲取到服務的實例。好比想使用採用MD5算法的報文摘要服務,只須要調用MessageDigest.getInstance("MD5")便可。
加密和解密過程當中並不可少的就是密鑰(Key)。加密算法通常分紅對稱和非對稱兩種。對稱加密算法使用同一個密鑰進行加密和解密;而非對稱加密算法使用一對公鑰和私鑰,一個加密的時候,另一個就用來解密。不一樣的加密算法,有不一樣的密鑰。對稱加密算法使用的是SecretKey,而非對稱加密算法則使用PublicKey和PrivateKey。與密鑰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獲取到密鑰自己的內容,再進行解密。
報文摘要的目的在於防止信息被有意或無心的修改。經過對原始數據應用某些算法,能夠獲得一個校驗碼。當收到數據以後,只須要應用一樣的算法,再比較校驗碼是否一致,就能夠判斷數據是否被修改過。相對原始數據來講,校驗碼長度更小,更容易進行比較。消息認證碼(Message Authentication 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 = "Hello World".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。