Это возможно, используя переплетение байтового кода с такой библиотекой, как Byte Buddy . Вместо использования стандартного разрешения ReflectPermission("suppressAccessChecks")
вы можете создать собственное разрешение и заменить методы AccessibleObject.setAccessible
пользовательскими методами, которые проверяют ваше специальное разрешение с помощью преобразований Byte Buddy.
Один из возможных способов работы этого настраиваемого разрешения состоит в том, чтобы оно основывало доступ на загрузчике классов вызывающей стороны и объекте, доступ к которому изменяется. Использование этого позволяет изолированному коду (коду, загруженному из отдельного приложения со своим собственным загрузчиком классов) вызывать setAccessible
для классов в своем собственном jar-файле, но не для стандартных классов 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;
}
// equals and hashCode omitted
@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) {
// Ignore. Use standard permission.
}
}
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;
}
// Check that the class loader instance is of a custom type
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()));
}
}
Последний шаг - установить Java-агент Byte Buddy и выполнить преобразование. Кувшин, содержащий заглушки, также должен быть добавлен в путь к классу начальной загрузки. Это необходимо, потому что класс AccessibleObject
будет загружен загрузчиком начальной загрузки, поэтому любые заглушки также должны быть загружены туда.
Instrumentation instrumentation = ByteBuddyAgent.install();
// Append the jar containing the stub replacement to the bootstrap classpath
instrumentation.appendToBootstrapClassLoaderSearch(jarFile);
AgentBuilder agentBuilder = new AgentBuilder.Default()
.disableClassFormatChanges()
.with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
.ignore(none()); // disable default ignores so we can transform Java classes
.type(ElementMatchers.named("java.lang.reflect.AccessibleObject"))
.transform(transformer)
.installOnByteBuddyAgent();
Это будет работать при использовании SecurityManager и изоляции классов-заглушек и кода, к которому вы применяете избирательные разрешения, в отдельных jar-файлах, загружаемых во время выполнения. Необходимость загружать файлы jar во время выполнения вместо того, чтобы иметь их в качестве стандартных зависимостей или связанных библиотек, несколько усложняет ситуацию, но, похоже, это является требованием для изоляции ненадежного кода при использовании SecurityManager
.
My Github repo sandbox-runtime содержит полный подробный пример среды выполнения в песочнице с выполнением изолированного ненадежного кода и более избирательными разрешениями на отражение. У меня также есть пост в блоге с более подробной информацией о выборочном набореДоступные разрешения штук.