通过 HTTP 保护 Spring Data RepositoryRestResource (CrudRepository),但不在内部
Posted
技术标签:
【中文标题】通过 HTTP 保护 Spring Data RepositoryRestResource (CrudRepository),但不在内部【英文标题】:Securing a Spring Data RepositoryRestResource (CrudRepository) over HTTP, but not internally 【发布时间】: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);
【问题讨论】:
【参考方案1】:您可以控制对 Spring Data REST 的访问 尝试将 RepositoryDetectionStrategies 更改为“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 QueryMethods 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 类供您在内部使用而不受安全限制。
【讨论】:
当心...它可能会导致许多副作用,例如github.com/spring-projects/spring-boot/issues/2392 或***.com/questions/43494321/… 感谢@Piotr - 我最终推出了自己的休息控制器,因为事情变得太不可预测了。我已经这样编辑了我的帖子。 是的,我自己尝试了该解决方案,但最终将其删除。现在尝试从这里github.com/spring-projects/spring-data-examples/blob/master/…。他们称之为 SecurityUtils.runAs("system", "system", "ROLE_ADMIN");在任何与回购相关的查询之前。还是不错的尝试:) 啊,是的,这听起来很聪明。我已将您的链接添加到我的编辑中:) 干杯。【参考方案3】:您可以尝试使用方法注释 @PreAuthorize("hasRole('ROLE_REST_USER')") 来创建 SecuredServiceInterface
SecuredServiceInterface 将在 REST 控制器中使用,并从您的应用内部使用的 ServiceInterface 扩展而来。
【讨论】:
您好,Oleksandr,感谢您的发帖。我认为这就是我所做的 - 请参阅编辑 2。但有时安全注释已实现,有时它们未实现,我认为这可能是 spring 有时会暴露安全接口的问题,否则会暴露不安全的接口? 我的怀疑是正确的 - 每次我运行时,Spring 都会通过休息公开 UserRepository 或 UserRepositoryRest,在它们之间随机选择......知道如何解决这个问题吗?以上是关于通过 HTTP 保护 Spring Data RepositoryRestResource (CrudRepository),但不在内部的主要内容,如果未能解决你的问题,请参考以下文章
C# 无法通过 HttpResponseMessage 获取 HTTP 返回代码
在 Spring Security(spring-boot 项目)中使用 ldap 凭据进行 Http 基本身份验证以保护休息服务调用
Spring Security 保护路由 hasRole,hasAuthority 不适用于 CORS http 请求