限制只有REST访问能够安全地访问Spring Data Rest存储库

11

我正在使用Spring Data Rest来公开一个仓库。我使用@PreAuthorize@PostFilter来限制对REST端点的访问仅局限于管理员用户并过滤结果。

@PreAuthorize("hasRole('ROLE_ADMIN')")
@PostFilter("hasPermission(filterObject, 'read')
public interface SomeRepository extends CrudRepository<SomeEntity, Long> {
}

与此同时,我有另一个控制器不需要任何身份验证,但是使用了存储库。

@Controller
public class SomeController {

 @Autowired
 SomeRepository repository;

 @RequestMapping(value = "/test")
 public ResponseEntity test () {
 // Do something
 repository.findAll();
 // Do something else
 }
}

这不起作用是因为发送到 "/test" 的请求的用户不是管理员,因此它没有访问该存储库的权限。

我的问题是,在应用程序内部使用存储库时,是否可能仅向存储库的REST接口添加安全性而不是整个应用程序?

谢谢

8个回答

8
请评估以下可能性:
  • 在REST事件处理程序中进行安全检查
  • 添加自定义存储库方法供内部使用
  • 使用RunAsManager(或临时切换SecurityContext以执行特权操作)
使用REST事件处理程序来保护修改请求:
@Service
@RepositoryEventHandler
public class FooService {

  /**
   * Handles before-* events.
   */
  @HandleBeforeCreate
  @HandleBeforeSave
  @HandleBeforeDelete
  @PreAuthorize("hasRole('ADMIN')")
  public void onBeforeModify(final Foo entity){
    // noop
  }

  /**
   * Handles before-* events.
   */
  @HandleBeforeLinkSave
  @HandleBeforeLinkDelete
  @PreAuthorize("hasRole('ADMIN')")
  public void onBeforeModifyLink(final Foo entity, final Object linked){
    // noop
  }
}

在添加非安全的自定义方法以供内部使用时,保护标准CRUD方法:

仓库

(repository)

public interface FooDao extends CrudRepository<Foo, Long> {

 @Override
 @PreAuthorize("hasRole('ADMIN')")
 <S extends Foo> S save(final S entity);

  /**
   * Saves entity without security checks.
   */
  @Transactional
  @Modifying
  default <S extends Foo> S saveInternal(final S entity) {
    return save(entity);
  }
}

我已经编辑了答案,“@HandleBeforeLinkSave”和“@HandleBeforeLinkDelete”需要两个参数的方法。 - Kirill Rakhman

2
一种解决方案是从您的存储库接口中删除@PreAuthorize注释,并在配置类中扩展WebSecurityConfigAdaptor并覆盖configure(HttpSecurity security)方法。从这里,您可以使用AntMatcher根据需要对REST端点施加访问限制。例如:
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests().antMatchers("/someEntities/**").hasRole('ADMIN')
    .anyRequest().permitAll();   
}

查看http://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#jc-httpsecurity获取更多详细信息。


谢谢Adam,这是使用@PreAuthorize的正确解决方案,但我正在寻找其他东西,因为我也在使用@PostFilter。我会更新我的问题。 - Gerardo

1
我遇到了同样的问题,并想出了一种解决方法,虽然不完全正确,但可以暂时应付。
我基本上创建了一个安全工具bean,可以用于检查是否使用Spring Data REST API调用了内部或外部方法(注:我的repository前缀为/api/,如果您有另一个前缀,则需要相应地更改正则表达式)。
@Component("securityUtils")
public class SecurityUtils {
    public boolean isRestRequest(){
        HttpServletRequest r = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
        return Pattern.matches("^/api/", UrlUtils.buildRequestUrl(r));
    }
}

为了使其工作,您需要将以下行添加到web.xml中的侦听器中:
<listener-class>org.springframework.web.context.request.RequestContextListener</listener-class>

在基于表达式的访问控制中使用以下方法(其中表达式中的最后一行允许您从任何控制器方法中使用 save 方法,这些方法映射到不以 / api / 开头的URL):
@Override
@PreAuthorize("hasRole('ROLE_ADMINISTRATOR') " +
        "or hasPermission(#user, 'WRITE') " +
        "or !@securityUtils.isRestRequest()")
<S extends User> S save(@P("user") S user);

注意事项:

  1. 当您想要在/api路由上公开自定义功能时,不能使用此功能,因为它仅是对路由的简单正则表达式检查。
  2. 必须明确地将此检查添加到每个存储库或存储库方法中,以便在内部省略授权检查(这也可能是一个优点)。

1
在我看来,正确的解决方案应该是有两个存储库,一个称为EntityRepository,另一个是SecuredEntityRepository。
例如:
@RestResource(exported = false)
public abstract interface CustomerRepository extends JpaRepository<Customer, Long> {

}

以及安全版本:

@RestResource(exported = true)
public abstract interface SecuredCustomerRepository extends CustomerRepository {

    @Override
    @PreAuthorize("#id == principal.customer.id or hasAuthority('ADMIN_CUSTOMER_ONE')")
    public Customer findOne(@Param("id") Long id);

    @Override
    @Query("SELECT o FROM #{#entityName} o WHERE o.id = ?#{principal.customer.id} or 1 = ?#{ hasAuthority('ADMIN_CUSTOMER_LIST') ? 1 : 0 }")
    public Page<Customer> findAll(Pageable pageable);

    @Override
    @SuppressWarnings("unchecked")
    @PreAuthorize("#customer.id == principal.customer.id or hasAuthority('ADMIN_CUSTOMER_SAVE')")
    public Customer save(@P("customer") Customer customer);

    @Override
    @PreAuthorize("hasAuthority('ADMIN_CUSTOMER_DELETE')")
    public void delete(@Param("id") Long id);

    @Override
    @PreAuthorize("hasAuthority('ADMIN_CUSTOMER_DELETE')")
    public void delete(Customer customer);

}

由于SD REST中自动装配机制存在问题,目前无法实现此功能:https://jira.spring.io/browse/DATAREST-923


0
我用以下代码装饰了存储库类:
@PreAuthorize("hasRole('admin')")
这将锁定所有内容。
然后,我像这样装饰想要启用内部使用但不是 REST 的内容:
@Transactional
@Modifying
@PreAuthorize("hasRole('user')")
@RestResource(exported = false)
default <S extends SomeEntity> S saveInternal(final S entity) {
        return save(entity);
}

而我想通过Rest接口公开的任何内容(精选几个)都是通过以下方式公开的:

@PreAuthorize("(hasRole('user')) and 
               (#entity.user.username == principal.name)")
@Override
<S extends SomeEntity> S save(@Param("entity") S entity);

请注意,这也验证了您正在保存授权保存的记录。

0

当然可以。只需要更改@PreAuthorize注释的位置即可。该注释可以放置在类或单个方法中。

例如:

@Controller
public class SomeController {

 @Autowired
 SomeRepository repository;

 @RequestMapping(value = "/test")
 @PreAuthorize(....)
 public ResponseEntity test () {
 // Do something
 repository.findAll();
 // Do something else
 }
}

是完全合法的(请注意test()方法的注释)。


1
仓库端点由Spring Data Rest自动公开,它们没有任何控制器来放置注释。这个解决方案适用于我的自定义控制器,我已经在使用它了。问题出现在仓库公开的端点上。如何限制这些端点而不限制其他类对仓库的访问。谢谢。 - Gerardo
@Gerardo 好的,现在我明白你的问题了。很抱歉我错过了那个基本细节 :-) 说实话,我从未使用过Spring Data Rest本身。我认为Spring Data REST事件处理程序是为像你这样的目的而设计的。在这里看一下 https://jaxenter.com/rest-api-spring-java-8-112289.html - Stefano Cazzola

0
在我的团队中,我们评估了这篇文章中的几个答案,但它们都不适用于我们的情况。
Johannes Hiemer的一个变体answer对我们有用。我们配置了Spring Data REST仅公开已注释的存储库:
 data.rest:
    detection-strategy: annotated

然后我们定义了两个没有层次关系的存储库。

其中一个存储库将通过向其添加@RepositoryRestResource注释来公开。对于这个存储库,我们默认拒绝访问每个方法,因此必须在方法级别上指定身份验证,以减少错误地暴露方法的机会。例如,最初我们扩展了CrudRepository并不想公开删除操作:

@RepositoryRestResource
@PreAuthorize("denyAll()")
interface SomeRestResourceRepository : Repository<SomeEntity, Long> {
}

用于内部调用的存储库被定义为常规的Spring Data存储库:

interface SomeRepository : Repository<SomeEntity, Long> {
}

我们正在使用spring-boot-starter-data-rest 2.6.3。


0

我通过添加自己的检查来解决了这个问题。我创建了自己的AbstractHttpConfigurer类,具有全局安全性。我声明了可以公开访问的方法。

public class CommonSpringKeycloakTutorialsSecurityAdapter extends AbstractHttpConfigurer<CommonSpringKeycloakTutorialsSecurityAdapter, HttpSecurity> {

public static String[] PERMIT_ALL_URL = {"/api/user/createUser"};

    @Override
    public void init(HttpSecurity http) throws Exception {
        // any method that adds another configurer
        // must be done in the init method
        http
                // disable csrf because of API mode
                .csrf().disable()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)

                .and()
                // manage routes securisation here
                .authorizeRequests().antMatchers(HttpMethod.OPTIONS).permitAll()

                // manage routes securisation here
                .and()
                .authorizeRequests()
                .antMatchers(HttpMethod.OPTIONS).permitAll()
                .antMatchers("/swagger-ui.html*", "/swagger-ui/**", "/v3/api-docs/**").permitAll()
                .antMatchers(PERMIT_ALL_URL).permitAll()
                .anyRequest().authenticated();

    }

然后我基于全局权限创建了自己的检查。

@Component("securityUtils")
public class SecurityUtils {
    public boolean isPermitRestRequest(){
        HttpServletRequest r = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
       String currentUrl = UrlUtils.buildRequestUrl(r);
        for(String url: CommonSpringKeycloakTutorialsSecurityAdapter.PERMIT_ALL_URL)  {
                if(currentUrl.equals(url)) {
                    return true;
                }
        }
        return false;
    }
}

为了使本地验证起作用,请包含一个监听器

@WebListener
public class MyRequestContextListener extends RequestContextListener {
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接