Java:SecurityManager 安全管理器SecurityPermission的使用

关于SecurityManager的细节本文章不做介绍,主要聚集在场景搭建以及核心流程的代码跟踪,理解其设计思路。

场景搭建

主要验证System.getSecurityManager().checkSecurityAccess(“a.b.c”)的三个场景:

  • 与MainClass在同一个module
  • 与MainClass在不同的module
  • 在另一个jar包中

分别新增A、B、C、SecurityManagerTest四个类:

ClassPositionDescribe
Amodule: learn-main与Main class SecurityManagerTest在同一个module
Bmodule: learn-module在单独一个module
Cjar: com.saleson:personal:1.0-SNAPSHOT.jar在jar包中
SecurityManagerTestmodule: learn-mainmain class

A、B、C三个类的代码是一样的,仅是输出的日志为了区分会有调整,代码如下:

package com.saleson.learn.java.security;

import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.Objects;

/**
 * @author saleson
 * @date 2022-04-07 13:47
 */
public class A {

    public void print() {
        print(true);
    }

    public void print(boolean direct) {
        if (direct) {
            _print();
        } else {
            AccessController.doPrivileged(new PrivilegedAction() {

                @Override
                public Object run() {
                    _print();
                    return null;
                }
            });
        }
    }
    
    private void _print() {
        SecurityManager securityManager = System.getSecurityManager();
        if (Objects.nonNull(securityManager)) {
            securityManager.checkSecurityAccess("a.b.c");
        }
        System.out.println(this.getClass().getSimpleName() + ".print");
    }
}

security.policy内容如下:

# grant1: main classess所在module的权限配置
grant codeBase "file:/Users/saleson/IdeaProjects/learn/learn-main/target/classess" {
    permission java.security.SecurityPermission "a.b.*";
};

# grant2: 另一个module的权限配置
grant codeBase "file:/Users/saleson/IdeaProjects/learn/learn-module/target/classess" {
    permission java.security.SecurityPermission "a.b.*";
};

# grant3: jar的权限配置
grant codeBase "file:///Users/saleson/m2/repository/com/saleson/personal/1.0-SNAPSHOT/personal-1.0-SNAPSHOT.jar" {
    permission java.security.SecurityPermission "a.b.*";
};

# idea debug 
grant {
  permission java.io.FilePermission "/Users/saleson/Library/Caches/IntelliJIdea2019.1/captureAgent/debugger-agent-storage.jar", "read";
  permission java.io.FilePermission "*", "read,write";
  permission java.util.PropertyPermission "*", "read,write";
};

SecurityManagerTest类代码:

package com.saleson.learn.java.security;

import com.saleson.learn.module.java.security.B;

/**
 * @author saleson
 * @date 2022-04-02 20:26
 */
public class SecurityManagerTest {

    public static void main(String[] args) {
        // -Djava.security.manager -Djava.security.policy=/Users/saleson/IdeaProjects/learn/learn-main/src/main/java/com/saleson/learn/java/security/security.policy
//        System.setSecurityManager(new SecurityManager());
        new A().print();
        new B().print();
        new com.saleson.java.security.test2.C().print();
    }
}

在debug SecurityManagerTest类时,添加jvm参数:

-Djava.security.manager -Djava.security.policy=/Users/saleson/IdeaProjects/learn/learn-main/src/main/java/com/saleson/learn/java/security/security.policy

在这里插入图片描述

验证场景介绍

全部正常执行

运行 SecurityManagerTest类,全部正常执行完成,输出如下:

/Users/saleson/dev_tools/openjdk/jdk-11.0.14.1+1/Contents/Home/bin/java -Djava.security.manager -Djava.security.policy=/Users/saleson/IdeaProjects/learn/learn-main/src/main/java/com/saleson/learn/java/security/security.policy ... com.saleson.learn.java.security.SecurityManagerTest
Connected to the target VM, address: '127.0.0.1:58939', transport: 'socket'
A.print
B.print
C.print
Disconnected from the target VM, address: '127.0.0.1:58939', transport: 'socket'

Process finished with exit code 0
grant1 注释掉

运行 SecurityManagerTest类,在执行A.print()时会抛错,输出如下:

/Users/saleson/dev_tools/openjdk/jdk-11.0.14.1+1/Contents/Home/bin/java -Djava.security.manager -Djava.security.policy=/Users/saleson/IdeaProjects/learn/learn-main/src/main/java/com/saleson/learn/java/security/security.policy ... com.saleson.learn.java.security.SecurityManagerTest
Connected to the target VM, address: '127.0.0.1:60206', transport: 'socket'
Exception in thread "main" java.security.AccessControlException: access denied ("java.security.SecurityPermission" "a.b.c")
	at java.base/java.security.AccessControlContext.checkPermission(AccessControlContext.java:472)
	at java.base/java.security.AccessController.checkPermission(AccessController.java:897)
	at java.base/java.lang.SecurityManager.checkPermission(SecurityManager.java:322)
	at java.base/java.lang.SecurityManager.checkSecurityAccess(SecurityManager.java:1435)
	at com.saleson.learn.java.security.A._print(A.java:37)
	at com.saleson.learn.java.security.A.print(A.java:20)
	at com.saleson.learn.java.security.A.print(A.java:14)
	at com.saleson.learn.java.security.SecurityManagerTest.main(SecurityManagerTest.java:17)
Disconnected from the target VM, address: '127.0.0.1:60206', transport: 'socket'

Process finished with exit code 1

A.print()无法执行;

grant1 和 A.print() 注释掉

把SecurityManagerTest类中的代码调整下:

public class SecurityManagerTest {

    public static void main(String[] args) {
//        System.setSecurityManager(new SecurityManager());
//        new A().print();
        new B().print();
        new com.saleson.java.security.test2.C().print();
    }
}

运行 SecurityManagerTest类,在执行A.print()时会抛错,输出如下:

/Users/saleson/dev_tools/openjdk/jdk-11.0.14.1+1/Contents/Home/bin/java -Djava.security.manager -Djava.security.policy=/Users/saleson/IdeaProjects/learn/learn-main/src/main/java/com/saleson/learn/java/security/security.policy ... com.saleson.learn.java.security.SecurityManagerTest
Connected to the target VM, address: '127.0.0.1:60738', transport: 'socket'
Exception in thread "main" java.security.AccessControlException: access denied ("java.security.SecurityPermission" "a.b.c")
	at java.base/java.security.AccessControlContext.checkPermission(AccessControlContext.java:472)
	at java.base/java.security.AccessController.checkPermission(AccessController.java:897)
	at java.base/java.lang.SecurityManager.checkPermission(SecurityManager.java:322)
	at java.base/java.lang.SecurityManager.checkSecurityAccess(SecurityManager.java:1435)
	at com.saleson.learn.module.java.security.B._print(B.java:37)
	at com.saleson.learn.module.java.security.B.print(B.java:20)
	at com.saleson.learn.module.java.security.B.print(B.java:14)
	at com.saleson.learn.java.security.SecurityManagerTest.main(SecurityManagerTest.java:18)
Disconnected from the target VM, address: '127.0.0.1:60738', transport: 'socket'

Process finished with exit code 1

将SecurityManagerTest的代码再调整下,print() 改为 print(false), 仍能正常执行:

public class SecurityManagerTest {

    public static void main(String[] args) {
//        System.setSecurityManager(new SecurityManager());
//        new A().print();
        new B().print(false);
        new com.saleson.java.security.test2.C().print(false);
    }
}

运行 SecurityManagerTest类,输出如下:

/Users/saleson/dev_tools/openjdk/jdk-11.0.14.1+1/Contents/Home/bin/java -Djava.security.manager -Djava.security.policy=/Users/saleson/IdeaProjects/learn/learn-main/src/main/java/com/saleson/learn/java/security/security.policy ... com.saleson.learn.java.security.SecurityManagerTest
Connected to the target VM, address: '127.0.0.1:62839', transport: 'socket'
B.print
C.print
Disconnected from the target VM, address: '127.0.0.1:62839', transport: 'socket'

Process finished with exit code 0

为啥能正常执行了呢,这是因为AccessController.doPrivileged(),再重新看下B.print()方法:

    public void print(boolean direct) {
        if (direct) {
            _print();
        } else {
            AccessController.doPrivileged(new PrivilegedAction() {

                @Override
                public Object run() {
                    _print();
                    return null;
                }
            });
        }
    }

后面的节章会从代码debug的视角对比有无使用AccessController.doPrivileged()的区别。

grant2 和 grant3 注释掉之后执行结果类似。

源码跟踪

首先放一张调用栈的图:
在这里插入图片描述
关于ProtectionDemain和CodeSource跟类加载有关,简单的介绍下:

  • codeSource
    代码源,该对象是由ClassLoader生成,ClassLoader读取class和jar包得知类的所在目录或者jar包路径、签名者以及证书等。
public class CodeSource implements java.io.Serializable {

    /**
     * The code location.
     *
     * @serial
     */
    private final URL location;

    /*
     * The code signers.
     */
    private transient CodeSigner[] signers = null;

    /*
     * The code signers. Certificate chains are concatenated.
     */
    private transient java.security.cert.Certificate[] certs = null;

    private transient String locationNoFragString;
}
  • ProtectionDomain
    从类名就可以看出来这是保护域对象,它内部包含了CodeSource、PermissionCollection。

想了解更多相关内容可以查阅java之jvm学习笔记十(策略和保护域) 进行了解。

下面主要从debug的视角跟踪AccessController、AccessControlContext、SecurityPermission进行对比和理解。

AccessController

我们在代码里调用System.getSecurityManager().checkPermission(Permission)方法检测相关的执行权限时,真正去执行检测逻辑的是在AccessControlContext.checkPermission(Permission)方法中,完整的调用链见下面的时序图:
在这里插入图片描述
从security.policy的结构来看,grant codeBase “file:/…” 管理的是class的目录或者jar,而且为什么C.class所在的jar赋予了权限,如果SecurityManagerTest.class所在的目录没有赋予权限仍会无法执行呢?

原因在于AccessControlContext,AccessControlContext是由AccessController.getStackAccessControlContext()获取的,这是一个静态方法,从方法名可以看出,获取一个stack结构的AccessControlContext;AccessControlContext类中确实也一个跟调用栈有关的field:AccessControlContext.context,该field是一个ProtectionDomain[];ProtectionDomain包含CodeSource对象,前面介绍过,CodeSource包含调用类的所在目录或者jar包路径,并且是以先进后出的栈结构存储的,如图:
在这里插入图片描述
上面是AccessControlContext.checkPermission(Permission)的断点信息,该方法内有一个for循环,以后进先出的方式进行循环访问,所以即使C.pring()方法通过了安全权限检测,但是SecurityManagerTest.class没有通过安全权限检测仍是无法执行。这种情况也是可以通过其它方式避过的:AccessController.doPrivileged(PrivilegedAction)

AccessController.doPrivileged

AccessController.doPrivileged(PrivilegedAction)也是一个native方法,调用该方法后,jvm会重新生成一个access control object并且替换掉当前的。openjdk中的注释是:
在这里插入图片描述

在SecurityManagerTest.main()方法中改为C.print(false),采用AccessController.doPrivileged(PrivilegedAction)的方式进行调用。

public class SecurityManagerTest {

    public static void main(String[] args) {
//        new A().print();
        new B().print(false);
        new com.saleson.java.security.test2.C().print(false);
    }
}

print() 方法:

public void print(boolean direct) {
        if (direct) {
            this._print();
        } else {
            AccessController.doPrivileged(new PrivilegedAction() {
                public Object run() {
                    C.this._print();
                    return null;
                }
            });
        }

调用之后断点再看一下:
在这里插入图片描述
采用AccessController.doPrivileged(PrivilegedAction)后,AccessControlContext.context只有1个ProtectionDomain对象,安全权限检测通过,所以能够正常执行。

SecurityPermission

在执行SecurityPermission.implies(Permission)方法之前,会先在BasicPermissionCollection.implies(Permission)方法中找到匹配的Permission。

BasicPermissionCollection.implies(Permission)代码逻辑:

    @Override
    public boolean implies(Permission permission) {
        if (! (permission instanceof BasicPermission))
            return false;

        BasicPermission bp = (BasicPermission) permission;

        // random subclasses of BasicPermission do not imply each other
        if (bp.getClass() != permClass)
            return false;

        // short circuit if the "*" Permission was added
        if (all_allowed)
            return true;

        // strategy:
        // Check for full match first. Then work our way up the
        // path looking for matches on a.b..*

        String path = bp.getCanonicalName();
        //System.out.println("check "+path);

        Permission x = perms.get(path);

        if (x != null) {
            // we have a direct hit!
            return x.implies(permission);
        }

        // work our way up the tree...
        int last, offset;

        offset = path.length()-1;

        while ((last = path.lastIndexOf('.', offset)) != -1) {

            path = path.substring(0, last+1) + "*";
            //System.out.println("check "+path);

            x = perms.get(path);

            if (x != null) {
                return x.implies(permission);
            }
            offset = last -1;
        }

        // we don't have to check for "*" as it was already checked
        // at the top (all_allowed), so we just return false
        return false;
    }

找到之后再调用Permission.implies(Permission)。

SecurityPermission类的逻辑都在其父类BasicPermission中,BasicPermission在构造方法中会init()进行简单的解析:

  1. 先判断后缀是否为*
  2. 如果是则对path进行处理,例如name = “x.x.*”
    wildcard = true
    path = “x.x.”

BasicPermission.init(String)

    private void init(String name) {
        if (name == null)
            throw new NullPointerException("name can't be null");

        int len = name.length();

        if (len == 0) {
            throw new IllegalArgumentException("name can't be empty");
        }

        char last = name.charAt(len - 1);

        // Is wildcard or ends with ".*"?
        if (last == '*' && (len == 1 || name.charAt(len - 2) == '.')) {
            wildcard = true;
            if (len == 1) {
                path = "";
            } else {
                path = name.substring(0, len - 1);
            }
        } else {
            if (name.equals("exitVM")) {
                wildcard = true;
                path = "exitVM.";
                exitVM = true;
            } else {
                path = name;
            }
        }
    }

BasicPermission.implies(Permission)方法也是一个简单的对比逻辑:

    @Override
    public boolean implies(Permission p) {
        if ((p == null) || (p.getClass() != getClass()))
            return false;

        BasicPermission that = (BasicPermission) p;

        if (this.wildcard) {
            if (that.wildcard) {
                // one wildcard can imply another
                return that.path.startsWith(path);
            } else {
                // make sure ap.path is longer so a.b.* doesn't imply a.b
                return (that.path.length() > this.path.length()) &&
                    that.path.startsWith(this.path);
            }
        } else {
            if (that.wildcard) {
                // a non-wildcard can't imply a wildcard
                return false;
            }
            else {
                return this.path.equals(that.path);
            }
        }
    }
SecurityPermission的使用案例

在SecurityPermission的构造参数中仅有name参数会参与到对比计算中,但是name支持’*'这个通配符;在使用时可以在.policy文件中配置:

grant {
	permission java.security.SecurityPermission "a.b.*";
}

在check时,可以将name以’.'进行任意的组合用于检测:

// check pass
System.getSecurityManager().securityManager.checkSecurityAccess("a.b.c");
// check pass
System.getSecurityManager().securityManager.checkSecurityAccess("a.b.c.c");
// check fail
System.getSecurityManager().securityManager.checkSecurityAccess("a.c.c");

参考

java之jvm学习笔记四(安全管理器)
java之jvm学习笔记八(实践对jar包的代码签名)
java之jvm学习笔记十(策略和保护域)
java之jvm学习笔记十二(访问控制器的栈校验机制)
使用Policy文件来设置Java的安全策略
使用Policy文件来设置Java的安全策略
java 权限 库,java – 授予多个代码库的权限
java安全管理器SecurityManager介绍
Java安全——安全管理器、访问控制器和类装载器
Java安全——理解Java沙箱
Java沙箱机制的实现——安全管理器、访问控制器

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值