通过HTTP而不是内部保护Spring Data RepositoryRestResource(CrudRepository)的安全

Max*_*ord 6 java security spring spring-security spring-boot

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

我已经启动并运行了春季安全性,但是在CrudRepository方法上添加诸如PreAuthorize之类的注释时,也会在我从服务层中调用安全性表达式时执行安全性表达式。

如果有人可以为此指出我正确的方向,我将感到非常高兴。

编辑1

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

编辑2

这是编辑1中描述的代码

UserRepository.java

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

    // .. some extra methods

}
Run Code Online (Sandbox Code Playgroud)

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

}
Run Code Online (Sandbox Code Playgroud)

Max*_*ord 6

编辑:我不再推荐这个 - 我最终只是滚动了我自己的 REST 控制器,因为它变得太hacky 和不可预测。否则,请参阅此处了解可能的替代方案


可以实现这篇文章标题中的目标,但由于 Spring 未正式支持,因此有点复杂。

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

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

错误票: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> { }
Run Code Online (Sandbox Code Playgroud)

请注意,没有安全注释(例如@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);

}
Run Code Online (Sandbox Code Playgroud)

请注意,这里我们使用了安全注释,并且我们使用导出的=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();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

第四步

使用以下内容创建另一个文件:

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

利润

应该这样做 - Spring 现在应该正确地仅导出您的 UserRepositoryRest 类,同时忽略您的 UserRepository 类供您在没有安全限制的情况下在内部使用。