Spring Security:如何启用自定义表达式结果类型支持?

时间:2016-08-04 14:31:31

标签: spring spring-security spring-boot

在我的Spring Boot应用程序中,我在控制器方法中使用@PreAuthorize注释来授权它们。表达式使用简单的boolean - 返回方法,如下所示:

@ResponseStatus(OK)
@PreAuthorize("@auth.authentication.mayReadMe(principal)")
public UserDto readMe() {
    ...

mayReadMe(...)方法只返回一个布尔值,但它使用三元逻辑并只将特殊枚举转换为boolean

boolean mayReadMe(@Nonnull UserDetails principal);

现在假设我想重写授权组件并让方法返回枚举:

@Nonnull
foo.bar.FooBarEnum mayReadMe(@Nonnull final UserDetails principal);

但是,我遇到以下异常:

java.lang.IllegalArgumentException: Failed to evaluate expression '@primaryAuth.authentication.mayReadMe(principal)'
    at org.springframework.security.access.expression.ExpressionUtils.evaluateAsBoolean(ExpressionUtils.java:15)
    at org.springframework.security.access.expression.method.ExpressionBasedPreInvocationAdvice.before(ExpressionBasedPreInvocationAdvice.java:44)
    at org.springframework.security.access.prepost.PreInvocationAuthorizationAdviceVoter.vote(PreInvocationAuthorizationAdviceVoter.java:57)
    at org.springframework.security.access.prepost.PreInvocationAuthorizationAdviceVoter.vote(PreInvocationAuthorizationAdviceVoter.java:25)
    at org.springframework.security.access.vote.AffirmativeBased.decide(AffirmativeBased.java:62)
    at org.springframework.security.access.intercept.AbstractSecurityInterceptor.beforeInvocation(AbstractSecurityInterceptor.java:232)
    at org.springframework.security.access.intercept.aopalliance.MethodSecurityInterceptor.invoke(MethodSecurityInterceptor.java:64)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
    at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:655)
...
Caused by: org.springframework.expression.spel.SpelEvaluationException: EL1001E:(pos 0): Type conversion problem, cannot convert from @javax.annotation.Nonnull foo.bar.FooBarEnum 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:11)
    ... 113 common frames omitted
Caused by: org.springframework.core.convert.ConverterNotFoundException: No converter found capable of converting from type [@javax.annotation.Nonnull foo.bar.FooBarEnum] 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)
    ... 116 common frames omitted

异常消息非常清楚,但我不能以任何方式注入我的自定义转换器。到目前为止我尝试过的:

  • 通过WebMvcConfigurerAdapter.addFormatters(FormatterRegistry)ConverterGenericConverter
  • 注册自定义转换器
  • Bean - 使用自定义ExpressionBasedPreInvocationAdvice(但据我所知,它不应该有效)
  • ......不幸的是,在几个小时之后,我还记不起其他一些方法。

如何注入自定义类型转换器,以便@PreAuthorization表达式可以将foo.bar.FooBarEnum视为返回类型?

编辑1

为什么我需要返回自定义类型,而不是布尔值。我还在编写一个简单的REST API自描述子系统,只是一个简单的GET /api端点来返回端点列表等等。此列表由描述API端点,HTTP方法,传入和传出DTO的特定对象组成,我尝试添加到定义对象的最后一件事是端点授权策略表达式。请注意,返回@PreAuthorize字符串表达式(我的意思是原始字符串)并不是一个好主意,但返回描述授权规则的自定义对象可能会更好。我最想要的是返回一个像这样的对象:

public final class AuthorizationExpression
    implements BooleanSupplier {
    ...
    public IExpression toExpression() {
    ...
}

我希望在我尝试注入的转换器中使用BooleanSupplier以满足授权需求 - 只需返回truefalse,以及IExpression 1}}应该是toString - 使用Spring表达式求值程序在GET /api处理程序中编辑。因此mayReadMe签名可能如下:

AuthorizationExpression mayReadMe(...)

因此我可以将AuthorizationExpression用于某个用例。 FooBarEnum只是编辑前原始问题的简化。

2 个答案:

答案 0 :(得分:0)

建议,让你的枚举实现转换方法:

public enum FooBarEnum {

    // previous code

    public boolean booleanValue() {
        // TODO
    }
}

并更改注释:

@PreAuthorize("@auth.authentication.mayReadMe(principal).booleanValue()")

答案 1 :(得分:0)

想出来。我只需要调整DefaultMethodSecurityExpressionHandler实例。让我们假设净两个类作为库类:

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;
    }

}

其中ForwardingStandardEvaluationContext是一个简单的转发装饰器来装饰StandardEvaluationContext的实例,因为后者是ConversionService - 意识到:

public abstract class ForwardingStandardEvaluationContext
        extends StandardEvaluationContext {

    protected abstract StandardEvaluationContext standardEvaluationContext();

    @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); }

}

然后是几个应用程序类:

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = false)
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;
    }

}

最后转换服务配置:

@Configuration
class ConversionConfiguration {

    @Bean
    public ConversionService conversionService() {
        final DefaultConversionService conversionService = new DefaultConversionService();
        conversionService.addConverter(FooBar.class, Boolean.class, FooBar::mayProceed);
        return conversionService;
    }

}

上面的代码使@PreAuthorize能够理解FooBar - 返回表达式。