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<ConstructorResolver> constructorResolvers) { standardEvaluationContext().setConstructorResolvers(constructorResolvers); } @Override public List<ConstructorResolver> 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<MethodResolver> methodResolvers) { standardEvaluationContext().setMethodResolvers(methodResolvers); } @Override public List<MethodResolver> 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<PropertyAccessor> propertyAccessors) { standardEvaluationContext().setPropertyAccessors(propertyAccessors); } @Override public List<PropertyAccessor> 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<String, Object> 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<String> 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 <T> 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 <T> 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<MethodInvocation> 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<ConstructorResolver> getConstructorResolvers() { return evaluationContext().getConstructorResolvers(); } @Override public List<MethodResolver> getMethodResolvers() { return evaluationContext().getMethodResolvers(); } @Override public List<PropertyAccessor> 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 лучше раскрасит такие выражения.
