Java安全机制之一——SecurityManager和AccessController
前言:
在看socket相關代碼的時候,AbstractPlainSocketImpl中的一段代碼吸引了我,其實之前見過很多次類似的代碼,但一直不想去看,只知道肯定和權限什么的相關,這次既然又碰到了就研究一下,畢竟也不能對java基本代碼一無所知。
static {
    java.security.AccessController.doPrivileged(
        new java.security.PrivilegedAction<Void>() {
            public Void run() {
                System.loadLibrary("net");
                return null;
            }
        });
}
一些概念:
在jdk1.0的時代,applet依然是前端的一種可用的技術方案,比如可以嵌入在網頁里運行。那個時候jdk的設計者們認為本地代碼是安全的、遠端代碼是有風險的,而applet就是屬于遠端代碼。因此,為了保證用戶主機的安全和隱私,設計者參考了沙箱的思想,依托于當時jdk的體量很小,使用SecurityManager來分隔本地代碼和遠程代碼,一個有權限,一個沒有權限。
當時還出現了簽名相關的機制(本文不關心,所以沒做了解),隨著java發展,1.1的時候出現了JAVABEAN、JDBC、反射等新概念,于是有了更多的新權限。設計者發現完全授予本地代碼所有權限變得不合理,在1.2的時候重構了SecurityManager,變成了現在這樣以最小粒度控制權限。這個時候的SecurityManager有兩個功能,一是防御遠程代碼、二是防御本地代碼的漏洞。
不知道是什么時候起,安全機制引入了域(ProtectDomain)的概念,也可以視作將一個大沙箱拆分為多個小沙箱。一個域對應一個沙箱,不同的代碼(Codesource)被劃分到不同域中,不同的域有著不同的權限(Permission),就像下圖一樣。同時可以給不同的域配置不同的權限,靜態和動態均可,這個配置被稱為策略(Policy)。
注意!
在JDK20和JDK21的security-guide中都提到了,和SecurityManager與之相關的api已被棄用,并將在未來的版本中刪除。SecurityManager沒有替代者。有關討論和備選方案,請參閱JEP 411: Deprecate the Security Manager for Removal。
AccessController
AccessController主要有兩個功能,對應的核心方法也是兩類
checkPermission(校驗是否存在權限)
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);
}
調用該方法時,一般會new一個期望的權限,然后作為入參傳入checkPermission方法。
FilePermission perm = new FilePermission("C:\\Users\\Administrator\\Desktop\\liveController.txt", "read");
AccessController.checkPermission(perm);
注意,校驗權限的時候會校驗調用鏈路徑上所有類的權限;假如調用鏈是從i開始,一直調用到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;
     }
 }
代碼執行的時候,每一次方法的調用都代表著一次入棧,而權限校驗的時候則正好是從棧頂開始,依次判斷每個棧幀是否具有權限,一直到棧底。
doPrivileged(臨時授權)
public static native <T> T doPrivileged(PrivilegedAction<T> action);
這個方法的功能是將當前類所擁有的權限,能且僅能臨時賦予其上游調用方。
在這個場景下,必然存在多個域,且只有某些域擁有權限A,但是其他域并沒有這個權限。在java語言中很容易出現這個情況,比如我們調用一些第三方jar包的方法,三方jar包還能調用別的三方jar包,這種場景很有可能只有最底層的方法所對應的域擁有權限。此時為了方法的成功,就可以使用該方法。
使用的時候就是將代碼邏輯放入AccessController.doPrivileged中即可,如下述代碼一般。
//項目B,會打成security-demo.jar
public class PermissionDemo {
    /**
     * 使用特權訪問機制
     * @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所在的當前類也需要擁有權限。以這個例子為例,文件讀寫是在hutool的FileUtil中執行,hutool對應的是域C;PermissionDemo對應的是域B,且會將自身權限向上傳遞;而Aperson對應的是域A。這個例子中,想要Aperson執行成功,必須是域C和域B都擁有test.txt的read權限。
對應的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對應的那層之后,校驗就直接返回了,不校驗下面層是否存在權限。
ProtectDomain
protectDomain類由codeSource和permission構成
CodeSource
類的來源,一般為jar包路徑或者classpath路徑(target/classes)
因為所有類在通過ClassLoader引入的,所以ClassLoader知道類的基本信息,在defineClass時,將CodeSource和Permission進行了綁定。同理,由于類必須通過ClassLoader加載,對于使用自定義ClassLoader加載的類,就只有那個類加載器知道對應的CodeSource和permission。因此,不同的類加載器本身就屬于不同的域。
Permission
Java抽象出的頂層的類,核心方法是implies,該方法用來判斷當前線程是否隱含指定權限,由各自的子類實現。子類實現過多,這里就不列舉了。
PermissionCollection本質是個list,里面是某一類權限的多個實例,比如文件夾A-讀權限,文件夾B-寫權限,文件夾C-讀寫權限。
Permissions核心是一個map,key是Permissoin子類,value是PermissionCollection
SecurityManager
SecurityManage里有一堆check方法,調用的是AccessController.checkPermission方法,入參就是Permission各個子類的實例化。
開啟方式:
隱性:啟動時添加-Djava.security.manager
顯性:System.setSecurityManager
public class NoShowTest {
     static class CustomManager extends SecurityManager{
         @Override
         public void checkRead(String file) {
             throw new AccessControlException("無權限訪問");
         }
     }
    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沙箱機制的實現——安全管理器、訪問控制器 - 掘金
第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 - 山河已無恙 - 博客園
總結
以上是生活随笔為你收集整理的Java安全机制之一——SecurityManager和AccessController的全部內容,希望文章能夠幫你解決所遇到的問題。
                            
                        - 上一篇: Go 接口:nil接口为什么不等于nil
 - 下一篇: Python 数据库应用教程:安装 My