【问题标题】:Adding additional Spring Security method annotations添加额外的 Spring Security 方法注解
【发布时间】:2021-06-21 09:13:11
【问题描述】:

我正在编写一个库,它使用 Spring Security 和方法安全性来检查用户是否被许可执行特定操作。这是对通常基于角色的安全性的补充,这会导致问题。

注释看起来就像他们在这个测试类中所做的那样:

@RestController
class TestController {

    @RolesAllowed("ROLE_USER")
    @Licensed("a")
    public ResponseEntity<String> a() {
        return ResponseEntity.ok("a");
    }

    @RolesAllowed("ROLE_USER")
    @Licensed("b")
    public ResponseEntity<String> b() {
        return ResponseEntity.ok("b");
    }

    @RolesAllowed("ROLE_USER")
    @Licensed("c")
    public ResponseEntity<String> c() {
        return ResponseEntity.ok("c");
    }
}

处理注释似乎很简单,因为您添加了customMethodSecurityDataSource:

@EnableGlobalMethodSecurity(
        securedEnabled = true,
        jsr250Enabled = true,
        prePostEnabled = true
)
@Configuration
public class LicenceSecurityConfiguration extends GlobalMethodSecurityConfiguration {

    @Override protected MethodSecurityMetadataSource customMethodSecurityMetadataSource() {
        return new LicensedAnnotationSecurityMetadataSource();
    }

    // more configurations
}

但问题在于 Spring 的实现:

@Override
public Collection<ConfigAttribute> getAttributes(Method method, Class<?> targetClass) {
    DefaultCacheKey cacheKey = new DefaultCacheKey(method, targetClass);
    synchronized (this.attributeCache) {
        Collection<ConfigAttribute> cached = this.attributeCache.get(cacheKey);
        // Check for canonical value indicating there is no config attribute,
        if (cached != null) {
            return cached;
        }
        // No cached value, so query the sources to find a result
        Collection<ConfigAttribute> attributes = null;
        for (MethodSecurityMetadataSource s : this.methodSecurityMetadataSources) {
            attributes = s.getAttributes(method, targetClass);
            if (attributes != null && !attributes.isEmpty()) {
                break;
            }
        }
        // Put it in the cache.
        if (attributes == null || attributes.isEmpty()) {
            this.attributeCache.put(cacheKey, NULL_CONFIG_ATTRIBUTE);
            return NULL_CONFIG_ATTRIBUTE;
        }
        this.logger.debug(LogMessage.format("Caching method [%s] with attributes %s", cacheKey, attributes));
        this.attributeCache.put(cacheKey, attributes);
        return attributes;
    }

首先处理我的自定义元数据源,一旦找到它识别的注释,它就会停止处理。具体来说,在这个 if 块中:

if (attributes != null && !attributes.isEmpty()) {
    break;
}

结果是我的LicenceDecisionVoter投弃权票;毕竟,可能还有其他检查角色的注释处理器。并且因为没有更多的属性可以投票,所以只返回ACCESS_ABSTAIN,并且根据 Spring 的默认和推荐配置,拒绝访问。从不检查角色。

除了对 Spring 自己的注解处理器(如 @Secured 和 JSR-250 注解)实施扫描之外,我还有其他选择吗?

或者为了这个特定目的首先使用 Spring Security 是错误的?

【问题讨论】:

  • 澄清一下——你为什么不使用@PreAuthorize/@PostAuthorize
  • 因为我认为任何许可问题都可以简化为布尔值,因此您只需要一个标签。因此,如果许可证允许 40 个用户,您需要检查isAllowedToAddUser,或类似的东西。然而,@Preauthorize 并不能解决问题,因为一旦 Spring 找到它识别的注解,它就会停止处理。因此,我必须构建 @Preauthorize 注释,使其涵盖基于角色的访问和许可访问。看起来……不优雅。
  • @PreAuthorize 中写入适当的安全表达式,例如@PreAuthorize("hasRole('USER') and @licenseChecker.isLicensed('a')"。不需要自定义选民(现在建议不要这样做),但使用适当的表达方式。
  • 我对 Spring 的最大问题是过多的文章和 SO 问题在较新的 Spring 版本中不能很好地老化。那么有什么反对自定义选民的呢?我会检查表达式,但它可能需要对代码进行根本性的重写。你知道,只要你改变一件事......
  • 为什么需要重写?您已经获得了所需的逻辑(即在您的 Voter 中)。您只需将该选民设为一个 bean(而不是真正的 RoleVoter)并调用该方法(它应该返回一个布尔值)。反对选民的最大理由是他们过于简单化。您可以编写非常复杂的安全表达式(如 IP 地址检查、IP 范围、角色/权限等),这对于选民来说是不可能的。

标签: java spring spring-boot spring-security annotations


【解决方案1】:

正如承诺的那样,解决方案。这比我想象的要多,而且代码可能有问题,因为它部分是从 Spring 复制的,而且其中一些代码看起来很狡猾(或者至少,IntelliJ 认为是这样)。

关键是删除GlobalMethodSecurityConfiguration。把它留给应用程序本身。 (自动)配置类如下所示:

@EnableConfigurationProperties(LicenceProperties.class)
@Configuration
@Import(LicensedMetadataSourceAdvisorRegistrar.class)
public class LicenceAutoConfiguration {

    @Bean public <T extends Licence> LicenceChecker<T> licenceChecker(
            @Lazy @Autowired final LicenceProperties properties,
            @Lazy @Autowired final LicenceFactory<T> factory
    ) throws InvalidSignatureException, LicenceExpiredException, WrappedApiException,
             IOException, ParseException, InvalidKeySpecException {
        final LicenceLoader loader = new LicenceLoader(factory.getPublicKey());
        final T licence = loader.load(properties.getLicenceFile(), factory.getType());
        return factory.getChecker(licence);
    }

    @Bean MethodSecurityInterceptor licenceSecurityInterceptor(
            final LicensedMetadataSource metadataSource,
            final LicenceChecker<?> licenceChecker
    ) {
        final MethodSecurityInterceptor interceptor = new MethodSecurityInterceptor();
        interceptor.setAccessDecisionManager(decisionManager(licenceChecker));
        interceptor.setSecurityMetadataSource(metadataSource);
        return interceptor;
    }

    @Bean LicenceAccessDecisionManager decisionManager(@Autowired final LicenceChecker<?> licenceChecker) {
        return new LicenceAccessDecisionManager(licenceChecker);
    }

    @Bean LicensedMetadataSource licensedMetadataSource() {
        return new LicensedMetadataSource();
    }
}

注册商:

public class LicensedMetadataSourceAdvisorRegistrar implements ImportBeanDefinitionRegistrar {

    @Override
    public void registerBeanDefinitions(final AnnotationMetadata importingClassMetadata,
                                        final BeanDefinitionRegistry registry) {
        final BeanDefinitionBuilder advisor = BeanDefinitionBuilder
                .rootBeanDefinition(LicensedMetadataSourceAdvisor.class);
        advisor.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
        advisor.addConstructorArgReference("licensedMetadataSource");
        registry.registerBeanDefinition("licensedMetadataSourceAdvisor", advisor.getBeanDefinition());
    }
}

最后,顾问:

public class LicensedMetadataSourceAdvisor extends AbstractPointcutAdvisor implements BeanFactoryAware {

    private final LicenceMetadataSourcePointcut pointcut = new LicenceMetadataSourcePointcut();

    private transient LicensedMetadataSource attributeSource;

    private transient BeanFactory beanFactory;
    private transient MethodInterceptor interceptor;

    private transient volatile Object adviceMonitor = new Object();

    public LicensedMetadataSourceAdvisor(final LicensedMetadataSource attributeSource) {
        this.attributeSource = attributeSource;
    }

    @Override public void setBeanFactory(final BeanFactory beanFactory) throws BeansException {
        this.beanFactory = beanFactory;
    }

    @Override public Pointcut getPointcut() {
        return pointcut;
    }

    @Override public Advice getAdvice() {
        synchronized (this.adviceMonitor) {
            if (this.interceptor == null) {
                Assert.state(this.beanFactory != null, "BeanFactory must be set to resolve 'adviceBeanName'");
                this.interceptor = this.beanFactory.getBean("licenceSecurityInterceptor", MethodInterceptor.class);
            }
            return this.interceptor;
        }
    }

    class LicenceMetadataSourcePointcut extends StaticMethodMatcherPointcut implements Serializable {

        @Override public boolean matches(final Method method, final Class<?> targetClass) {
            final LicensedMetadataSource source = LicensedMetadataSourceAdvisor.this.attributeSource;
            final Collection<ConfigAttribute> attributes = source.getAttributes(method, targetClass);
            return attributes != null && !attributes.isEmpty();
        }
    }
}

后两个类是从 Spring 中复制和修改的。顾问是从 MethodSecurityMetadataSourceAdvisor 复制的,这是 Spring 的某个人可能会查看的类,因为 transient volatile 同步对象(我复制了它,因为我还不能确定它是否应该是 final而是),并且因为它有一个从未使用过的私有方法。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-02-03
    • 1970-01-01
    • 2011-07-21
    • 1970-01-01
    • 2014-08-11
    • 2012-01-18
    • 2013-11-23
    相关资源
    最近更新 更多