純JAVA實現Online Judge--3.SecurityManager安全管理器
前言
上一篇的博文中,我們通過自己編譯使用者提交的程式碼,再通過自定製的類載入器將編譯出來後的class資訊載入進JVM中,最後再通過反射呼叫使用者程式碼的main,實現了執行使用者程式碼的目的。具體如何執行使用者的程式碼的部分,將會在後面的部落格(多執行緒跑題)中展開述說,裡面將介紹我如何利用多執行緒的方式,同時對同一份使用者程式碼跑多份測試用例,提升效率的同時,如何解決多執行緒中遇到的衝突問題。
現在首先要解決的是安全問題,既然我們執行使用者提交的程式碼了,這就存在一個風險,如果使用者想惡意攻擊我們的伺服器怎麼辦?比如使用者在提交的程式碼中,開啟遠端連線連上遠端的伺服器,開啟網路連線不斷下載東西,再或者不斷新建檔案並寫入大量資料,更有甚者直接呼叫System.exit(status);退出JAVA虛擬機器等等。
因為我們直接運行了未知的程式碼,我們就必須保證我們的伺服器安全,不然我們的系統都還沒判題幾個就被別人搞垮了。我們利用JAVA實現這個安全管理還是挺容易的,因為JAVA提供了SecurityManager安全管理器。每一個JAVA程式在啟動的時候,其實內部就設定了一個預設SecurityManager,我們這裡只需要繼承它,並複寫相應的方法即可。
SecurityManager安全管理器
首先,我們先貼上我們安全管理器的程式碼,然後再稍微分析一下:
package cn.superman.sandbox.core.securityManager; import java.io.FilePermission; import java.lang.reflect.ReflectPermission; import java.security.Permission; import java.security.SecurityPermission; import java.util.PropertyPermission; import java.util.logging.LoggingPermission; import cn.superman.sandbox.constant.ConstantParameter; public class SandboxSecurityManager extends SecurityManager { /** * 防止有人非法退出虛擬機器 */ @Override public void checkExit(int status) { if (status != ConstantParameter.EXIT_VALUE) { throw new RuntimeException("非法退出,不允許退出虛擬機器"); } super.checkExit(status); } @Override public void checkPermission(Permission perm) { conformPermissionToSandbox(perm); } @Override public void checkPermission(Permission perm, Object context) { conformPermissionToSandbox(perm); } /** * 只給與必要的許可權(比如讀取,獲取某些資訊等),避擴音交者進行非法操作。 * * @param perm */ private void conformPermissionToSandbox(Permission perm) { if (perm instanceof SecurityPermission) { if (perm.getName().startsWith("getProperty")) { return; } } else if (perm instanceof PropertyPermission) { if (perm.getActions().equals("read")) { return; } } else if (perm instanceof FilePermission) { if (perm.getActions().equals("read")) { return; } } else if (perm instanceof RuntimePermission || perm instanceof ReflectPermission || perm instanceof LoggingPermission) { return; } throw new SecurityException(perm.toString() + "無法使用該許可權"); } }
設定時機
我們在我們的沙箱初始化完成後,並在接收外界資料之前,對其進行設定。至於為什麼是這個時機呢?因為我們的沙箱在初始化時就需要用到一定我們限制的許可權,比如開啟socket等,因此我們要在初始化完成後,因為我們要開始對外服務了(可能需要執行使用者的程式碼了),這個時候就要保證我們沙箱的安全了,所以我們要在開始接收外界資料之前,設定這個安全管理器。
設定的程式碼很簡單:
System.setSecurityManager(new SandboxSecurityManager());
重點內容
其實從程式碼中可以看出,我們重點做的就是複寫checkPermission和checkExit。
複寫checkExit是為了防止使用者惡意呼叫System.exit(status);程式碼退出我們的虛擬機器,當我們發現使用者惡意呼叫時,但是他們傳過來的數值不對時,我們就直接拒絕他這個請求,並且丟擲異常通知上層認定這份程式碼是非法的。至於正確的退出值是多少,我們可以在程式初始化時,通過隨機函式自動生成一個等方式來確立。。
複寫checkPermission函式主要是為了,動態捕獲使用者程式碼在執行時請求了哪些許可權,對於合法符合業務邏輯的許可權,我們直接返回即可,給予放行(如反射,讀取檔案等)。對於非法的許可權,我們通過丟擲異常的方式,通知上層該使用者程式碼有違法操作,直接判定該份程式碼為非法。
許可權有很多種,大家可以通過檢視JDK文件或者其他博文的方式深入理解,下面給出一個許可權控制的大概簡圖:
預告
本篇博文介紹的內容,還不能完全的防止使用者提交的惡意程式碼,因為使用者還有可能提交一些死迴圈的程式碼,當我們沙箱執行這樣的程式碼時,就會有一個執行緒陷入死迴圈,導致伺服器的CPU資源被佔據不放。當然,排除死迴圈方式,使用者執行的程式碼也有可能會超時,這個時候我們除了判定使用者的程式碼執行超時之外,我們還要終止執行該執行緒,儘快釋放資源(因為已經得出結果了嘛)。
因此,在下篇博文中,我將會介紹我如何結合OJ的業務邏輯,強行終止(殺死)正在執行的執行緒。做到限時執行。