1. 程式人生 > >純JAVA實現Online Judge--3.SecurityManager安全管理器

純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的業務邏輯,強行終止(殺死)正在執行的執行緒。做到限時執行。