Прокачка @PreAuthorize в Spring Security произвольными типами и простым инспектируемым DSL
Spring Security — must-have компонент в Spring-приложениях, так как он отвечает за аутентификацию пользователя, а также за авторизацию тех или иных его действий в системе. Одним из методов авторизации в Spring Security является использование аннотации @PreAuthorize
, в которой с помощью выражений можно наглядно описать правила, следуя которым модуль авторизации решает, разрешить ли проведение операции или запретить.
В моём REST-сервисе возникла необходимость предоставить точку доступа к описанию правил авторизации для всех методов контроллеров сервиса. Причём, по возможности, избежать раскрытия специфики именно SpEL-выражений (т.е., вместо permitAll
нужно что-то вроде anybody
, а principal
избегать вовсе как избыточное выражение), но возвращать свои выражения, с которыми уже можно делать что угодно.
Начало
Давайте представим небольшой сервис и правила доступа к нему.
IGreetingService.java
— описывает небольшой сервис с минимальным набором операций
public interface IGreetingService {
@Nonnull
String sayHelloTo(@Nonnull String name);
@Nonnull
String sayGoodByeTo(@Nonnull String name);
}
GreetingService.java
— собственно простейшая реализация сервиса, содержащая также правила доступа к методам с учётом аннотации@PreAuthorize
@Service
public final class GreetingService
implements IGreetingService {
@Override
@Nonnull
@PreAuthorize("@A.maySayHelloTo(principal, #name)")
public String sayHelloTo(
@P("name") @Nonnull final String name
) {
return "hello " + name;
}
@Nonnull
@Override
@PreAuthorize("@A.maySayGoodByeTo(principal, #name)")
public String sayGoodByeTo(
@P("name") @Nonnull final String name
) {
return "good bye" + name;
}
}
IAuthorizationComponent.java
— такой же простой интерфейс, содержащий несколько правил
public interface IAuthorizationComponent {
boolean maySayHelloTo(@Nonnull UserDetails principal, @Nonnull String name);
boolean maySayGoodByeTo(@Nonnull UserDetails principal, @Nonnull String name);
}
AuthorizationComponent.java
— реализация правил авторизации (вместо текущихtrue
иfalse
можно представить более сложные правила, учитывающие входные параметры, но об этом ниже)
@Component("A")
public final class AuthorizationComponent
implements IAuthorizationComponent {
@Override
public boolean maySayHelloTo(@Nonnull final UserDetails principal, @Nonnull final String name) {
return true;
}
@Override
public boolean maySayGoodByeTo(@Nonnull final UserDetails principal, @Nonnull final String name) {
return false;
}
}
От boolean
к выражениям
Имея в наличии только результат логических выражений, нельзя получить описания самого правила. Нужно каким-то образом определить, какие правила отвечают за конкретное действие. Допустим, мы решаем использовать не boolean
, а объект, описывающий правило на более высоком уровне. К такому объекту у нас было бы всего два требования:
- уметь полноценно влиять на работу авторизации, т.е. просто возвращать логическое значение о том, можно ли разрешить или запретить операцию;
- уметь превращаться в текстовые представления, которые легко читаются человеком (хотя можно и превращать такие выражения в другие, более машинно-читыемые представления, но это здесь пока излишне).
Исходя из требований, нам достаточно иметь в распоряжение что-то типа:
public interface IAuthorizationExpression {
boolean mayProceed();
@Nonnull
String toHumanReadableExpression();
}
И слегка изменить компонент авторизации:
public interface IAuthorizationComponent {
@Nonnull
IAuthorizationExpression maySayHelloTo(@Nonnull UserDetails principal, @Nonnull String name);
@Nonnull
IAuthorizationExpression maySayGoodByeTo(@Nonnull UserDetails principal, @Nonnull String name);
}
@Component("A")
public final class AuthorizationComponent
implements IAuthorizationComponent {
@Nonnull
@Override
public IAuthorizationExpression maySayHelloTo(@Nonnull final UserDetails principal, @Nonnull final String name) {
return simpleAuthorizationExpression(true);
}
@Nonnull
@Override
public IAuthorizationExpression maySayGoodByeTo(@Nonnull final UserDetails principal, @Nonnull final String name) {
return simpleAuthorizationExpression(true);
}
}
SimpleAuthorizationExpression.java
— простейшее выражение, зависящее от единственного логического значения
public final class SimpleAuthorizationExpression
implements IAuthorizationExpression {
private static final IAuthorizationExpression mayProceedExpression = new SimpleAuthorizationExpression(true);
private static final IAuthorizationExpression mayNotProceedExpression = new SimpleAuthorizationExpression(false);
private final boolean mayProceed;
private SimpleAuthorizationExpression(final boolean mayProceed) {
this.mayProceed = mayProceed;
}
public static IAuthorizationExpression simpleAuthorizationExpression(final boolean mayProceed) {
return mayProceed ? mayProceedExpression : mayNotProceedExpression;
}
public boolean mayProceed() {
return mayProceed;
}
@Nonnull
public String toHumanReadableExpression() {
return mayProceed ? "TRUE" : "FALSE";
}
}
К сожалению, в обычном режиме @PreAuthorize
работает так, что его выражения могут возвращать только булевские значения. Поэтому при обращении к методам сервиса получится следующее исключение:
Exception in thread "main" java.lang.IllegalArgumentException: Failed to evaluate expression '@A.maySayHelloTo(principal, #name)'
at org.springframework.security.access.expression.ExpressionUtils.evaluateAsBoolean(ExpressionUtils.java:30)
at org.springframework.security.access.expression.method.ExpressionBasedPreInvocationAdvice.before(ExpressionBasedPreInvocationAdvice.java:59)
at org.springframework.security.access.prepost.PreInvocationAuthorizationAdviceVoter.vote(PreInvocationAuthorizationAdviceVoter.java:72)
at org.springframework.security.access.prepost.PreInvocationAuthorizationAdviceVoter.vote(PreInvocationAuthorizationAdviceVoter.java:40)
at org.springframework.security.access.vote.AffirmativeBased.decide(AffirmativeBased.java:63)
at org.springframework.security.access.intercept.AbstractSecurityInterceptor.beforeInvocation(AbstractSecurityInterceptor.java:233)
at org.springframework.security.access.intercept.aopalliance.MethodSecurityInterceptor.invoke(MethodSecurityInterceptor.java:65)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:213)
at com.sun.proxy.$Proxy38.sayHelloTo(Unknown Source)
at test.springfx.security.app.Application.lambda$main$0(Application.java:23)
at test.springfx.security.app.Application$$Lambda$7/2043106095.run(Unknown Source)
at test.springfx.security.fakes.FakeAuthentication.withFakeAuthentication(FakeAuthentication.java:32)
at test.springfx.security.app.Application.main(Application.java:23)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:497)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)
Caused by: org.springframework.expression.spel.SpelEvaluationException: EL1001E:(pos 0): Type conversion problem, cannot convert from @javax.annotation.Nonnull test.springfx.security.app.auth.IAuthorizationExpression$1 to java.lang.Boolean
at org.springframework.expression.spel.support.StandardTypeConverter.convertValue(StandardTypeConverter.java:78)
at org.springframework.expression.common.ExpressionUtils.convertTypedValue(ExpressionUtils.java:53)
at org.springframework.expression.spel.standard.SpelExpression.getValue(SpelExpression.java:301)
at org.springframework.security.access.expression.ExpressionUtils.evaluateAsBoolean(ExpressionUtils.java:26)
... 18 more
Caused by: org.springframework.core.convert.ConverterNotFoundException: No converter found capable of converting from type [@javax.annotation.Nonnull test.springfx.security.app.auth.IAuthorizationExpression$1] to type [java.lang.Boolean]
at org.springframework.core.convert.support.GenericConversionService.handleConverterNotFound(GenericConversionService.java:313)
at org.springframework.core.convert.support.GenericConversionService.convert(GenericConversionService.java:195)
at org.springframework.expression.spel.support.StandardTypeConverter.convertValue(StandardTypeConverter.java:74)
... 21 more
Настройка @PreAuthorize
В первую очередь для устранения проблемы с небулевыми значениями нужно настроить GlobalMethodSecurityConfiguration
, потому как он позволяет настроить контекст вычисления выражений в @PreAuthorize
. За это отвечает т.н. TypeConverter
, к которому довольно просто добраться:
public abstract class CustomTypesGlobalMethodSecurityConfiguration
extends GlobalMethodSecurityConfiguration {
protected abstract ApplicationContext applicationContext();
protected abstract ConversionService conversionService();
@Override
protected MethodSecurityExpressionHandler createExpressionHandler() {
final ApplicationContext applicationContext = applicationContext();
final TypeConverter typeConverter = new StandardTypeConverter(conversionService());
final DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler() {
@Override
public StandardEvaluationContext createEvaluationContextInternal(final Authentication authentication, final MethodInvocation methodInvocation) {
final StandardEvaluationContext decoratedStandardEvaluationContext = super.createEvaluationContextInternal(authentication, methodInvocation);
return new ForwardingStandardEvaluationContext() {
@Override
protected StandardEvaluationContext standardEvaluationContext() {
return decoratedStandardEvaluationContext;
}
@Override
public TypeConverter getTypeConverter() {
return typeConverter;
}
};
}
};
handler.setApplicationContext(applicationContext);
return handler;
}
}
Здесь есть несколько моментов. Во-первых, мы будем использовать стандартный DefaultMethodSecurityExpressionHandler
, который сделает большую часть за нас, просто переопределив возвращаемый им контекст. Во-вторых, предок DefaultMethodSecurityExpressionHandler
, а именно AbstractSecurityExpressionHandler
, запрещает создание своего контекста, но мы можем создать т.н. внутренний контекст, в котором и переопределим TypeConverter
. В-третьих, нам нужно написать forwarding-декоратор для StandardEvaluationContext
, чтобы не сломать поведение оригинального контекста:
ForwardingStandardEvaluationContext.java
— forwarding-декоратор дляStandardEvaluationContext
public abstract class ForwardingStandardEvaluationContext
extends StandardEvaluationContext {
protected abstract StandardEvaluationContext standardEvaluationContext();
// @formatter:off
@Override public void setRootObject(final Object rootObject, final TypeDescriptor typeDescriptor) { standardEvaluationContext().setRootObject(rootObject, typeDescriptor); }
@Override public void setRootObject(final Object rootObject) { standardEvaluationContext().setRootObject(rootObject); }
@Override public TypedValue getRootObject() { return standardEvaluationContext().getRootObject(); }
@Override public void addConstructorResolver(final ConstructorResolver resolver) { standardEvaluationContext().addConstructorResolver(resolver); }
@Override public boolean removeConstructorResolver(final ConstructorResolver resolver) { return standardEvaluationContext().removeConstructorResolver(resolver); }
@Override public void setConstructorResolvers(final List constructorResolvers) { standardEvaluationContext().setConstructorResolvers(constructorResolvers); }
@Override public List getConstructorResolvers() { return standardEvaluationContext().getConstructorResolvers(); }
@Override public void addMethodResolver(final MethodResolver resolver) { standardEvaluationContext().addMethodResolver(resolver); }
@Override public boolean removeMethodResolver(final MethodResolver methodResolver) { return standardEvaluationContext().removeMethodResolver(methodResolver); }
@Override public void setMethodResolvers(final List methodResolvers) { standardEvaluationContext().setMethodResolvers(methodResolvers); }
@Override public List getMethodResolvers() { return standardEvaluationContext().getMethodResolvers(); }
@Override public void setBeanResolver(final BeanResolver beanResolver) { standardEvaluationContext().setBeanResolver(beanResolver); }
@Override public BeanResolver getBeanResolver() { return standardEvaluationContext().getBeanResolver(); }
@Override public void addPropertyAccessor(final PropertyAccessor accessor) { standardEvaluationContext().addPropertyAccessor(accessor); }
@Override public boolean removePropertyAccessor(final PropertyAccessor accessor) { return standardEvaluationContext().removePropertyAccessor(accessor); }
@Override public void setPropertyAccessors(final List propertyAccessors) { standardEvaluationContext().setPropertyAccessors(propertyAccessors); }
@Override public List getPropertyAccessors() { return standardEvaluationContext().getPropertyAccessors(); }
@Override public void setTypeLocator(final TypeLocator typeLocator) { standardEvaluationContext().setTypeLocator(typeLocator); }
@Override public TypeLocator getTypeLocator() { return standardEvaluationContext().getTypeLocator(); }
@Override public void setTypeConverter(final TypeConverter typeConverter) { standardEvaluationContext().setTypeConverter(typeConverter); }
@Override public TypeConverter getTypeConverter() { return standardEvaluationContext().getTypeConverter(); }
@Override public void setTypeComparator(final TypeComparator typeComparator) { standardEvaluationContext().setTypeComparator(typeComparator); }
@Override public TypeComparator getTypeComparator() { return standardEvaluationContext().getTypeComparator(); }
@Override public void setOperatorOverloader(final OperatorOverloader operatorOverloader) { standardEvaluationContext().setOperatorOverloader(operatorOverloader); }
@Override public OperatorOverloader getOperatorOverloader() { return standardEvaluationContext().getOperatorOverloader(); }
@Override public void setVariable(final String name, final Object value) { standardEvaluationContext().setVariable(name, value); }
@Override public void setVariables(final Map variables) { standardEvaluationContext().setVariables(variables); }
@Override public void registerFunction(final String name, final Method method) { standardEvaluationContext().registerFunction(name, method); }
@Override public Object lookupVariable(final String name) { return standardEvaluationContext().lookupVariable(name); }
@Override public void registerMethodFilter(final Class> type, final MethodFilter filter) throws IllegalStateException { standardEvaluationContext().registerMethodFilter(type, filter); }
// @formatter:on
}
Да, Java здесь не лучше выглядит, и было бы здорово, если бы Java умела by как Kotlin, чтобы не нужно было писать так много. Или, например, @Delegate из Lombok. Можно, конечно, было использовать и поле, а не абстрактный метод, но абстрактный метод мне кажется чуточку гибче (впрочем, я не знаю, умеют ли Kotlin и Lombok делегировать к методу, возвращающему декорируемый объект).
Два класса выше я бы отнёс с «библиотечному» слою, т.е. его можно использовать в нескольких приложениях отдельно и настраивать в каждом приложении под свои нужны. И вот уже в «слое приложения» теперь можно без проблем добавить свой конвертер из IAuthorizationExpression
в boolean
:
SecurityConfiguration.java
— здесь мы просто связываем контекст приложения и сервис преобразований
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = false)
public class SecurityConfiguration
extends CustomTypesGlobalMethodSecurityConfiguration {
private final ApplicationContext applicationContext;
private final ConversionService conversionService;
public SecurityConfiguration(
@Autowired final ApplicationContext applicationContext,
@Autowired final ConversionService conversionService
) {
this.applicationContext = applicationContext;
this.conversionService = conversionService;
}
@Override
protected ApplicationContext applicationContext() {
return applicationContext;
}
@Override
protected ConversionService conversionService() {
return conversionService;
}
}
ConversionConfiguration.java
— и, собственно, сама конфигурация сервиса, в котором просто добавляется ещё один конвертер к списку уже существующих
@Configuration
public class ConversionConfiguration {
@Bean
public ConversionService conversionService() {
final DefaultConversionService conversionService = new DefaultConversionService();
conversionService.addConverter(IAuthorizationExpression.class, Boolean.class, IAuthorizationExpression::mayProceed);
return conversionService;
}
}
Теперь IAuthorizationExpression
работает как boolean
и может участвовать в логических операциях напрямую.
Более сложные выражения
Поскольку у нас уже есть в наличии объект-выражение, мы можем расширить его базовый функционал и создать выражения, сложнее чем SimpleAuthorizationExpression
. Это позволит нам комбинировать выражения любой сложности, при этом соблюдая требования, указанные выше.
Мне всегда нравились fluent-интерфейсы, методы которых можно объединять удобным образом. Например, Mockito и Hamcrest используют такой подход вовсю. И Java теперь тоже для базовых интерфейсов типа Function
, Supplier
, Consumer
, Comparator
и т.д. Java 8 привнесла в язык кучу хороших возможностей, и одну из них, а именно методы по умолчанию, можно использовать для расширения базового функционала выражений. Например, можно добавить в IAuthorizationExpression
простой предикат AND:
default IAuthorizationExpression and(final IAuthorizationExpression r) {
return new IAuthorizationExpression() {
@Override
public boolean mayProceed() {
return IAuthorizationExpression.this.mayProceed() && r.mayProceed();
}
@Nonnull
@Override
public String toHumanReadableExpression() {
return new StringBuilder("(")
.append(IAuthorizationExpression.this.toHumanReadableExpression())
.append(" AND ")
.append(r.toHumanReadableExpression())
.append(')')
.toString();
}
};
}
Как видно, операция AND строится очень просто. В методе mayProceed
можно просто получить результат композиции выражений с помощью &&
, а в toHumanReadableExpression
— сформировать строку именно для этого выражения. Теперь можно комбинировать выражение, используя операцию AND, например, так:
simpleAuthorizationExpression(true).and(simpleAuthorizationExpression(true))
И в тот же момент строковое представление для такого выражения будет:
(TRUE AND TRUE)
Весьма неплохо. Так же без проблем можно добавить поддержку операции OR или унарной операции NOT. Кроме того, можно создавать и более сложные выражения без тривиальных операций, поскольку SimpleAuthorizationExpression
не имеет большого смысла. Например, выражение, определяющее, является ли пользователь root-ом:
public final class IsRootAuthorizationExpression
implements IAuthorizationExpression {
private final UserDetails userDetails;
private IsRootAuthorizationExpression(final UserDetails userDetails) {
this.userDetails = userDetails;
}
public static IAuthorizationExpression isRoot(final UserDetails userDetails) {
return new IsRootAuthorizationExpression(userDetails);
}
@Override
public boolean mayProceed() {
return Objects.equals(userDetails.getUsername(), "root");
}
@Nonnull
@Override
public String toHumanReadableExpression() {
return "isRoot";
}
}
Или является ли строка, представленная переменной name, запрещённой:
public final class IsNamePermittedAuthorizationExpression
implements IAuthorizationExpression {
private static final Collection bannedStrings = emptyList();
private final String name;
private IsNamePermittedAuthorizationExpression(final String name) {
this.name = name;
}
public static IAuthorizationExpression isNamePermitted(final String name) {
return new IsNamePermittedAuthorizationExpression(name);
}
@Override
public boolean mayProceed() {
return !bannedStrings.contains(name.toLowerCase());
}
@Nonnull
@Override
public String toHumanReadableExpression() {
return new StringBuilder()
.append("(name NOT IN (")
.append(bannedStrings.stream().collect(joining()))
.append("))")
.toString();
}
}
Теперь правила авторизации могут быть представлены иначе:
@Nonnull
@Override
public IAuthorizationExpression maySayHelloTo(@Nonnull final UserDetails principal, @Nonnull final String name) {
return isNamePermitted(name);
}
@Nonnull
@Override
public IAuthorizationExpression maySayGoodByeTo(@Nonnull final UserDetails principal, @Nonnull final String name) {
return isRoot(principal).and(isNamePermitted(name));
}
Код вполне читаем и прекрасно понятно, что именно делают эти выражения. А вот как выглядят сами строковые представления:
(name NOT IN ())
(isRoot AND (name NOT IN ()))
Аналогия на лицо, верно?
Преобразование @PreAuthorize
в IAuthorizationExpression
и строковое представление
Теперь осталось только получить эти выражения во время выполнения приложения. Допустим есть отдельная возможность получить список всех методов, которые нам нужны (там может быть много специфики, и она нас сейчас не очень интересует). Имея такой набор методов, остаётся просто «виртуально» выполнить выражения из @PreAuthorize
, заменив недостающие переменные какимо-либо значениями. Например:
@Service
public final class DiscoverService
implements IDiscoverService {
private static final UserDetails userDetailsMock = (UserDetails) newProxyInstance(
DiscoverService.class.getClassLoader(),
new Class>[]{ UserDetails.class },
(proxy, method, args) -> {
throw new AssertionError(method);
}
);
private static final Authentication authenticationMock = (Authentication) newProxyInstance(
DiscoverService.class.getClassLoader(),
new Class>[]{ Authentication.class },
(proxy, method, args) -> {
switch ( method.getName() ) {
case "getPrincipal":
return userDetailsMock;
case "isAuthenticated":
return true;
default:
throw new AssertionError(method);
}
}
);
private final ApplicationContext applicationContext;
private final ConversionService conversionService;
public DiscoverService(
@Autowired final ApplicationContext applicationContext,
@Autowired final ConversionService conversionService
) {
this.applicationContext = applicationContext;
this.conversionService = conversionService;
}
@Override
@Nullable
public String toAuthorizationExpression(@Nonnull final T object, @Nonnull final Class extends T> inspectType, @Nonnull final String methodName,
@Nonnull final Class>... parameterTypes)
throws NoSuchMethodException {
final Method method = inspectType.getMethod(methodName, parameterTypes);
final DefaultMethodSecurityExpressionHandler expressionHandler = createMethodSecurityExpressionHandler();
final MethodInvocation invocation = createMethodInvocation(object, method);
final EvaluationContext evaluationContext = createEvaluationContext(method, expressionHandler, invocation);
final Object value = evaluate(method, evaluationContext);
return resolveAsString(value);
}
private DefaultMethodSecurityExpressionHandler createMethodSecurityExpressionHandler() {
final DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
expressionHandler.setApplicationContext(applicationContext);
return expressionHandler;
}
private MethodInvocation createMethodInvocation(@Nonnull final T object, final Method method) {
final Parameter[] parameters = method.getParameters();
return new SimpleMethodInvocation(object, method, Stream.of(parameters).map(<...>).toArray(Object[]::new));
}
private EvaluationContext createEvaluationContext(final Method method, final SecurityExpressionHandler expressionHandler,
final MethodInvocation invocation) {
final EvaluationContext decoratedExpressionContext = expressionHandler.createEvaluationContext(authenticationMock, invocation);
final TypeConverter typeConverter = new StandardTypeConverter(conversionService);
return new ForwardingEvaluationContext() {
@Override
protected EvaluationContext evaluationContext() {
return decoratedExpressionContext;
}
@Override
public TypeConverter getTypeConverter() {
return typeConverter;
}
@Override
public Object lookupVariable(final String name) {
return <...>;
}
};
}
private static Object evaluate(final Method method, final EvaluationContext evaluationContext) {
final ExpressionParser parser = new SpelExpressionParser();
final PreAuthorize preAuthorizeAnnotation = method.getAnnotation(PreAuthorize.class);
final Expression expression = parser.parseExpression(preAuthorizeAnnotation.value());
return expression.getValue(evaluationContext, Object.class);
}
private static String resolveAsString(final Object value) {
if ( value instanceof IAuthorizationExpression ) {
return ((IAuthorizationExpression) value).toHumanReadableExpression();
}
return String.valueOf(value);
}
}
Этот код немного сложнее, но в действительности в нём нет ничего сложного. Трудности могут возникнуть разве с нахождением недостающих переменных в для выражений (например, #name
из примеров выше). Вместо <...>
нужно подставить свою реализацию для подстановки аргументов в параметры. На самом деле, здесь часто можно обойтись и просто null
-ом, но в некоторых случаях такое решение не работает из-за понятных причин. И ещё одна не очень приятная особенность: нужно вручную ещё дополнительно создать ForwardingEvaluationContext
за тем же принципом, что и ForwardingStandardEvaluationContext
выше:
public abstract class ForwardingEvaluationContext
implements EvaluationContext {
protected abstract EvaluationContext evaluationContext();
// @formatter:off
@Override public TypedValue getRootObject() { return evaluationContext().getRootObject(); }
@Override public List getConstructorResolvers() { return evaluationContext().getConstructorResolvers(); }
@Override public List getMethodResolvers() { return evaluationContext().getMethodResolvers(); }
@Override public List getPropertyAccessors() { return evaluationContext().getPropertyAccessors(); }
@Override public TypeLocator getTypeLocator() { return evaluationContext().getTypeLocator(); }
@Override public TypeConverter getTypeConverter() { return evaluationContext().getTypeConverter(); }
@Override public TypeComparator getTypeComparator() { return evaluationContext().getTypeComparator(); }
@Override public OperatorOverloader getOperatorOverloader() { return evaluationContext().getOperatorOverloader(); }
@Override public BeanResolver getBeanResolver() { return evaluationContext().getBeanResolver(); }
@Override public void setVariable(final String name, final Object value) { evaluationContext().setVariable(name, value); }
@Override public Object lookupVariable(final String name) { return evaluationContext().lookupVariable(name); }
// @formatter:on
}
Вот и всё: мы добавили поддержку произвольных типов в @PreAuthorize
в качестве результата вычисления выражения или в качестве операндов, а также извлекать удобные для прочтения человеком представления этих выражений.
Что осталось на заднем плане
На самом деле, как говорилось выше, в моём приложении правилами авторизации обвешаны методы в контроллерах, а не в сервисах. Поскольку я использую Springmvc-router, получение списка методов не составляет для меня большого труда. Насколько мне известно, просто это сделать и стандартными средствами, что позволяет исследовать не только контроллеры, но и сервисы и вообще разные компоненты. Поэтому способ получения всех методов, проаннотированных @PreAuthorize
, остаётся на личное усмотрение.
Я также не люблю открытые конструкторы и всегда им предпочитаю статические фабричные методы из-за возможности:
- скрыть реальный возвращаемый тип;
- иметь в наличии только один конструктор (который только присваивает параметры в поля);
- возвращать объекты из некоторого кеша, а не всегда создавать объекты.
«Effective Java» — замечательная книга. А наличие final
и nullability-аннотаций — это дело привычки, и как бы мне хотелось считать — привычки хорошего тона.
И последнее. В @PreAuthorize
я предпочитаю использовать просто вызовы методов авторизирующих компонентов вместо сложных выражений. Во-первых, это позволяет дать некоторое имя правилу авторизации, а также избежать в некоторых случаях строковых констант, которые наверняка захочется использовать (но лично я считаю, что не надо). Во-вторых, это позволяет собирать правила авторизации в определённые группы и соответствующе к ним обращаться. В-третьих, правила авторизации могут быть слишком длинными, что вряд ли упростит сопровождение таких выражений в @PreAuthorize
. К тому же, компилятор скорее сам отловит имеющиеся ошибки, не оставляя их на время выполнения. Да и любимая IDE лучше раскрасит такие выражения.