关于SecurityManager的细节本文章不做介绍,主要聚集在场景搭建以及核心流程的代码跟踪,理解其设计思路。
文章目录
场景搭建
主要验证System.getSecurityManager().checkSecurityAccess(“a.b.c”)的三个场景:
- 与MainClass在同一个module
- 与MainClass在不同的module
- 在另一个jar包中
分别新增A、B、C、SecurityManagerTest四个类:
Class | Position | Describe |
---|---|---|
A | module: learn-main | 与Main class SecurityManagerTest在同一个module |
B | module: learn-module | 在单独一个module |
C | jar: com.saleson:personal:1.0-SNAPSHOT.jar | 在jar包中 |
SecurityManagerTest | module: learn-main | main 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()进行简单的解析:
- 先判断后缀是否为*
- 如果是则对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沙箱机制的实现——安全管理器、访问控制器