2017-04-06 41 views
0

我有一個Spring Data項目,它使用RepositoryRestResource和CrudRepository來通過REST公開實體。當需要通過HTTP訪問存儲庫時,我需要能夠保護存儲庫,但在內部使用時(例如,在服務層中),不能保證存儲庫的安全。通過HTTP(但不是內部)保護Spring Data RepositoryRestResource(CrudRepository)

我有彈簧安全啓動和運行,但在CrudRepository方法中添加像PreAuthorize一樣的註釋也會導致在我從服務層調用這些方法時執行安全表達式。

如果有人能指出我正確的方向,我會很高興。

編輯1

我試着從UserRepository取出,其餘的出口和安全註釋內部使用,則繼承UserRepository爲UserRepositoryRestExported,出口和固定的那一個。然而,我看到一些不一致的運行之間的安全註釋實現,這讓我懷疑Spring是否有時導出UserRepositoryRestExported,其他時候導出UserRepository ...?

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

} 

回答

1

編輯:我不推薦這個了 - 我結束了我自己的REST控制器,因爲它變得太hacky和不可預知。否則see here for a possible alternative


可以在本文的標題中實現目標,但它有點複雜,因爲沒有得到Spring的官方支持。

作爲一個粗略概述,您必須創建兩個存儲庫,一個供內部使用,另一個(安全)供外部使用。然後你必須修改彈簧,以便它只輸出外部使用的彈簧。

大部分代碼來自下面的鏈接;一個巨大的感謝威爾忠實於未來與修復:

的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> { } 

請注意有n o安全註釋(例如@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); 

} 

注意我們正在使用的安全註解,我們也明確地把資源庫與出口=真正。

步驟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類可以在沒有安全限制的情況下在內部使用。

+1

請注意...它可能會導致很多副作用,例如https://github.com/spring-projects/spring-boot/issues/2392或https://stackoverflow.com/questions/43494321/extending-repositoryrestmvcconfiguration -breaks-jackson-localdatetime-serialisat – Piotr

+0

謝謝@Piotr - 最後我終於把我自己的休息控制器拖到了最後,因爲事情變得太難以預料了。我編輯我的帖子這樣說。 –

+0

是的,我自己試過這個解決方案,但最終刪除了它。現在嘗試從這裏開始https://github.com/spring-projects/spring-data-examples/blob/master/rest/security/src/main/java/example/springdata/rest/security/Application.java#L57 。他們稱之爲SecurityUtils.runAs(「system」,「system」,「ROLE_ADMIN」);在任何回購相關的查詢之前。不錯的嘗試:) – Piotr

0

你可以嘗試創建SecuredServiceInterface與所描述的代碼方法註釋ation @PreAuthorize(「hasRole('ROLE_REST_USER')」)

SecuredServiceInterface將在REST控制器中使用,並從您的應用程序內部使用的ServiceInterface擴展。

+0

嗨Oleksandr,感謝張貼。我認爲這就是我所做的 - 見編輯2.然而,有時安全註釋是實現的,有時它們不是,我認爲這可能是春天有時暴露安全接口的問題,否則暴露不安全的接口? –

+0

我的可疑是正確的 - 每次運行時,Spring將通過休息公開UserRepository或UserRepositoryRest,隨機選擇它們......任何想法如何解決這個問題? –

相關問題