百度360必应搜狗淘宝本站头条
当前位置:网站首页 > IT技术 > 正文

Spring Security 全局方法安全:预过滤和后过滤(2)

wptr33 2024-11-27 21:39 22 浏览

Spring Security 全局方法安全:预过滤和后过滤(1) 》我们学习了应用方法授权的预过滤。本文接着学习应用方法授权后过滤和在 Spring Data Repository 中使用过滤。

2 应用方法授权后过滤

在本节中,我们将实现后过滤。假设我们有以下场景。一个前端在 Angular 中实现,后端是基于 spring 的应用产品。用户拥有自己的产品,他们只能获得自己产品的详细信息。为了获得产品的详细信息,前端调用后端公开的端点 ( 图 6 )。


图 6 后过滤场景。客户端调用端点来检索需要在前端显示的数据。后过滤实现确保客户端只获得当前身份认证用户拥有的数据。

在服务类的后端,开发人员编写了一个方法 List<Product> findProducts(),用于检索产品的详细信息。客户端应用程序在前端显示这些详细信息。开发人员如何确保调用此方法的人只接收他们自己拥有的产品,而不是其他人拥有的产品 ?通过保持授权规则与应用程序的业务规则分离来实现此功能的一个选项称为后过滤。在本节中,我们将讨论后过滤是如何工作的,并演示它在应用程序中的实现。

与预过滤类似,后过滤也依赖于一个切面。这个切面允许调用一个方法,但是一旦方法返回,切面就接受返回的值,并确保它遵循您定义的规则。与预过滤一样,后过滤更改该方法返回的集合或数组。您提供返回集合内的元素应该遵循的条件。后过滤切面从返回的集合或数组中过滤那些不遵循您的规则的元素。

要应用后过滤,您需要使用 @PostFilter 注解。 @PostFilter 注解的工作原理类似于我们在《Spring Security 全局方法安全:预过滤和后过滤(1) 》和本文中使用的所有其他 pre-/post- 注解。您将授权规则作为注解值的 SpEL 表达式提供,该规则就是过滤方面使用的规则,如图 7 所示。此外,与预过滤类似,预过滤只适用于数组和集合。确保只对返回类型为数组或集合的方法应用 @PostFilter 注解。


图 7 后过滤。切面拦截受保护的方法返回的集合,并过滤不遵循您提供的规则的值。与后授权不同,当返回值不遵循授权规则时,后过滤不会向调用者抛出异常。

让我们在一个示例中应用后过滤。 为了保持一致,我保留了与本章前面例子中相同的用户,这样配置类就不会改变。为方便起见,我重复下面清单中的配置。

清单 6 配置类

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ProjectConfig {

  @Bean
  public UserDetailsService userDetailsService() {
    var uds = new InMemoryUserDetailsManager();

    var u1 = User.withUsername("nikolai")
            .password("12345")
            .authorities("read")
            .build();

    var u2 = User.withUsername("julien")
            .password("12345")
            .authorities("write")
            .build();

    uds.createUser(u1);
    uds.createUser(u2);

    return uds;
  }

  @Bean
  public PasswordEncoder passwordEncoder() {
    return NoOpPasswordEncoder.getInstance();
  }
}

下面的代码片段显示 Product 类也保持不变:

public class Product {

  private String name;
  private String owner;

  // Omitted constructor, getters, and setters
}

ProductService 类中,我们现在实现了一个返回产品列表的方法。在实际场景中,我们假设应用程序将从数据库或任何其他数据源读取产品。为了使我们的示例简短并使您能够关注我们所讨论的方面,我们使用了一个简单的集合,如清单 7 所示。

我用 @PostFilter 注解了 findProducts() 方法,该方法返回产品列表。我添加的条件作为注解的值, filterObject.owner == authentication.name,只允许返回所有者等于已认证用户的产品 ( 图 8 )。在 equals 操作符的左边,我们使用 filterObject 来引用返回集合中的元素。在操作符的右侧,我们使用身份认证来引用存储在 SecurityContext 中的 authentication 对象。


图 8 在用于授权的 SpEL 表达式中,我们使用 filterObject 引用返回集合中的对象,并使用 authentication 引用来自安全上下文的 Authentication 实例。

清单 7 ProductService

@Service
public class ProductService {

  @PostFilter
  ? ("filterObject.owner == authentication.name") //为该方法返回的集合中的对象添加过滤条件
  public List<Product> findProducts() {
    List<Product> products = new ArrayList<>();

    products.add(new Product("beer", "nikolai"));
    products.add(new Product("candy", "nikolai"));
    products.add(new Product("chocolate", "julien"));

    return products;
  }
}

我们定义了一个控制器类,使我们的方法可以通过端点访问。下一个清单展示了控制器类。

清单 8 ProductController

@RestController
public class ProductController {

  @Autowired
  private ProductService productService;

  @GetMapping("/find")
  public List<Product> findProducts() {
    return productService.findProducts();
  }
}

现在是运行应用程序并通过调用 /find 端点测试其行为的时候了。我们希望在 HTTP 响应体中只看到经过身份认证的用户拥有的产品。下面的代码片段显示了调用每个用户 NikolaiJulien 的端点的结果。要使用用户 Julien 调用端点 /find 和身份认证,使用 cURL 命令:

curl -u julien:12345 http://localhost:8080/find

响应体:

[
  {"name":"chocolate","owner":"julien"}
]

要使用 Nikolai 用户调用端点 /find 和身份验证,使用 cURL 命令:

curl -u nikolai:12345 http://localhost:8080/find

响应体:

[
  {"name":"beer","owner":"nikolai"},
  {"name":"candy","owner":"nikolai"}
]

3 在 Spring Data Repository 中使用过滤

在本节中,我们将讨论 Spring Data Repository 应用的过滤。理解这种方法很重要,因为我们经常使用数据库来持久化应用程序的数据。实现 Spring Boot 应用程序是很常见的,使用 Spring Data 作为一个高级层连接到数据库,无论是 SQL 还是 NoSQL。我们将讨论在使用 Spring Data 时在存储库级别应用过滤的两种方法,并通过示例实现这些方法。

我们采用的第一种方法是你在本文中学到的 :使用 @PreFilter@PostFilter 注解。我们讨论的第二种方法是在查询中直接集成授权规则。正如您将在本节学到的,在选择在 Spring Data Repository 中应用过滤的方式时,您需要注意。如前所述,我们有两个选择:

  • 使用 @PreFilter @PostFilter 注解
  • 直接在查询中应用过滤

在存储库中使用 @PreFilter 注解与在应用程序的任何其他层中应用该注解是相同的。但说到后过滤,情况就变了。在存储库方法上使用 @PostFilter 技术上可以很好地工作,但是从性能的角度来看,它很少是一个好的选择。

假设您有一个管理公司文档的应用程序。开发人员需要实现一个功能,在用户登录后,所有的文档都列在一个网页上。开发人员决定使用 Spring Data Repository 的 findAll() 方法,并使用 @PostFilter 对其进行注解,以允许 Spring Security 对文档进行过滤,使该方法只返回当前登录用户所拥有的文档。这种方法显然是错误的,因为它允许应用程序从数据库检索所有记录,然后过滤记录本身。如果我们有大量文档,调用没有分页的 findAll() 可能会直接导致 OutOfMemoryError。即使文档的数量不足以填满堆,但如果在应用程序中过滤记录,而不是一开始就从数据库中检索您需要的内容,那么性能仍然较差 ( 图 9 )。

在服务级别,你没有其他选择,而不是过滤应用中的记录。不过,如果你知道从存储库中,您只需要检索记录登录用户所拥有的,你应该执行一个查询,只从数据库中提取所需的文件。


图 9 一个糟糕的设计解剖。 当您需要在存储库级别应用过滤时,最好先确保您只能检索所需的数据。 否则,您的应用程序可能面临重内存和性能问题。

注意

在任何从数据源检索数据的情况下,无论是数据库、web服务、输入流还是其他任何东西,都要确保应用程序只检索它需要的数据。尽量避免在应用程序内部过滤数据的需要。

让我们研究一个应用程序,在这个应用程序中,我们首先在 Spring Data Repository 方法上使用 @PostFilter 注解,然后改用第二种方法,在查询中直接写入条件。这样,我们就有机会对这两种方法进行实验并进行比较。

我创建了一个新项目,其中使用了与本章前面示例相同的配置类。与前面的示例一样,我们编写了一个管理产品的应用程序,但这一次我们从数据库中的一个表中检索产品详细信息。对于我们的示例,我们实现了产品的搜索功能 ( 图 10 )。我们编写一个端点,该端点接收一个字符串,并返回名称中包含给定字符串的产品列表。但是我们需要确保只返回与经过身份认证的用户相关的产品。

我们使用 Spring Data JPA 连接到数据库。出于这个原因,我们还需要根据您的数据库管理服务器技术,向 pom.xml 文件添加 spring-boot-starter-data-jpa 依赖项和连接驱动程序。下面的代码片段提供了我在 pom.xml 文件中使用的依赖项:

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
   <groupId>mysql</groupId>
   <artifactId>mysql-connector-java</artifactId>
   <scope>runtime</scope>
</dependency>



图 10 在我们的场景中,我们首先使用 @PostFilter 实现应用程序来根据产品的所有者过滤产品。然后更改实现,将条件直接添加到查询中。这样,我们可以确保应用程序只从源获取所需的记录。

application.properties 文件中,我们添加了 Spring Boot 需要创建数据源的属性。 在下一个代码片段:

spring.datasource.url=jdbc:mysql://localhost:3306/spring
? ?useLegacyDatetimeCode=false&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=
spring.datasource.initialization-mode=always

我们还需要数据库中的一个表来存储我们的应用程序检索的产品详细信息。我们定义了一个 schema.sql 文件,我们在其中编写用于创建表的脚本,以及一个 data.sql 文件,我们在其中编写查询以在表中插入测试数据。您需要将这两个文件 ( schema.sqldata.sql ) 放在 Spring 启动项目的资源文件夹中,以便在应用程序开始时找到并执行它们。下一个代码段显示用于创建表的查询,我们需要在 schema.sql 文件中写入它:

CREATE TABLE IF NOT EXISTS `spring`.`product` (
  `id` INT NOT NULL AUTO_INCREMENT,
  `name` VARCHAR(45) NULL,
  `owner` VARCHAR(45) NULL,
  PRIMARY KEY (`id`));

data.sql 文件中,我编写了三个 INSERT 语句,下一个代码片段就会显示出来。这些语句将创建我们稍后证明应用程序行为所需的测试数据。

INSERT IGNORE INTO `spring`.`product` (`id`, `name`, `owner`) VALUES ('1', 'beer', 'nikolai');
INSERT IGNORE INTO `spring`.`product` (`id`, `name`, `owner`) VALUES ('2', 'candy', 'nikolai');
INSERT IGNORE INTO `spring`.`product` (`id`, `name`, `owner`) VALUES ('3', 'chocolate', 'julien');

为了在我们的应用程序中映射产品表,我们需要编写一个实体类。下面的清单定义了 Product 实体。

清单 9 Product 实体类

@Entity
public class Product {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;
    private String name;
    private String owner;

    // Omitted getters and setters
}

对于 Product 实体,我们还编写了在下一个清单中定义的 Spring Data Repository 接口。请注意,这次我们直接在由存储库接口声明的方法上使用了 @PostFilter 注解。

清单 10 ProductRepository 接口

public interface ProductRepository
        extends JpaRepository<Product, Integer> {

    @PostFilter
    ? ("filterObject.owner == authentication.name") //为Spring Data存储库声明的方法使用@PostFilter 注解
    List<Product> findProductByNameContains(String text);
}

下一个清单将向您展示如何定义一个控制器类,该控制器类实现我们用于测试行为的端点。

清单 11 ProductController

@RestController
public class ProductController {

  @Autowired
  private ProductRepository productRepository;

  @GetMapping("/products/{text}")
  public List<Product> findProductsContaining(@PathVariable String text) {

    return productRepository.findProductByNameContains(text);
  }
}

启动应用程序,我们可以测试调用 /products/{text} 端点时发生了什么。通过在使用 Nikolai 用户进行身份认证时搜索字母 c, HTTP 响应只包含产品 candy 。即使巧克力也含有 c,因为 Julien 拥有它,巧克力也不会出现在响应中。您将在下一个代码片段中找到调用及其响应。要调用端点 /products 并与用户 Nikolai 进行身份认证,发出以下命令:

curl -u nikolai:12345 http://localhost:8080/products/c
copy

响应体:

[
  {"id":2,"name":"candy","owner":"nikolai"}
]

要调用端点 /products 并与用户 Julien 进行身份验证,请发出以下命令:

curl -u julien:12345 http://localhost:8080/products/c

响应体:

[
  {"id":3,"name":"chocolate","owner":"julien"}
]

我们在本节前面讨论过,在存储库中使用 @PostFilter 并不是最好的选择。相反,我们应该确保不从数据库中选择不需要的内容。那么,我们如何改变我们的示例,只选择所需的数据,而不是在选择后过滤数据呢?我们可以在存储库类使用的查询中直接提供 SpEL 表达式。为此,我们遵循两个简单的步骤:

  1. 我们向 Spring 上下文添加了一个 SecurityEvaluationContextExtension 类型的对象。我们可以使用配置类中的一个简单的 @Bean 方法来实现这一点。
  2. 我们使用适当的子句来调整存储库类中的查询,以便进行选择。

在我们的项目中,要在上下文中添加 SecurityEvaluationContextExtension Bean,我们需要更改配置类,如清单 12 所示。

清单 12 将 SecurityEvaLiaNyContextExtextentent 添加到上下文中

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ProjectConfig {

    //将 SecurityEvalionDoxtExtextentent 添加到 Spring 上下文
  @Bean
  public SecurityEvaluationContextExtension 
    securityEvaluationContextExtension() {

    return new SecurityEvaluationContextExtension();
  }

    // Omitted declaration of the UserDetailsService and PasswordEncoder
}

ProductRepository 接口中,我们在方法之前添加查询,并使用 SpEL 表达式用适当的条件调整 WHERE 子句。下面的清单给出了更改。

清单 13 在存储库接口的查询中使用 SpEL

public interface ProductRepository
        extends JpaRepository<Product, Integer> {

    @Query("SELECT p FROM Product p WHERE p.name LIKE %:text% AND p.owner=?#{authentication.name}") //在查询中使用 SpEL 来向记录的所有者添加条件
    List<Product> findProductByNameContains(String text);
}

现在我们可以启动应用程序并通过调用 /products/{text} 端点来测试它。我们希望该行为与使用 @PostFilter 时的行为保持一致。但是现在,只从数据库中检索正确所有者的记录,这使得功能更快、更可靠。下一个代码片段显示对端点的调用。要调用端点 /products 并使用 Nikolai 用户进行身份认证,我们使用以下命令:

curl -u nikolai:12345 http://localhost:8080/products/c

响应体:

[
  {"id":2,"name":"candy","owner":"nikolai"}
]

要调用端点 /products 并使用 Julien 用户进行身份认证,我们使用以下命令:

curl -u julien:12345 http://localhost:8080/products/c

响应体:

[
  {"id":3,"name":"chocolate","owner":"julien"}
]

总结

  • 过滤是一种授权方法,框架在这种方法中验证方法的输入参数或方法返回的值,并排除不满足您定义的某些条件的元素。作为一种授权方法,过滤关注的是方法的输入和输出值,而不是方法执行本身。
  • 您使用过滤来确保一个方法除了它授权处理的值之外不能获得其他值,并且不能返回方法调用者不应该获得的值。
  • 当使用过滤时,您不限制对该方法的访问,但您限制了可以通过该方法的参数发送的内容或该方法返回的内容。这种方法允许您控制方法的输入和输出。
  • 要限制可以通过方法参数发送的值,可以使用 @PreFilter 注解。@PreFilter 注解接收允许将值作为方法参数发送的条件。框架从作为参数给出的集合中过滤所有不遵循给定规则的值。
  • 要使用 @PreFilter 注解,方法的参数必须是一个集合或数组。从定义规则的注解的 SpEL 表达式中,我们使用 filterObject 引用集合中的对象。
  • 要限制该方法返回的值,可以使用 @PostFilter 注解。当使用 @PostFilter 注解时,方法的返回类型必须是集合或数组。框架根据您定义为 @PostFilter 注解值的规则过滤返回集合中的值。
  • 您也可以在 Spring Data 存储库中使用 @PreFilter@PostFilter 注解。但是在 Spring Data 存储库方法上使用 @PostFilter 并不是一个好的选择。为了避免性能问题,在本例中,过滤结果应该直接在数据库级进行。
  • Spring Security 很容易与 Spring Data 集成,您可以使用它来避免使用 Spring Data 存储库的方法发出 @PostFilter

相关推荐

VPS主机搭建Ghost环境:Nginx Node.js MariaDB

Ghost是一款个人博客系统,它是使用Node.js语言和MySQL数据库开发的,同时支持MySQL、MariaDB、SQLite和PostgreSQL。用户可以在支持Node.js的服务器上使用自己...

centos7飞速搭建zabbix5.0并添加windows、linux监控

一、环境zabbix所在服务器系统为centos7,监控的服务器为windows2016和centos7。二、安装zabbix官方安装帮助页面...

Zabbix5.0安装部署

全盘展示运行状态,减轻运维人员的重复性工作量,提高系统排错速度,加速运维知识学习积累。1.png...

MariaDB10在CentOS7系统下,迁移数据存储位置

背景在CentOS7下如果没有默认安装MySQL数据库,可以选择安装MariaDB,最新的版本现在是10可以选择直接yum默认安装的方式yum-yinstallmariadbyum-yi...

frappe项目安装过程

1,准备一台虚拟机,debian12或者ubuntusever22.04.3可以用virtualbox/qemu,或者你的超融合服务器安装一些常用工具和依赖库我这里选择server模式安装,用tab...

最新zabbix一键安装脚本(基于centos8)

一、环境准备注意:操作系统必须是centos8及以上的,因为我配的安装源是centos8的。并且必须连接互联网,脚本是基于yum安装的!!!...

ip地址管理之phpIPAM保姆级安装教程 (原创)

本教程基于Ubuntu24.04LTS,安装phpIPAM(最新稳定版1.7),使用Apache、PHP8.3和MariaDB,遵循最佳实践,确保安全性和稳定性。一、环境准备1....

centos7傻瓜式安装搭建zabbix5.0监控服务器教程

zabbix([`zaebiks])是一个基于WEB界面的提供分布式系统监视...

zabbix7.0LTS 保姆级安装教程 小白也能轻松上手安装

系统环境:rockylinux9.4(yumupdate升级到最新版本)数据库:mariadb10.5.22第一步:关闭防火墙和selinux使用脚本关闭...

ubuntu通过下载安装包安装mariadb10.4

要在Ubuntu18.04上安装MariaDB10.4.34,用的是那个tar.gz的安装包。步骤大概是:...

从0到1:基于 Linux 快速搭建高可用 MariaDB Galera 集群(实战指南)

在企业生产环境中,数据库的高可用性至关重要。今天带你从0到1,手把手在Linux系统上快速搭建一个高可用MariaDBGaleraCluster,实现数据库同步复制、故障自动恢复,保障业务...

Windows 中安装 MariaDB 数据库

mariadb在Windows下的安装非常简单,下载程序双击运行就可以了。需要注意:mariadb和MySQL数据库在Windows下默认是不区分大小写的,但是在Linux下是区分...

SQL执行顺序(SqlServer)

学习SQL这么久,如果突然有人问你SQL的执行顺是怎么样的?是不是很多人会觉得C#、JavaScript都是根据编程顺序来处理的,那么SQL也是根据编程顺序来执行的吗?...

C# - StreamWriter与StreamReader 读写文件 101

读写文本文件的方式:1)File静态类的File.ReadAllLines();与File.WriteAllLines();方法进行读写...

C#中的数组探究与学习

C#中的数组一般分为:...