Java安全机制之一——SecurityManager和AccessController
前言:
在看socket相關(guān)代碼的時候,AbstractPlainSocketImpl中的一段代碼吸引了我,其實之前見過很多次類似的代碼,但一直不想去看,只知道肯定和權(quán)限什么的相關(guān),這次既然又碰到了就研究一下,畢竟也不能對java基本代碼一無所知。
static {
java.security.AccessController.doPrivileged(
new java.security.PrivilegedAction<Void>() {
public Void run() {
System.loadLibrary("net");
return null;
}
});
}
一些概念:
在jdk1.0的時代,applet依然是前端的一種可用的技術(shù)方案,比如可以嵌入在網(wǎng)頁里運行。那個時候jdk的設(shè)計者們認為本地代碼是安全的、遠端代碼是有風(fēng)險的,而applet就是屬于遠端代碼。因此,為了保證用戶主機的安全和隱私,設(shè)計者參考了沙箱的思想,依托于當(dāng)時jdk的體量很小,使用SecurityManager來分隔本地代碼和遠程代碼,一個有權(quán)限,一個沒有權(quán)限。
當(dāng)時還出現(xiàn)了簽名相關(guān)的機制(本文不關(guān)心,所以沒做了解),隨著java發(fā)展,1.1的時候出現(xiàn)了JAVABEAN、JDBC、反射等新概念,于是有了更多的新權(quán)限。設(shè)計者發(fā)現(xiàn)完全授予本地代碼所有權(quán)限變得不合理,在1.2的時候重構(gòu)了SecurityManager,變成了現(xiàn)在這樣以最小粒度控制權(quán)限。這個時候的SecurityManager有兩個功能,一是防御遠程代碼、二是防御本地代碼的漏洞。
不知道是什么時候起,安全機制引入了域(ProtectDomain)的概念,也可以視作將一個大沙箱拆分為多個小沙箱。一個域?qū)?yīng)一個沙箱,不同的代碼(Codesource)被劃分到不同域中,不同的域有著不同的權(quán)限(Permission),就像下圖一樣。同時可以給不同的域配置不同的權(quán)限,靜態(tài)和動態(tài)均可,這個配置被稱為策略(Policy)。
注意!
在JDK20和JDK21的security-guide中都提到了,和SecurityManager與之相關(guān)的api已被棄用,并將在未來的版本中刪除。SecurityManager沒有替代者。有關(guān)討論和備選方案,請參閱JEP 411: Deprecate the Security Manager for Removal。
AccessController
AccessController主要有兩個功能,對應(yīng)的核心方法也是兩類
checkPermission(校驗是否存在權(quán)限)
public static void checkPermission(Permission perm)
throws AccessControlException
{
AccessControlContext stack = getStackAccessControlContext();
// if context is null, we had privileged system code on the stack.
//...其他獲取context方法
AccessControlContext acc = stack.optimize();
acc.checkPermission(perm);
}
調(diào)用該方法時,一般會new一個期望的權(quán)限,然后作為入?yún)魅隿heckPermission方法。
FilePermission perm = new FilePermission("C:\\Users\\Administrator\\Desktop\\liveController.txt", "read");
AccessController.checkPermission(perm);
注意,校驗權(quán)限的時候會校驗調(diào)用鏈路徑上所有類的權(quán)限;假如調(diào)用鏈?zhǔn)菑膇開始,一直調(diào)用到m,校驗邏輯如下
for (int i = m; i > 0; i--) {
if (caller i's domain does not have the permission)
throw AccessControlException
else if (caller i is marked as privileged) {
if (a context was specified in the call to doPrivileged)
context.checkPermission(permission)
if (limited permissions were specified in the call to doPrivileged) {
for (each limited permission) {
if (the limited permission implies the requested permission)
return;
}
} else
return;
}
}
代碼執(zhí)行的時候,每一次方法的調(diào)用都代表著一次入棧,而權(quán)限校驗的時候則正好是從棧頂開始,依次判斷每個棧幀是否具有權(quán)限,一直到棧底。
doPrivileged(臨時授權(quán))
public static native <T> T doPrivileged(PrivilegedAction<T> action);
這個方法的功能是將當(dāng)前類所擁有的權(quán)限,能且僅能臨時賦予其上游調(diào)用方。
在這個場景下,必然存在多個域,且只有某些域擁有權(quán)限A,但是其他域并沒有這個權(quán)限。在java語言中很容易出現(xiàn)這個情況,比如我們調(diào)用一些第三方j(luò)ar包的方法,三方j(luò)ar包還能調(diào)用別的三方j(luò)ar包,這種場景很有可能只有最底層的方法所對應(yīng)的域擁有權(quán)限。此時為了方法的成功,就可以使用該方法。
使用的時候就是將代碼邏輯放入AccessController.doPrivileged中即可,如下述代碼一般。
//項目B,會打成security-demo.jar
public class PermissionDemo {
/**
* 使用特權(quán)訪問機制
* @param file
*/
public void runWithOutPermission(String file){
AccessController.doPrivileged((PrivilegedAction<String>) () -> {
//hutool的FileUtil
String s = FileUtil.readString(file, "utf-8");
System.out.println(s);
return s;
});
}
}
//項目A,引入security-demo.jar
public class Aperson {
public static void main(String[] args) {
new PermissionDemo().runWithOutPermission("C:\\Users\\Administrator\\Desktop\\test.txt");
}
}
這里需要注意的是,AccessController.doPrivileged所在的當(dāng)前類也需要擁有權(quán)限。以這個例子為例,文件讀寫是在hutool的FileUtil中執(zhí)行,hutool對應(yīng)的是域C;PermissionDemo對應(yīng)的是域B,且會將自身權(quán)限向上傳遞;而Aperson對應(yīng)的是域A。這個例子中,想要Aperson執(zhí)行成功,必須是域C和域B都擁有test.txt的read權(quán)限。
對應(yīng)的policy如下
grant codeBase "file:/C:/Users/Administrator/.m2/repository/cn/hutool/hutool-all/5.7.11/-"{
permission java.io.FilePermission "C:\\Users\\Administrator\\Desktop\\*", "read";
};
grant codeBase "file:/C:/Users/Administrator/.m2/repository/xxx/xxx/security-demo/-"{
permission java.io.FilePermission "C:\\Users\\Administrator\\Desktop\\*", "read";
};
從棧幀的角度來看的話,判斷到doPrivilege對應(yīng)的那層之后,校驗就直接返回了,不校驗下面層是否存在權(quán)限。
ProtectDomain
protectDomain類由codeSource和permission構(gòu)成
CodeSource
類的來源,一般為jar包路徑或者classpath路徑(target/classes)
因為所有類在通過ClassLoader引入的,所以ClassLoader知道類的基本信息,在defineClass時,將CodeSource和Permission進行了綁定。同理,由于類必須通過ClassLoader加載,對于使用自定義ClassLoader加載的類,就只有那個類加載器知道對應(yīng)的CodeSource和permission。因此,不同的類加載器本身就屬于不同的域。
Permission
Java抽象出的頂層的類,核心方法是implies,該方法用來判斷當(dāng)前線程是否隱含指定權(quán)限,由各自的子類實現(xiàn)。子類實現(xiàn)過多,這里就不列舉了。
PermissionCollection本質(zhì)是個list,里面是某一類權(quán)限的多個實例,比如文件夾A-讀權(quán)限,文件夾B-寫權(quán)限,文件夾C-讀寫權(quán)限。
Permissions核心是一個map,key是Permissoin子類,value是PermissionCollection
SecurityManager
SecurityManage里有一堆check方法,調(diào)用的是AccessController.checkPermission方法,入?yún)⒕褪荘ermission各個子類的實例化。
開啟方式:
隱性:啟動時添加-Djava.security.manager
顯性:System.setSecurityManager
public class NoShowTest {
static class CustomManager extends SecurityManager{
@Override
public void checkRead(String file) {
throw new AccessControlException("無權(quán)限訪問");
}
}
public static void main(String[] args) {
System.setSecurityManager(new CustomManager());
System.getSecurityManager().checkRead("C:\\Users\\Administrator\\Desktop\\liveController.txt");
}
}
Policy
啟動時通過 -Djava.security.policy=xxxx\custom.policy,如果沒有指定,則默認使用jdk路徑下\jre\lib\security\java.policy
參考:
Java安全:SecurityManager與AccessController - 掘金
Java沙箱機制的實現(xiàn)——安全管理器、訪問控制器 - 掘金
第21章-再談類的加載器
https://openjdk.org/jeps/411
https://docs.oracle.com/en/java/javase/20/security/java-security-overview1.html#GUID-BBEC2DC8-BA00-42B1-B52A-A49488FCF8FE
AccessController.doPrivileged - 山河已無恙 - 博客園
總結(jié)
以上是生活随笔為你收集整理的Java安全机制之一——SecurityManager和AccessController的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Go 接口:nil接口为什么不等于nil
- 下一篇: Python 数据库应用教程:安装 My