这可以通过使用像
Byte Buddy这样的库进行字节码编织来实现。不使用标准的
ReflectPermission("suppressAccessChecks")
权限,而是创建一个自定义权限,并使用Byte Buddy转换替换
AccessibleObject.setAccessible
方法,检查您的自定义权限的自定义方法。
此自定义权限可能的一种工作方式是基于调用者的类加载器和正在修改访问的对象。使用这种方法允许隔离的代码(从具有自己的类加载器的分离代码中加载)在其自己的jar中调用
setAccessible
,但不能调用标准Java类或您自己的应用程序类。
这样的权限可能如下所示:
public class UserSetAccessiblePermission extends Permission {
private final ClassLoader loader;
public UserSetAccessiblePermission(ClassLoader loader) {
super("userSetAccessible");
this.loader = loader;
}
@Override
public boolean implies(Permission permission) {
if (!(permission instanceof UserSetAccessiblePermission)) {
return false;
}
UserSetAccessiblePermission that = (UserSetAccessiblePermission) permission;
return that.loader == this.loader;
}
@Override
public String getActions() {
return "";
}
}
这是我选择实现该权限的方式,但它也可以是软件包或类白名单或黑名单。
现在有了这个权限,你可以创建一个存根类来替换
AccessibleObject.setAcessible
方法,并改为使用此权限。
public class AccessibleObjectStub {
private final static Permission STANDARD_ACCESS_PERMISSION =
new ReflectPermission("suppressAccessChecks");
public static void setAccessible(@This AccessibleObject ao, boolean flag)
throws SecurityException {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
Permission permission = STANDARD_ACCESS_PERMISSION;
if (isFromUserLoader(ao)) {
try {
permission = getUserAccessPermission(ao);
} catch (Exception e) {
}
}
sm.checkPermission(permission);
}
}
private static Permission getUserAccessPermission(AccessibleObject ao)
throws IllegalAccessException, InvocationTargetException, InstantiationException,
NoSuchMethodException, ClassNotFoundException {
ClassLoader aoClassLoader = getAccessibleObjectLoader(ao);
return new UserSetAccessiblePermission(aoClassLoader);
}
private static ClassLoader getAccessibleObjectLoader(AccessibleObject ao) {
return AccessController.doPrivileged(new PrivilegedAction<ClassLoader>() {
@Override
public ClassLoader run() {
if (ao instanceof Executable) {
return ((Executable) ao).getDeclaringClass().getClassLoader();
} else if (ao instanceof Field) {
return ((Field) ao).getDeclaringClass().getClassLoader();
}
throw new IllegalStateException("Unknown AccessibleObject type: " + ao.getClass());
}
});
}
private static boolean isFromUserLoader(AccessibleObject ao) {
ClassLoader loader = getAccessibleObjectLoader(ao);
if (loader == null) {
return false;
}
return UserClassLoaders.isUserClassLoader(loader);
}
}
使用这两个类,您现在可以使用Byte Buddy构建一个转换器来转换Java
AccessibleObject
以使用您的存根。
创建转换器的第一步是创建一个Byte Buddy类型池,其中包括引导类和包含您的存根的jar文件。
final TypePool bootstrapTypePool = TypePool.Default.of(
new ClassFileLocator.Compound(
new ClassFileLocator.ForJarFile(jarFile),
ClassFileLocator.ForClassLoader.of(null)));
接下来使用反射获取对
AccessObject.setAccessible0
方法的引用。这是一个私有方法,如果调用
setAccessible
时通过了权限检查,它实际上会修改可访问性。
Method setAccessible0Method;
try {
String setAccessible0MethodName = "setAccessible0";
Class[] paramTypes = new Class[2];
paramTypes[0] = AccessibleObject.class;
paramTypes[1] = boolean.class;
setAccessible0Method = AccessibleObject.class
.getDeclaredMethod(setAccessible0MethodName, paramTypes);
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
使用这两个零件可以制造变压器。
AgentBuilder.Transformer transformer = new AgentBuilder.Transformer() {
@Override
public DynamicType.Builder<?> transform(
DynamicType.Builder<?> builder,
TypeDescription typeDescription, ClassLoader classLoader) {
return builder.method(
ElementMatchers.named("setAccessible")
.and(ElementMatchers.takesArguments(boolean.class)))
.intercept(MethodDelegation.to(
bootstrapTypePool.describe(
"com.leacox.sandbox.security.stub.java.lang.reflect.AccessibleObjectStub")
.resolve())
.andThen(MethodCall.invoke(setAccessible0Method).withThis().withAllArguments()));
}
}
最后一步是安装Byte Buddy Java代理并执行转换。包含存根的jar文件必须附加到引导类路径中。这是必要的,因为
AccessibleObject
类将由引导加载器加载,因此任何存根也必须在那里加载。
Instrumentation instrumentation = ByteBuddyAgent.install();
instrumentation.appendToBootstrapClassLoaderSearch(jarFile);
AgentBuilder agentBuilder = new AgentBuilder.Default()
.disableClassFormatChanges()
.with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
.ignore(none());
.type(ElementMatchers.named("java.lang.reflect.AccessibleObject"))
.transform(transformer)
.installOnByteBuddyAgent();
当使用SecurityManager并将存根类和应用选择性权限的代码隔离在运行时加载的单独的jar中时,此方法可行。必须在运行时加载jar而不是作为标准依赖项或捆绑库,这使得事情变得有些复杂,但这似乎是使用SecurityManager隔离不受信任代码的要求。
我的Github存储库
sandbox-runtime具有完整且深入的示例,其中包括执行隔离的不受信任代码以及更多选择性反射权限。我还有一篇博客文章详细介绍了
selective setAccessible permissions部分。