Прокачка @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 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 лучше раскрасит такие выражения.

Комментарии (0)

© Habrahabr.ru