簡介
程序員肯定是不缺對象的,因為隨時都可以構建一個,對象多了肯定會出現點安全問題,一起來看看在java的對象構建中怎麼保證對象的安全性吧。
構造函數的異常
考慮下面的一個例子:
public class SensitiveOperation {
public SensitiveOperation(){
if(!doSecurityCheck()){
throw new SecurityException("Security check failed!");
}
}
//Security check return false
private boolean doSecurityCheck(){
return false;
}
public void storeMoney(){
System.out.println("Store 1000000 RMB!");
}
}
上面的例子中,我們在構造函數中做了一個securityCheck,因為這個securityCheck返回的值是false,所以會拋出SecurityException。
看下調用的例子:
public static void main(String[] args) {
SensitiveOperation sensitiveOperation = new SensitiveOperation();
sensitiveOperation.storeMoney();
}
這個調用會拋出下面的異常:
Exception in thread "main" java.lang.SecurityException: Security check failed!
at com.flydean.SensitiveOperation.<init>(SensitiveOperation.java:11)
at com.flydean.SensitiveUsage.main(SensitiveUsage.java:10)
那麼問題來了,上面的這個class是不是安全的呢?
Finalizer Attack
上面的class不是final的,所以我們可以構造一個class去繼承它。然後考慮這樣一個問題,當構造函數拋出異常之後,會執行什麼操作呢?
如果該對象已經被構建了,那麼這個對象在GC的時候需要執行finalize方法。那麼我們是不是可以在finalize方法中繞過安全檢查呢?
看下面的例子:
public class SensitiveOperationFinalizer extends SensitiveOperation{
public SensitiveOperationFinalizer(){
}
@Override
protected void finalize() {
System.out.println("We can still do store Money action!");
this.storeMoney();
System.exit(0);
}
}
上的例子中,我們繼承了SensitiveOperation,並且實現了finalize方法,在finalize中,我們調用了storeMoney。看下運行的代碼:
public void testFinalizer() throws InterruptedException {
try {
SensitiveOperation sensitiveOperation = new SensitiveOperationFinalizer();
sensitiveOperation.storeMoney();
}catch (Exception e){
System.out.println(e.getMessage());
}
System.gc();
Thread.sleep(10000);
}
運行結果:
Security check failed!
We can still do store Money action!
Store 1000000 RMB!
可以看到,雖然我們構造函數拋出了異常,但是storeMoney的操作還是被執行了!
這個操作就叫做Finalizer Attack。
解決Finalizer Attack
怎麼解決這個構造函數拋出異常的問題呢?這裡給大家介紹幾種解決方法。
使用final class
如果使用final class,那麼類是不能夠被繼承的,問題自然就解決了。
public final class SensitiveOperationFinal {
public SensitiveOperationFinal(){
if(!doSecurityCheck()){
throw new SecurityException("Security check failed!");
}
}
//Security check return false
private boolean doSecurityCheck(){
return false;
}
public void storeMoney(){
System.out.println("Store 1000000 RMB!");
}
}
使用final finalize方法
因為子類想要重寫finalize方法,如果我們的父類中finalize方法定義為final,也可以解決這個問題。
public final class SensitiveOperationFinal {
public SensitiveOperationFinal(){
if(!doSecurityCheck()){
throw new SecurityException("Security check failed!");
}
}
//Security check return false
private boolean doSecurityCheck(){
return false;
}
public void storeMoney(){
System.out.println("Store 1000000 RMB!");
}
final protected void finalize() {
}
}
使用flag變量
我們可以在對象構建完畢的時候設置一個flag變量,然後在每次安全操作的時候都去判斷一下這個flag變量,這樣也可以避免之前提到的問題:
public class SensitiveOperationFlag {
private volatile boolean flag= false;
public SensitiveOperationFlag(){
if(!doSecurityCheck()){
throw new SecurityException("Security check failed!");
}
flag=true;
}
//Security check return false
private boolean doSecurityCheck(){
return false;
}
public void storeMoney(){
if(!flag){
System.out.println("Object is not initiated yet!");
return;
}
System.out.println("Store 1000000 RMB!");
}
}
注意,這裡flag需要設置為volatile,只有這樣才能保證構造函數在flag設置之前執行。也就是說需要保證happens-before特性。
使用this或者super
在JDK6或者更高版本中,如果對象的構造函數在java.lang.Object構造函數退出之前引發異常,則JVM將不會執行該對象的finalize方法。
因為Java確保java.lang.Object構造函數在任何構造函數的第一條語句之上或之前執行。如果構造函數中的第一個語句是對超類的構造函數或同一個類中的另一個構造函數的調用,則java.lang.Object構造函數將在該調用中的某個位置執行。否則,Java將在該構造函數的代碼中的任何一個執行之前執行超類的默認構造函數,並且將通過隱式調用執行java.lang.Object構造函數。
也就是說如果異常發生在構造函數中的第一條this或者super中的時候,JVM將不會調用對象的finalize方法:
public class SensitiveOperationThis {
public SensitiveOperationThis(){
this(doSecurityCheck());
}
private SensitiveOperationThis(boolean secure) {
}
//Security check return false
private static boolean doSecurityCheck(){
throw new SecurityException("Security check failed!");
}
public void storeMoney(){
System.out.println("Store 1000000 RMB!");
}
}
本文的例子:
learn-java-base-9-to-20/tree/master/security
本文已收錄於 http://www.flydean.com/java-security-code-line-object/
最通俗的解讀,最深刻的乾貨,最簡潔的教程,眾多你不知道的小技巧等你來發現!
歡迎關注我的公眾號:「程序那些事」,懂技術,更懂你!