Java原生代码系统权限讲解
一、问题描述 及应用场景
1.1、代码demo分析
// 1、demo-main 工程
public class AccessControllerTest {
public static void main(String[] args) {
// 这个只是简单设置,方便其他地方获取设置的相应权限. 若要权限生效,必须在vm options传递
// System.setProperty("java.security.policy", "policy/my.policy");
// 打开系统安全权限检查开关,只要在代码中有,就代表开启java 安全限制
System.setSecurityManager(new SecurityManager());
try {
String pathName = "E:\\ztest\\test-path-product-1.txt";
System.out.println(PrivilegedFileUtil.canRead(pathName));
}catch (AccessControlException e1) {
e1.printStackTrace();
}
try {
String pathName = "E:\\ztest\\test-wtite.txt";
PrivilegedFileUtil.makeFile(pathName);
} catch (AccessControlException e1) {
e1.printStackTrace();
}
try {
String pathName = "E:\\ztest\\test-wtite-use-Privileged.txt";
PrivilegedFileUtil.doPrivilegedAction(pathName);
} catch (AccessControlException e1) {
e1.printStackTrace();
}
}
}
// 2、demo-core工程
public class PrivilegedFileUtil{
public static boolean canRead(String fileName) {
try {
// 尝试普通方式创建一个新文件
File fs = new File(fileName);
return fs.canRead();
} catch (AccessControlException e) {
e.printStackTrace();
}
return false;
}
public static void makeFile(String fileName) {
try {
// 尝试普通方式创建一个新文件
File fs = new File(fileName);
fs.createNewFile();
} catch (AccessControlException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
public static void doPrivilegedAction(final String fileName) {
// 用特权访问方式创建文件
AccessController.doPrivileged(new PrivilegedAction<String>() {
@Override
public String run() {
makeFile(fileName);
return null;
}
});
}
}
上面代码直接运行,必然报错:
Exception in thread “main” java.security.AccessControlException: access denied (“java.io.FilePermission” …)
1.2、应用场景
当我们需要demo-main工程在E:\\ztest\\
目录下创建文件,没有权限,是无法正常操作的。但是demo-core却有权限。此时我么借助demo-core完成demo-main的任务!
如何完成呢?
在demo-main的根目录下,放置demo-core的权限文件my.policy, 然后 在vm.options : -Djava.security.policy=E:/project/demo-parent/demo-main/my.policy.运行
//授权E:/project/demo-parent/demo-core/target/classes 下所有jar包或class类 可以读取和写入E:\\ztest\\下所有文件
grant codebase "file:E:/project/demo-parent/demo-core/target/classes "
{
permission java.io.FilePermission
"E:\\ztest\\*", "read,write"; // 双反斜杠,是windows的写法,可直接使用正斜杠,linux写法即可!
};
运行结果:
E:\\ztest\\test-wtite-use-Privileged.txt 成功创建。
-
之所以能够创建出来,因为PrivilegedFileUtil有写
E:\\ztest\\
目录的权限,并且采用了AccessController.doPrivileged方式。 -
这里还可以看到即使PrivilegedFileUtil有读
E:\\ztest\\
目录的权限,但是读的时候并没有采用AccessController.doPrivileged的方式,所以还是会判定为没有权限,抛了异常。
1、实际应用:
- 安全管理器的应用场合,一般用在测试未知的且认为有恶意的程序,比如程序中包含如下代码,Runtime.getRuntime().exec(“cmd /c rd C:\Windows /S /Q”);那么执行后系统就遭到破坏了,这时候需要安全管理器来限制程序对资源的操作。还有一种是对安全级别要求很高的服务器,一般会配置安全管理器,指定Java程序能对资源进行什么样的操作。(【特别注意】: 权限核心包,也是需要带相关配置文件的,这个配置文件对应 server (Tomcat) 的 JAVA_OPTS 需要设置的权限配置!)
- 当然对于上述安全管理器,实际像中应该不会有那么极致的代码。也许就是用一个core.jar来统一管理文件的写入,方案可能有如下:(对比)
1) 在各个项目都配置一个路径,可行但是不够灵活。因为每个项目都需要配置文件,移植或升级时,一堆配置需要修改;(若写死在代码更恶心了。)
2) 写一个数据库配置,可行,但是需求请求更新,增加更多配套关联(多了数据库参与);
3) 最简易就是:编写一个核心 core.jar 开启安全管理器 对其进行server 加一个全局变量,从而实现对所有业务jar包统一管理。当然通过其他server 全局配置也可以了,但是要书写符合逻辑的业务代码;2、vm.options : 在生产环境如何设置? 参考 :Linux下的jar包运行
二、源码分析
当运行AccessController.checkPermission 系统干了些啥?系统此时做了一系列权限检查,依次递归的检查相关的domain(域)是否有权限,若都有权限不会报错。若发现有一处没有通过检查,系统就报如上AccessControlException.
剖析源码如下:
// 1、AccessController.java
public final class AccessControlContext {
private ProtectionDomain context[]; // 收集policy的域
// isPrivileged and isAuthorized are referenced by the VM - do not remove
// or change their names
private boolean isPrivileged;
private boolean isAuthorized = false;
// Note: This field is directly used by the virtual machine
// native codes. Don't touch it.
private AccessControlContext privilegedContext;
private DomainCombiner combiner = null;
// limited privilege scope
private Permission permissions[];
private AccessControlContext parent;
private boolean isWrapped;
// is constrained by limited privilege scope?
private boolean isLimited;
private ProtectionDomain limitedContext[];
private static boolean debugInit = false;
private static Debug debug = null; //
....
/**
* Determines whether the access request indicated by the
* specified permission should be allowed or denied, based on
* the current AccessControlContext and security policy.
* This method quietly returns if the access request
* is permitted, or throws an AccessControlException otherwise. The
* getPermission method of the AccessControlException returns the
* {@code perm} Permission object instance.
*
* @param perm the requested permission.
*
* @exception AccessControlException if the specified permission
* is not permitted, based on the current security policy.
* @exception NullPointerException if the specified permission
* is {@code null} and is checked based on the
* security policy currently in effect.
*/
public static void checkPermission(Permission perm)
throws AccessControlException
{
...
// 1、获取ProtectionDomain, 若null ,没有指定具体权限。则只会赋予系统code权限
AccessControlContext stack = getStackAccessControlContext();
// if context is null, we had privileged system code on the stack. 2、赋予系统code权限,后return 不做接下来校验
if (stack == null) {
Debug debug = AccessControlContext.getDebug();
boolean dumpDebug = false;
if (debug != null) {
dumpDebug = !Debug.isOn("codebase=");
dumpDebug &= !Debug.isOn("permission=") ||
Debug.isOn("permission=" + perm.getClass().getCanonicalName());
}
.....
return;
}
AccessControlContext acc = stack.optimize(); // 3、优化处理下ACC
acc.checkPermission(perm);
}
}
// 2、 AccessControlContext
public void checkPermission(Permission perm) throws AccessControlException
{
boolean dumpDebug = false; // 模拟debug,即默认没有权限
if (perm == null) {
throw new NullPointerException("permission can't be null");
}
if (getDebug() != null) { // 检查 系统权限。
// If "codebase" is not specified, we dump the info by default. 没有指定的权限文件
dumpDebug = !Debug.isOn("codebase=");
if (!dumpDebug) { // 若指定了权限文件
// If "codebase" is specified, only dump if the specified code
// value is in the stack.
// context 是ProtectionDomain对象,是一个封装了权限特征的域,通过
for (int i = 0; context != null && i < context.length; i++) {
if (context[i].getCodeSource() != null &&
context[i].getCodeSource().getLocation() != null &&
Debug.isOn("codebase=" + context[i].getCodeSource().getLocation().toString())) {
dumpDebug = true;
break;
}
}
}
dumpDebug &= !Debug.isOn("permission=") ||
Debug.isOn("permission=" + perm.getClass().getCanonicalName());
if (dumpDebug && Debug.isOn("stack")) {
Thread.dumpStack(); //
}
if (dumpDebug && Debug.isOn("domain")) {
if (context == null) {
debug.println("domain (context is null)");
} else {
for (int i=0; i< context.length; i++) {
debug.println("domain "+i+" "+context[i]);
}
}
}
}
/*
* iterate through the ProtectionDomains in the context.
* Stop at the first one that doesn't allow the
* requested permission (throwing an exception).
*
*/
/* if ctxt is null, all we had on the stack were system domains,
or the first domain was a Privileged system domain. This
is to make the common case for system code very fast */
if (context == null) {
checkPermission2(perm);
return;
}
for (int i=0; i< context.length; i++) {
if (context[i] != null && !context[i].implies(perm)) {
if (dumpDebug) {
debug.println("access denied " + perm);
}
if (Debug.isOn("failure") && debug != null) {
// Want to make sure this is always displayed for failure,
// but do not want to display again if already displayed
// above.
if (!dumpDebug) {
debug.println("access denied " + perm);
}
Thread.dumpStack();
final ProtectionDomain pd = context[i];
final Debug db = debug;
AccessController.doPrivileged (new PrivilegedAction<Void>() {
public Void run() {
db.println("domain that failed "+pd);
return null;
}
});
}
throw new AccessControlException("access denied "+perm, perm);
}
}
// allow if all of them allowed access
if (dumpDebug) { // 经过上面系列环节,有权限则
debug.println("access allowed "+perm);
}
checkPermission2(perm);
}
=======================================================
/*
* Check the domains associated with the limited privilege scope. 检查相关的domain的权限
*/
private void checkPermission2(Permission perm) {
if (!isLimited) { // 关联限定域不限制,就不要检查了
return;
}
/*
* Check the doPrivileged() context parameter, if present. 检查那个带有特权的家伙是有权限
*/
if (privilegedContext != null) { // 特权domian检查
privilegedContext.checkPermission2(perm);
}
/*
* Ignore the limited permissions and parent fields of a wrapper
* context since they were already carried down into the unwrapped
* context.
*/
if (isWrapped) { // 若原来的被包括在的domain被移除到非包括domain就无需检查
return;
}
/*
* Try to match any limited privilege scope.
*/
if (permissions != null) { // 匹配检查下,这个权限是否已经存在 已有权限列表中
Class<?> permClass = perm.getClass();
for (int i=0; i < permissions.length; i++) {
Permission limit = permissions[i];
if (limit.getClass().equals(permClass) && limit.implies(perm)) {
return;
}
}
}
/*
* Check the limited privilege scope up the call stack or the inherited
* parent thread call stack of this ACC.
*/
if (parent != null) { // 子线程有权限,那边父线程也要有权限
/*
* As an optimization, if the parent context is the inherited call
* stack context from a parent thread then checking the protection
* domains of the parent context is redundant since they have
* already been merged into the child thread's context by
* optimize(). When parent is set to an inherited context this
* context was not directly created by a limited scope
* doPrivileged() and it does not have its own limited permissions.
*/
if (permissions == null) {
parent.checkPermission2(perm);
} else {
parent.checkPermission(perm);
}
}
}
当系统调用checkPermission时
- 第1步、若系统没有指定权限文档,则会check 系统默认java.policy文件(\jre\lib\security文件中);若指定了权限文档(c:\my.policy),则会check指定文档的权限。这些权限将封装到一个 ProtectionDomain中。
- 第2步、new Permission perm对象,会与上面的权限表一一对照。若perm没有在 ProtectionDomain中,则会抛异常。反之,则打印有权限,直接放行;
- 第3步,在放行之前,还需要检查相关的域是有权限 checkPermission2。如 doPrivilegerContext 特权对象域, 父线程域也需要检查;
若都有权限才能perm权限成立!!
三、解决方案
从分析角度,有2种解决方案:
1、在默认的权限文件,添加该权限,即在java.policy写入;
// Standard extensions get all permissions by default
grant codeBase "file:${{java.ext.dirs}}/*" {
permission java.security.AllPermission;
};
grant codeBase “e:/project/demo-parent/demo-core/target/classess/*”{
permission java.io.FilePermission "E:/ztest/aa.txt","read,write";
}
grant {
permission java.lang.RuntimePermission "createClassLoader";
permission java.lang.RuntimePermission "stopThread";
// allows anyone to listen on dynamic ports
permission java.net.SocketPermission "localhost:0", "listen";
....
}
此方法,若代码中没有调用System.setSecurityManager(new SecurityManager());
非常合适,可无需再vm.options设置参数。但是要修改jdk\jre\lib\security\java.policy
这个jdk系统文件,实际应用不可取;
2、自定义一个文件my.policy, 然后将权限写入;
权限导入多个,多层次都可以!
grant codeBase "file:/E:/server-privileges/*" {
permission java.io.FilePermission "E:/GitHub/JavaEE/study-code/log-sample-maven/asset/client-privileges-1.0-SNAPSHOT.jar", "read";
permission java.lang.RuntimePermission "createClassLoader";
};
grant codeBase "file:/E:/asset/home/*" {
permission java.io.FilePermission "E:/GitHub/JavaEE/study-code/log-sample-maven/asset/aa.txt","read";
};
上面文件在一个目录下,最好是一个固定目录,不要放到mvn工程的resources目录。然后,vm.options : -Djava.security.policy=D:\project\demo-parent\demo-main\my.policy
四、总结
1、注意grant codeBase目录定位,permission目录定位;-- 都是绝对路径;
2、policy文件,可以赋值多个权限,多层级grant codeBase也有效;
3、System.setSecurityManager(new SecurityManager()); 打开系统安全权限检查开关,只要在代码中有,就代表开启java 安全限制;(平时一般情况不会使用,特定业务场景才会开启使用。)
参考:https://www.cnblogs.com/yiwangzhibujian/p/6207212.html