专门保护对 Spring 数据 Rest 存储库的 REST 访问

Securing exclusively the REST access to a Spring Data Rest Repository

我正在使用 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 接口添加安全性,而不是在应用程序内部使用存储库时?

谢谢

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

例如

@Controller
public class SomeController {

 @Autowired
 SomeRepository repository;

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

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

一种解决方案是从您的存储库界面中删除@PreAuthorize 注释,并在配置class 中扩展WebSecurityConfigAdaptor 并覆盖configure(HttpSecurity security) 方法。从这里您可以根据需要使用 AntMatchers 对 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

我 运行 遇到了同样的问题,并提出了一个感觉不完全正确但暂时有效的解决方法。

我基本上创建了一个安全实用程序 bean,可用于使用 Spring 数据 REST API 检查方法是在内部还是外部调用(备注:我的存储库前缀为 /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>

并像这样在基于表达式的访问控制中使用该方法(表达式中的最后一行允许您使用映射到不以 URL 开头的任何控制器方法的 save 方法/api/:

@Override
@PreAuthorize("hasRole('ROLE_ADMINISTRATOR') " +
        "or hasPermission(#user, 'WRITE') " +
        "or !@securityUtils.isRestRequest()")
<S extends User> S save(@P("user") S user);

注意事项:

  1. 当您想通过 /api 路由公开自定义功能时,您不能使用它,因为这只是针对路由的简单正则表达式检查
  2. 必须将检查显式添加到要在内部省略授权检查的每个存储库或存储库方法(也可能是一个优势)

在我看来,正确的解决方案是拥有两个存储库,一个称为 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 中的 auto-wiring 机制存在问题,目前无法实现:https://jira.spring.io/browse/DATAREST-923

请评估这些可能性:

  • 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 方法,同时在存储库上添加非安全自定义方法供内部使用:

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

我用这个装饰了存储库 class:

@PreAuthorize("hasRole('admin')")

它锁定了一切。

然后无论我想启用内部使用但不休息,我都这样装饰:

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

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

我通过添加自己的支票解决了这个问题 我创建了具有全局安全性的 AbstractHttpConfigurer class。我已经声明了可以是 public.

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

在我的团队中,我们评估了此 post 中的几个答案,但它们不适合我们的场景。

Johannes Hiemer 的变体对我们有用。我们将 Spring Data REST 配置为仅公开带注释的存储库:

 data.rest:
    detection-strategy: annotated

然后我们定义了2个没有层级关系的repositories

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

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

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

interface SomeRepository : Repository<SomeEntity, Long> {
}

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