【问题标题】:Securing a Spring Data RepositoryRestResource (CrudRepository) over HTTP, but not internally通过 HTTP 保护 Spring Data RepositoryRestResource (CrudRepository),但不在内部
【发布时间】:2017-09-02 02:12:15
【问题描述】:

我有一个 Spring Data 项目,它使用 RepositoryRestResource 和 CrudRepository's 通过 REST 公开实体。当通过 HTTP 访问存储库时,我需要能够保护它,但在内部使用时(例如,在服务层中)不保证它的安全。

我已启动并运行 Spring Security,但在 CrudRepository 方法上添加 PreAuthorize 等注释也会导致在我从服务层中调用这些方法时执行安全表达式。

如果有人能指出我正确的方向,我会很高兴。

编辑 1

我尝试从 UserRepository 中删除 Rest Export 和安全注释以供内部使用,然后将 UserRepository 子类化为 UserRepositoryRestExported,导出并保护该注释。但是,我在运行之间看到一些不一致的安全注释实现,这让我想知道 Spring 是否有时会导出 UserRepositoryRestExported,而有时会导出 UserRepository ......?

编辑 2

这是编辑 1 中描述的代码

UserRepository.java

@Component("UserRepository")
public interface UserRepository extends CrudRepository<User, Long> {

    // .. some extra methods

}

UserRepositoryRest.java

@Component("UserRepositoryRest")
@RepositoryRestResource(collectionResourceRel = "users", path = "users")
public interface UserRepositoryRest extends UserRepository {

    @PostAuthorize("authentication.name == returnObject.getName() || hasRole('ROLE_ADMIN')")
    @Override
    User findOne(Long id);

    @PostFilter("authentication.name == filterObject.getName() || hasRole('ROLE_ADMIN')")
    @Override
    Iterable<User> findAll();

    @PreAuthorize("principal.getCell() == #user.getName() || hasRole('ROLE_ADMIN')")
    @Override
    void delete(@P("user") User user);

    User save(User entity);

    long count();

    boolean exists(Long primaryKey);

}

【问题讨论】:

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


    【解决方案1】:

    您可以控制对 Spring Data REST 的访问 尝试将 RepositoryDe​​tectionStrategies 更改为“ANNOTATED”,并确保将导出的标志设置为 true,以便导出您想要导出的存储库,例如:https://www.javabullets.com/4-ways-to-control-access-to-spring-data-rest/

    【讨论】:

    • 嗨!一般来说,总结答案中的内容比重定向到一个 url 更好,因为该链接将来可能会变得无效或更改。
    【解决方案2】:

    编辑:我不再推荐这个了 - 我最终只是滚动了我自己的 REST 控制器,因为它变得太 hacky 和不可预测。否则see here for a possible alternative


    本帖标题中的目标是可以实现的,但是由于没有Spring官方支持,所以有点复杂。

    大致来说,您必须创建两个存储库,一个供内部使用,一个(安全)供外部使用。然后你必须修改spring,让它只导出一个供外部使用。

    大部分代码来自下面链接的帖子;非常感谢 Will Faithful 提出解决方案:

    Bug 票:https://jira.spring.io/browse/DATAREST-923

    修复存储库:https://github.com/wfaithfull/spring-data-rest-multiple-repositories-workaround

    第 1 步

    创建仅供内部使用的不安全、非导出的存储库:

    @RepositoryRestResource(exported = false)
    @Component("UserRepository")
    public interface UserRepository extends CrudRepository<User, Long> { }
    

    请注意,没有安全注释(例如@PreAuthorized)并且@RepositoryRestResource 设置为exported=false。

    第 2 步

    创建安全的、导出的存储库以仅通过 HTTP REST 使用:

    @Component("UserRepositoryRest")
    @Primary
    @RepositoryRestResource(collectionResourceRel = "users", path = "users", exported = true)
    public interface UserRepositoryRest extends UserRepository {
    
        @PostAuthorize(" principal.getUsername() == returnObject.getUsername() || hasRole('ROLE_ADMIN') ")
        @Override
        User findOne(Long id);
    
    }
    

    注意这里我们使用了安全注释,并且我们使用exported=true 明确地导出了存储库。

    第 3 步

    这是有点复杂的地方。如果您停在这里,Spring 有时会加载并尝试导出您的 UserRepository 类,有时会加载并尝试导出您的 UserRepositoryRest 类。这可能会导致单元测试偶尔失败(大约 50% 的时间),以及其他奇怪的副作用,使这难以追踪。

    我们将通过调整 Spring 选择导出存储库的方式来解决此问题。创建一个包含以下内容的文件:

    import org.springframework.beans.factory.BeanFactory;
    import org.springframework.beans.factory.BeanFactoryUtils;
    import org.springframework.beans.factory.ListableBeanFactory;
    import org.springframework.beans.factory.support.DefaultListableBeanFactory;
    import org.springframework.data.mapping.PersistentEntity;
    import org.springframework.data.repository.core.EntityInformation;
    import org.springframework.data.repository.core.RepositoryInformation;
    import org.springframework.data.repository.core.support.RepositoryFactoryInformation;
    import org.springframework.data.repository.query.QueryMethod;
    import org.springframework.data.repository.support.Repositories;
    import org.springframework.data.rest.core.annotation.RepositoryRestResource;
    import org.springframework.util.Assert;
    import org.springframework.util.ClassUtils;
    
    import java.io.Serializable;
    import java.util.*;
    
    /**
     * @author Will Faithfull
     *
     * Warning: Ugly hack territory.
     *
     * Firstly, I can't just swap out this implementation, because Repositories is referenced everywhere directly without an
     * interface.
     *
     * Unfortunately, the offending code is in a private method, {@link #cacheRepositoryFactory(String)}, and modifies private
     * fields in the Repositories class. This means we can either use reflection, or replicate the functionality of the class.
     *
     * In this instance, I've chosen to do the latter because it's simpler, and most of this code is a simple copy/paste from
     * Repositories. The superclass is given an empty bean factory to satisfy it's constructor demands, and ensure that
     * it will keep as little redundant state as possible.
     */
    public class ExportAwareRepositories extends Repositories {
    
        static final Repositories NONE = new ExportAwareRepositories();
    
        private static final RepositoryFactoryInformation<Object, Serializable> EMPTY_REPOSITORY_FACTORY_INFO = EmptyRepositoryFactoryInformation.INSTANCE;
        private static final String DOMAIN_TYPE_MUST_NOT_BE_NULL = "Domain type must not be null!";
    
        private final BeanFactory beanFactory;
        private final Map<Class<?>, String> repositoryBeanNames;
        private final Map<Class<?>, RepositoryFactoryInformation<Object, Serializable>> repositoryFactoryInfos;
    
        /**
         * Constructor to create the {@link #NONE} instance.
         */
        private ExportAwareRepositories() {
            /* Mug off the superclass with an empty beanfactory to placate the Assert.notNull */
            super(new DefaultListableBeanFactory());
            this.beanFactory = null;
            this.repositoryBeanNames = Collections.<Class<?>, String> emptyMap();
            this.repositoryFactoryInfos = Collections.<Class<?>, RepositoryFactoryInformation<Object, Serializable>> emptyMap();
        }
    
        /**
         * Creates a new {@link Repositories} instance by looking up the repository instances and meta information from the
         * given {@link ListableBeanFactory}.
         *
         * @param factory must not be {@literal null}.
         */
        public ExportAwareRepositories(ListableBeanFactory factory) {
            /* Mug off the superclass with an empty beanfactory to placate the Assert.notNull */
            super(new DefaultListableBeanFactory());
            Assert.notNull(factory, "Factory must not be null!");
    
            this.beanFactory = factory;
            this.repositoryFactoryInfos = new HashMap<Class<?>, RepositoryFactoryInformation<Object, Serializable>>();
            this.repositoryBeanNames = new HashMap<Class<?>, String>();
    
            populateRepositoryFactoryInformation(factory);
        }
    
        private void populateRepositoryFactoryInformation(ListableBeanFactory factory) {
    
            for (String name : BeanFactoryUtils.beanNamesForTypeIncludingAncestors(factory, RepositoryFactoryInformation.class,
                    false, false)) {
                cacheRepositoryFactory(name);
            }
        }
    
        @SuppressWarnings({ "rawtypes", "unchecked" })
        private synchronized void cacheRepositoryFactory(String name) {
    
            RepositoryFactoryInformation repositoryFactoryInformation = beanFactory.getBean(name,
                    RepositoryFactoryInformation.class);
            Class<?> domainType = ClassUtils
                    .getUserClass(repositoryFactoryInformation.getRepositoryInformation().getDomainType());
    
            RepositoryInformation information = repositoryFactoryInformation.getRepositoryInformation();
            Set<Class<?>> alternativeDomainTypes = information.getAlternativeDomainTypes();
            String beanName = BeanFactoryUtils.transformedBeanName(name);
    
            Set<Class<?>> typesToRegister = new HashSet<Class<?>>(alternativeDomainTypes.size() + 1);
            typesToRegister.add(domainType);
            typesToRegister.addAll(alternativeDomainTypes);
    
            for (Class<?> type : typesToRegister) {
                // I still want to add repositories if they don't have an exported counterpart, so we eagerly add repositories
                // but then check whether to supercede them. If you have more than one repository with exported=true, clearly
                // the last one that arrives here will be the registered one. I don't know why anyone would do this though.
                if(this.repositoryFactoryInfos.containsKey(type)) {
                    Class<?> repoInterface = information.getRepositoryInterface();
                    if(repoInterface.isAnnotationPresent(RepositoryRestResource.class)) {
                        boolean exported = repoInterface.getAnnotation(RepositoryRestResource.class).exported();
    
                        if(exported) { // Then this has priority.
                            this.repositoryFactoryInfos.put(type, repositoryFactoryInformation);
                            this.repositoryBeanNames.put(type, beanName);
                        }
                    }
                } else {
                    this.repositoryFactoryInfos.put(type, repositoryFactoryInformation);
                    this.repositoryBeanNames.put(type, beanName);
                }
            }
        }
    
        /**
         * Returns whether we have a repository instance registered to manage instances of the given domain class.
         *
         * @param domainClass must not be {@literal null}.
         * @return
         */
        @Override
        public boolean hasRepositoryFor(Class<?> domainClass) {
    
            Assert.notNull(domainClass, DOMAIN_TYPE_MUST_NOT_BE_NULL);
    
            return repositoryFactoryInfos.containsKey(domainClass);
        }
    
        /**
         * Returns the repository managing the given domain class.
         *
         * @param domainClass must not be {@literal null}.
         * @return
         */
        @Override
        public Object getRepositoryFor(Class<?> domainClass) {
    
            Assert.notNull(domainClass, DOMAIN_TYPE_MUST_NOT_BE_NULL);
    
            String repositoryBeanName = repositoryBeanNames.get(domainClass);
            return repositoryBeanName == null || beanFactory == null ? null : beanFactory.getBean(repositoryBeanName);
        }
    
        /**
         * Returns the {@link RepositoryFactoryInformation} for the given domain class. The given <code>code</code> is
         * converted to the actual user class if necessary, @see ClassUtils#getUserClass.
         *
         * @param domainClass must not be {@literal null}.
         * @return the {@link RepositoryFactoryInformation} for the given domain class or {@literal null} if no repository
         *         registered for this domain class.
         */
        private RepositoryFactoryInformation<Object, Serializable> getRepositoryFactoryInfoFor(Class<?> domainClass) {
    
            Assert.notNull(domainClass, DOMAIN_TYPE_MUST_NOT_BE_NULL);
    
            Class<?> userType = ClassUtils.getUserClass(domainClass);
            RepositoryFactoryInformation<Object, Serializable> repositoryInfo = repositoryFactoryInfos.get(userType);
    
            if (repositoryInfo != null) {
                return repositoryInfo;
            }
    
            if (!userType.equals(Object.class)) {
                return getRepositoryFactoryInfoFor(userType.getSuperclass());
            }
    
            return EMPTY_REPOSITORY_FACTORY_INFO;
        }
    
        /**
         * Returns the {@link EntityInformation} for the given domain class.
         *
         * @param domainClass must not be {@literal null}.
         * @return
         */
        @SuppressWarnings("unchecked")
        @Override
        public <T, S extends Serializable> EntityInformation<T, S> getEntityInformationFor(Class<?> domainClass) {
    
            Assert.notNull(domainClass, DOMAIN_TYPE_MUST_NOT_BE_NULL);
    
            return (EntityInformation<T, S>) getRepositoryFactoryInfoFor(domainClass).getEntityInformation();
        }
    
        /**
         * Returns the {@link RepositoryInformation} for the given domain class.
         *
         * @param domainClass must not be {@literal null}.
         * @return the {@link RepositoryInformation} for the given domain class or {@literal null} if no repository registered
         *         for this domain class.
         */
        @Override
        public RepositoryInformation getRepositoryInformationFor(Class<?> domainClass) {
    
            Assert.notNull(domainClass, DOMAIN_TYPE_MUST_NOT_BE_NULL);
    
            RepositoryFactoryInformation<Object, Serializable> information = getRepositoryFactoryInfoFor(domainClass);
            return information == EMPTY_REPOSITORY_FACTORY_INFO ? null : information.getRepositoryInformation();
        }
    
        /**
         * Returns the {@link RepositoryInformation} for the given repository interface.
         *
         * @param repositoryInterface must not be {@literal null}.
         * @return the {@link RepositoryInformation} for the given repository interface or {@literal null} there's no
         *         repository instance registered for the given interface.
         * @since 1.12
         */
        @Override
        public RepositoryInformation getRepositoryInformation(Class<?> repositoryInterface) {
    
            for (RepositoryFactoryInformation<Object, Serializable> factoryInformation : repositoryFactoryInfos.values()) {
    
                RepositoryInformation information = factoryInformation.getRepositoryInformation();
    
                if (information.getRepositoryInterface().equals(repositoryInterface)) {
                    return information;
                }
            }
    
            return null;
        }
    
        /**
         * Returns the {@link PersistentEntity} for the given domain class. Might return {@literal null} in case the module
         * storing the given domain class does not support the mapping subsystem.
         *
         * @param domainClass must not be {@literal null}.
         * @return the {@link PersistentEntity} for the given domain class or {@literal null} if no repository is registered
         *         for the domain class or the repository is not backed by a {@link MappingContext} implementation.
         */
        @Override
        public PersistentEntity<?, ?> getPersistentEntity(Class<?> domainClass) {
    
            Assert.notNull(domainClass, DOMAIN_TYPE_MUST_NOT_BE_NULL);
            return getRepositoryFactoryInfoFor(domainClass).getPersistentEntity();
        }
    
        /**
         * Returns the {@link QueryMethod}s contained in the repository managing the given domain class.
         *
         * @param domainClass must not be {@literal null}.
         * @return
         */
        @Override
        public List<QueryMethod> getQueryMethodsFor(Class<?> domainClass) {
    
            Assert.notNull(domainClass, DOMAIN_TYPE_MUST_NOT_BE_NULL);
            return getRepositoryFactoryInfoFor(domainClass).getQueryMethods();
        }
    
        /*
         * (non-Javadoc)
         * @see java.lang.Iterable#iterator()
         */
        @Override
        public Iterator<Class<?>> iterator() {
            return repositoryFactoryInfos.keySet().iterator();
        }
    
        /**
         * Null-object to avoid nasty {@literal null} checks in cache lookups.
         *
         * @author Thomas Darimont
         */
        private static enum EmptyRepositoryFactoryInformation implements RepositoryFactoryInformation<Object, Serializable> {
    
            INSTANCE;
    
            @Override
            public EntityInformation<Object, Serializable> getEntityInformation() {
                return null;
            }
    
            @Override
            public RepositoryInformation getRepositoryInformation() {
                return null;
            }
    
            @Override
            public PersistentEntity<?, ?> getPersistentEntity() {
                return null;
            }
    
            @Override
            public List<QueryMethod> getQueryMethods() {
                return Collections.<QueryMethod> emptyList();
            }
        }
    }
    

    第 4 步

    创建另一个包含以下内容的文件:

    import me.faithfull.hack.ExportAwareRepositories;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.data.repository.support.Repositories;
    import org.springframework.data.rest.webmvc.config.RepositoryRestMvcConfiguration;
    
    /**
     * @author Will Faithfull
     */
    @Configuration
    public class RepositoryRestConfiguration extends RepositoryRestMvcConfiguration {
    
        @Autowired
        ApplicationContext context;
    
        /**
         * We replace the stock repostiories with our modified subclass.
         */
        @Override
        public Repositories repositories() {
            return new ExportAwareRepositories(context);
        }
    }
    

    利润

    应该这样做 - Spring 现在应该正确地只导出您的 UserRepositoryRest 类,同时忽略您的 UserRepository 类供您在内部使用而不受安全限制。

    【讨论】:

    【解决方案3】:

    您可以尝试使用方法注释 @PreAuthorize("hasRole('ROLE_REST_USER')") 来创建 SecuredServiceInterface

    SecuredServiceInterface 将在 REST 控制器中使用,并从您的应用内部使用的 ServiceInterface 扩展而来。

    【讨论】:

    • 您好,Oleksandr,感谢您的发帖。我认为这就是我所做的 - 请参阅编辑 2。但有时安全注释已实现,有时它们未实现,我认为这可能是 spring 有时会暴露安全接口的问题,否则会暴露不安全的接口?
    • 我的怀疑是正确的 - 每次我运行时,Spring 都会通过休息公开 UserRepository 或 UserRepositoryRest,在它们之间随机选择......知道如何解决这个问题吗?
    猜你喜欢
    • 2021-07-14
    • 2021-12-16
    • 2019-02-27
    • 2015-06-19
    • 2016-04-17
    • 2016-12-18
    • 2019-12-12
    • 2015-12-25
    • 1970-01-01
    相关资源
    最近更新 更多