Spring Security in Action 第十七章 全局方法安全:预过滤和后过滤

本专栏将从基础开始,循序渐进,以实战为线索,逐步深入SpringSecurity相关知识相关知识,打造完整的SpringSecurity学习步骤,提升工程化编码能力和思维能力,写出高质量代码。希望大家都能够从中有所收获,也请大家多多支持。
专栏地址:SpringSecurity专栏
本文涉及的代码都已放在gitee上:gitee地址
如果文章知识点有错误的地方,请指正!大家一起学习,一起进步。
专栏汇总:专栏汇总


本章包括

  • 使用预过滤来限制方法接收的参数值
  • 使用后过滤来限制方法返回的内容
  • 将过滤与Spring Data集成起来

在第16章中,你学会了如何使用全局方法安全来应用授权规则。我们研究了使用@PreAuthorize和@PostAuthorize注解的例子。通过使用这些注解,你可以应用一种方法,即应用程序要么允许方法调用,要么完全拒绝调用。 假设你不想禁止调用一个方法,但你想确保发送给它的参数遵循一些规则。或者,在另一种情况下,你想确保有人调用该方法后,该方法的调用者只收到返回值的授权部分。我们将这样的功能命名为过滤,并将其分为两类。

  • Prefiltering—在调用方法之前,框架会过滤参数的值。
  • Postfiltering—框架会在方法调用后过滤返回值。

image-20230131135605696

图17.1 客户端调用api,提供一个不符合授权规则的值。在预授权的情况下,该方法根本就没有被调用,调用者会收到一个异常。在预过滤的情况下,方面调用该方法,但只提供符合给定规则的值。

过滤的工作方式与调用授权不同(图17.1)。通过过滤,框架会执行调用,如果某个参数或返回值不符合你定义的授权规则,框架不会抛出一个异常。相反,它会过滤掉那些不符合你指定条件的元素。

从一开始就必须提到,你只能对集合和数组应用过滤功能。只有当方法收到一个数组或对象集合作为参数时,你才会使用预过滤。框架会根据你定义的规则来过滤这个集合或数组。后过滤也是如此:只有当方法返回一个集合或数组时,你才能应用这种方法。框架会根据你指定的规则来过滤方法返回的值。

17.1 应用预过滤进行方法授权

在本节中,我们将讨论预过滤背后的机制,然后在一个例子中实现预过滤。你可以使用过滤来指示框架在有人调用一个方法时验证通过方法参数发送的值。框架会过滤那些不符合给定条件的值,只调用那些符合条件的值的方法。我们将这种功能命名为预过滤(图17.2)。

image-20230131140020634

图17.2 通过预过滤,一个切面拦截对受保护方法的调用。切面对调用者提供的参数值进行过滤,只向方法发送符合你定义的规则的值。

在现实世界的例子中,你会发现预过滤很适用的需求,因为它将授权规则与方法实现的业务逻辑解耦。假设你实现了一个用例,你只处理由认证用户拥有的特定细节。这个用例可以从多个地方调用,但它的责任始终是只处理经过认证的用户的详细信息,无论谁调用这个用例。与其确保用例的调用者正确地应用授权规则,你不如让用例应用它自己的授权规则。当然,你可以在方法中这样做。但是,将授权逻辑从业务逻辑中分离出来,可以增强代码的可维护性,使其他人更容易阅读和理解。

正如我们在第16章讨论的调用授权的情况一样,Spring Security也通过使用方面来实现过滤。方面拦截特定的方法调用,并可以用其他指令对其进行增强。对于预过滤,一个方面拦截了带有@PreFilter注解的方法,并根据你定义的标准过滤作为参数提供的collection中的值(图17.3)。

image-20230131140601438

图17.3 通过预过滤,我们将授权责任与业务实现解耦。Spring Security提供的方面只负责授权规则,而服务方法只负责其实现的用例的业务逻辑。

与我们在第16章讨论的@PreAuthorize和@PostAuthorize注解类似,你将授权规则设置为@PreFilter注解的值。在这些规则中,你以SpEL表达式的形式提供,你使用filterObject来指代你作为参数提供给方法的集合或数组内的任何元素。

为了看到预过滤的应用,让我们在一个项目上工作。假设你有一个购买和销售产品的应用程序,它的后端实现了/sell这个api。当用户出售产品时,应用程序的前端调用这个api。但登录的用户只能出售他们自己的产品。让我们来实现一个简单的场景,即调用一个服务方法来销售作为参数收到的产品。通过这个例子,你可以学到如何应用@PreFilter注解,因为这就是我们用来确保该方法只接收当前登录用户所拥有的产品。

一旦我们创建了这个项目,我们就写一个配置类以确保我们有几个用户来测试我们的实现。你可以在清单17.1中找到配置类的直接定义。我称之为ProjectConfig的配置类只声明了一个UserDetailsService和一个PasswordEncoder,并且我用@GlobalMethodSecurity(prePostEnabled=true)来注释它。对于filtering注解,我们仍然需要使用@GlobalMethodSecurity注解并启用pre/postauthorization注解。提供的UserDetailsService定义了我们在测试中需要的两个用户。Nikolai和Julien。

package com.laurentiuspilca.ssia.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

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

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

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

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

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

        return uds;
    }

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

我使用你在下一个清单中找到的模型类来描述产品。

package com.laurentiuspilca.ssia.model;

import java.util.Objects;

public class Product {
    
    

    private String name;
    private String owner;

    public Product(String name, String owner) {
    
    
        this.name = name;
        this.owner = owner;
    }

    public String getName() {
    
    
        return name;
    }

    public void setName(String name) {
    
    
        this.name = name;
    }

    public String getOwner() {
    
    
        return owner;
    }

    public void setOwner(String owner) {
    
    
        this.owner = owner;
    }

    @Override
    public boolean equals(Object o) {
    
    
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Product product = (Product) o;
        return Objects.equals(name, product.name) &&
                Objects.equals(owner, product.owner);
    }

    @Override
    public int hashCode() {
    
    
        return Objects.hash(name, owner);
    }
}

ProductService类定义了我们用@PreFilter保护的服务方法。 你可以在清单17.3中找到ProductService类。在列表中,在sellProducts()方法之前,你可以看到@PreFilter注解的使用。 与该注解一起使用的Spring表达式语言(SpEL)是filterObject .own == authentication.name,它只允许产品的所有者属性等于登录用户的用户名的值。在SpEL表达式中的等号运算符的左边,我们使用filterObject.owner == authentication.name。通过filterObject,我们把列表中的对象称为参数。因为我们有一个prod- ucts的列表,在我们的例子中,filterObject的类型是Product。出于这个原因,我们可以引用产品的所有者属性。在表达式中的等号运算符的右边;我们使用认证对象。对于@PreFilter和@Post- Filter注解,我们可以直接引用认证对象,它在认证后的SecurityContext中是可用的(图17.4)。

image-20230131141236838

图17.4 当使用filterObject进行预过滤时,我们指的是调用者作为参数提供的列表里面的对象。认证对象是认证过程后存储在安全上下文中的对象。

服务方法返回的列表与该方法接收的列表完全一致。这样,我们可以通过检查HTTP响应体中返回的列表来测试和验证框架是否按照我们的预期过滤了列表。

package com.laurentiuspilca.ssia.service;

import com.laurentiuspilca.ssia.model.Product;
import org.springframework.security.access.prepost.PreFilter;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class ProductService {
    
    

    //作为参数给出的List只允许认证用户拥有的产品。
    @PreFilter("filterObject.owner == authentication.name")
    public List<Product> sellProducts(List<Product> products) {
    
    
        // sell products and return the sold products list
        //为测试目的返回产品
        return products;
    }
}

为了使我们的测试更容易,我定义了一个api来调用受保护的服务方法。 清单17.4在一个名为ProductController的控制器类中定义了这个api。 在这里,为了使api的调用更短,我创建了一个列表并直接将其作为参数提供给服务方法。在真实世界的场景中,这个列表应该由客户端在请求体中提供。你也可以观察到,我使用@GetMapping进行操作,这是非标准的。但要知道,我这样做是为了避免在我们的例子中处理CSRF保护,这样可以让你专注于眼前的主题。你在第10章中了解了CSRF保护。

package com.laurentiuspilca.ssia.controllers;

import com.laurentiuspilca.ssia.model.Product;
import com.laurentiuspilca.ssia.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.ArrayList;
import java.util.List;

@RestController
public class ProductController {
    
    

    @Autowired
    private ProductService productService;

    @GetMapping("/sell")
    public List<Product> sellProduct() {
    
    
        List<Product> products = new ArrayList<>();

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

        return productService.sellProducts(products);
    }
}

让我们启动应用程序,看看当我们调用/sell端点时会发生什么。 观察我们提供的列表中的三个产品作为服务方法的参数。我把其中两个产品分配给用户Nikolai,另一个分配给用户Julien。

当我们调用api并对用户Nikolai进行认证时,我们希望在响应中只看到与她相关的两个产品。当我们调用api并对Julien进行认证时,我们应该在api中只看到与Julien有关的一个产品。在下面的代码段中,你可以看到测试调用及其结果。 要调用端点/sell并以用户Nikolai进行认证,请使用这个命令。

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

响应为

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

要调用端点/sell并以用户Julien进行认证,请使用此命令。

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

响应为

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

你需要注意的是,这个切面会改变给定的集合。在我们的例子中,不要指望它能返回一个新的List实例。事实上,它是同一个实例,该方面从该实例中删除了不符合给定规则的元素。这一点是需要考虑的。你必须始终确保你提供的集合实例不是不可变的。提供一个不可变的集合来处理,在执行时就会出现异常,因为过滤方面不能改变集合的内容(图 17.5)。

image-20230131141958322

图 17.5 这个切面拦截并改变作为参数的集合。你需要提供一个集合的可变实例,以便方面可以改变它。

清单17.5展示了我们在本节前面所做的同一个项目,但我用List.of()方法所返回的不可变的实例改变了List的定义,以测试在这种情况下会发生什么。

清单17.5 使用一个不可变的集合

package com.laurentiuspilca.ssia.controllers;

import com.laurentiuspilca.ssia.model.Product;
import com.laurentiuspilca.ssia.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
public class ProductController {
    
    

    @Autowired
    private ProductService productService;

    @GetMapping("/sell")
    public List<Product> sellProduct() {
    
    
        List<Product> products = List.of(
                new Product("beer", "nikolai"),
                new Product("candy", "nikolai"),
                new Product("chocolate", "julien"));

        return productService.sellProducts(products);
    }
}

运行应用程序并调用/sell api的结果是HTTP响应,状态为500 Internal Server Error,并且在控制台日志中出现了一个异常,如下面的代码片断所示。

curl -u julien:12345 http://localhost:8080/sell
{
    
    
"status":500,
"error":"Internal Server Error",
"message":"No message available",
"path":"/sell"
}

在应用程序的控制台中,你可以发现一个类似于下面代码片断中提出的异常。

java.lang.UnsupportedOperationException: null
at java.base/java.util.ImmutableCollections.uoe(ImmutableCollections.java:73)
~[na:na]

17.2 应用方法授权的后过滤功能

在本节中,我们将实现后过滤。假设我们有以下场景。一个应用程序有一个用Angular实现的前端和一个基于Spring的后端,管理一些产品。用户拥有产品,他们只能获得其产品的详细信息。为了获得他们产品的详细信息,前端调用后端暴露的api(图17.6)。

image-20230131142420675

图17.6 后过滤场景。一个客户端调用一个端点来检索它需要在前端显示的数据。一个后过滤的实现可以确保客户端只得到当前认证用户所拥有的数据。

在后台的一个服务类中,开发者写了一个方法List findProducts(),用来检索产品的详细信息。客户端应用程序在前台显示这些细节。开发者如何确保调用这个方法的人只收到他们自己的产品,而不是其他人的产品?一个通过保持授权规则与应用程序的业务规则脱钩来实现这一功能的方案被称为后过滤。在这一节中,我们将讨论后过滤是如何工作的,并在一个应用程序中演示其实现。

与预过滤类似,后过滤也依赖于一个方面。这个方面允许调用一个方法,但是一旦该方法返回,该方面就会获取返回值,并确保它遵循你定义的规则。就像预过滤的情况一样,后过滤会改变一个集合或一个由方法返回的数组。你提供了返回集合中的元素应该遵循的规则。后置过滤方面从返回的集合或数组中过滤那些不遵循你的规则的元素。

为了应用后过滤,你需要使用@Post-Filter注解。@PostFilter注解的工作原理与我们在第14章和本章中使用的所有其他前/后注解相似。你为注解的值提供授权规则作为SpEL表达式,如图17.7所示,该规则是过滤方面使用的规则。另外,与预过滤类似,后过滤只适用于数组和集合。请确保你只对有数组或集合作为返回类型的方法应用@PostFilter注解。

image-20230131153747945

图17.7 后过滤。一个方面拦截由受保护方法返回的集合,并过滤那些不符合你提供的规则的值。与后授权不同,当返回的值不符合授权规则时,后过滤不会向调用者抛出一个异常。

让我们在一个例子中应用后滤波。为了保持一致,我保留了与本章中我们以前的例子中相同的用户,这样配置类就不会改变。为了你的方便,我重复了以下列表中提出的配置。

package com.laurentiuspilca.ssia.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

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

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

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

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

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

        return uds;
    }

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

接下来的代码片断显示,产品类也保持不变。

package com.laurentiuspilca.ssia.model;

public class Product {
    
    

    private String name;
    private String owner;

    public Product(String name, String owner) {
    
    
        this.name = name;
        this.owner = owner;
    }

    public String getName() {
    
    
        return name;
    }

    public void setName(String name) {
    
    
        this.name = name;
    }

    public String getOwner() {
    
    
        return owner;
    }

    public void setOwner(String owner) {
    
    
        this.owner = owner;
    }
}

在ProductService类中,我们现在实现了一个返回产品列表的方法。在现实世界中,我们假设应用程序会从数据库或其他数据源读取产品。为了使我们的例子简短,让你专注于我们讨论的方面,我们使用一个简单的集合,如清单17.7所示。

清单17.7 产品服务类

package com.laurentiuspilca.ssia.service;

import com.laurentiuspilca.ssia.model.Product;
import org.springframework.security.access.prepost.PostFilter;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

@Service
public class ProductService {
    
    

    //为方法所返回的集合中的对象添加过滤条件。
    @PostFilter("filterObject.owner == authentication.principal.username")
    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;
    }
}

我给返回产品列表的findProducts()方法加上了@PostFilter注解。我添加的条件作为注解的值,filterObject.owner == authentication.name,只允许返回所有者与认证用户相同的产品(图17.8)。在等价运算符的左边,我们用filterObject来指代返回集合中的元素。在操作符的右边,我们使用authentication来指代存储在SecurityContext中的Authentication对象。

image-20230131154325405

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

我们定义了一个controller类,使我们的方法可以通过一个api进行访问。下一个列表介绍了控制器类。

package com.laurentiuspilca.ssia.controllers;

import com.laurentiuspilca.ssia.model.Product;
import com.laurentiuspilca.ssia.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
public class ProductController {
    
    

    @Autowired
    private ProductService productService;

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

现在是运行应用程序并通过调用/find端点测试其行为的时候了。我们希望在HTTP响应体中只看到由认证用户拥有的产品。接下来的代码片段显示了我们每个用户(Nikolai和Julien)调用该端点的结果。要调用端点/find并对用户Julien进行认证,请使用这个cURL命令。

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

响应为

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

要调用api /find并以用户Nikolai进行认证,请使用这个cURL命令:

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

响应为

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

17.3 在Spring Data存储库中使用过滤功能

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

我们采取的第一种方法是你在本章中到目前为止学到的方法:使用@PreFilter和@PostFilter注解。我们讨论的第二种方法是在查询中直接整合授权规则。正如你将在本节中学习到的,当你在Spring Data资源库中选择应用过滤的方式时,你需要注意一下。如前所述,我们有两种选择。

  • 使用@PreFilter和@PostFilter注释
  • 在查询中直接应用过滤功能

在存储库的情况下使用@PreFilter注解与在你的应用程序的任何其他层应用该注解是一样的。但是当涉及到后置过滤器时,情况就变了。在存储库方法上使用@PostFilter在技术上是可行的,但从性能的角度来看,这很少是一个好的选择。

假设你有一个管理公司文件的应用程序。开发人员需要实现一个功能,即在用户登录后将所有的文件列在一个网页上。开发者决定使用Spring Data资源库的findAll()方法,并用@PostFilter对其进行注释,以允许Spring Security对文档进行过滤,使该方法只返回当前登录用户所拥有的文档。这种方法显然是错误的,因为它允许应用程序从数据库中检索所有的记录,然后自己过滤这些记录。如果我们有大量的文档,在没有分页的情况下调用findAll()可能直接导致OutOfMemoryError。即使文档的数量没有大到足以填满堆,在你的应用程序中过滤记录,而不是一开始就从数据库中只检索你需要的记录,性能还是比较差的(图17.9)。

image-20230131163310590

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

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

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

和前面的例子一样,我们编写了一个管理产品的应用程序,但这次我们从数据库的一个表中检索产品的详细信息。对于我们的例子,我们实现了产品的搜索功能(图17.10)。我们写一个api,接收一个字符串并返回名称中含有该字符串的产品列表。但我们需要确保只返回与认证用户相关的产品。

image-20230131165028002

图17.10 在我们的方案中,我们首先使用@PostFilter实现应用程序,根据产品的所有者过滤产品。然后我们改变实现,直接在查询中添加条件。这样,我们就能确保应用程序只从源头获得所需的记录。

在application.properties文件中,我们添加Spring Boot创建数据源所需的属性。在下一个代码段中,你可以看到我在application.properties文件中添加的属性。

spring.datasource.url=jdbc:mysql://localhost/spring?useLegacyDatetimeCode=false&serverTimezone=GMT%2B8&useSSL=false
spring.datasource.username=root
spring.datasource.password=
spring.datasource.initialization-mode=always

我们还需要在数据库中建立一个表来存储我们的应用程序所检索的产品细节。我们定义了一个schema.sql文件,用来编写创建表的脚本,还有一个data.sql文件,用来编写在表中插入测试数据的查询。你需要把这两个文件(schema.sql和data.sql)放在Spring Boot项目的资源文件夹中,这样它们就会在应用程序开始时被找到并执行。 接下来的代码片段向你展示了用于创建表的查询,我们需要把它写在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');

注意 记住,我们在本书的其他例子中使用了相同的表名。如果你在以前的例子中已经有了相同名称的表,在开始这个项目之前,你也许应该放弃这些表。另一个选择是使用不同的表。

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

package com.laurentiuspilca.ssia.entities;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class Product {
    
    

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

    public int getId() {
    
    
        return id;
    }

    public void setId(int id) {
    
    
        this.id = id;
    }

    public String getName() {
    
    
        return name;
    }

    public void setName(String name) {
    
    
        this.name = name;
    }

    public String getOwner() {
    
    
        return owner;
    }

    public void setOwner(String owner) {
    
    
        this.owner = owner;
    }
}

对于产品实体,我们也写了一个Spring Data资源库接口,定义在下一个列表中。请注意,这次我们直接在存储库接口所声明的方法上使用了@PostFilter注解。

package com.laurentiuspilca.ssia.repositories;

import com.laurentiuspilca.ssia.entities.Product;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.security.access.prepost.PostFilter;

import java.util.List;

public interface ProductRepository
        extends JpaRepository<Product, Integer> {
    
    

    //对Spring Data资源库声明的方法使用@PostFilter注解。
    @PostFilter("filterObject.owner == authentication.principal.username")
    List<Product> findProductByNameContains(String text);
}

下一个列表告诉你如何定义一个controller类,实现我们用于测试行为的api。

package com.laurentiuspilca.ssia.controllers;

import com.laurentiuspilca.ssia.entities.Product;
import com.laurentiuspilca.ssia.repositories.ProductRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
public class ProductController {
    
    

    @Autowired
    private ProductRepository productRepository;

    @GetMapping("/products/{text}")
    public List<Product> findProductsContaining(@PathVariable String text) {
    
    
        return productRepository.findProductByNameContains(text);
    }
}

启动应用程序,我们可以测试调用/products/{text}端点时会发生什么。通过搜索字母c,同时对用户Nikolai进行认证,HTTP响应只包含产品candy。即使chocolate也包含一个c,因为Julien拥有它,chocolate不会出现在响应中。你可以在接下来的代码段中找到这些调用和它们的响应。要调用端点/products,并以用户Nikolai进行身份验证,请发出这个命令。

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

响应为

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

要调用api/产品并以用户Julien进行身份验证,请用此命令:

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

响应为

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

我们在本节前面讨论过,在版本库中使用@PostFilter并不是最好的选择。相反,我们应该确保不从数据库中选择我们不需要的东西。那么我们如何改变我们的例子,只选择需要的数据,而不是在选择后过滤数据呢?我们可以在资源库类使用的查询中直接提供SpEL表达式。为了实现这一点,我们要遵循两个简单的步骤:

  • 我们向Spring上下文添加一个SecurityEvaluationContextExtension类型的对象。我们可以使用配置类中的一个简单的@Bean方法来做这件事。
  • 我们用适当的选择条件来调整我们资源库类中的查询。

在我们的项目中,为了在上下文中添加SecurityEvaluationContextExtension Bean,我们需要改变配置类,如下所示。

package com.laurentiuspilca.ssia.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.data.repository.query.SecurityEvaluationContextExtension;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

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

    //在Spring上下文中添加一个SecurityEvaluationContextExtension。
    @Bean
    public SecurityEvaluationContextExtension securityEvaluationContextExtension() {
    
    
        return new SecurityEvaluationContextExtension();
    }

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

在ProductRepository接口中,我们在方法之前添加查询,并使用SpEL表达式调整WHERE子句的适当条件。下面的代码展示了这种变化。

package com.laurentiuspilca.ssia.repositories;

import com.laurentiuspilca.ssia.entities.Product;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;

import java.util.List;

public interface ProductRepository
        extends JpaRepository<Product, Integer> {
    
    

    @Query("SELECT p FROM Product p WHERE p.name LIKE %:text% AND p.owner=?#{authentication.principal.username}")
    List<Product> findProductByNameContains(String text);
}

我们现在可以启动应用程序,并通过调用/products/{text}端点进行测试。 我们希望行为与我们使用@PostFilter的情况下保持一致。但是现在,只有正确的用户的记录才会从数据库中检索出来,这使得功能更快、更可靠。接下来的代码是对api的调用。为了调用端点/products和用户Nikolai的授权,我们使用这个命令。

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

响应为

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

为了调用api/产品并以用户Julien进行认证,我们使用这个命令。

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

响应为

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

总结

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

猜你喜欢

转载自blog.csdn.net/Learning_xzj/article/details/128998841