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

news2024/12/26 1:04:15

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

文章目录

    • 17.1 应用预过滤进行方法授权
    • 17.2 应用方法授权的后过滤功能
    • 17.3 在Spring Data存储库中使用过滤功能

本章包括

  • 使用预过滤来限制方法接收的参数值
  • 使用后过滤来限制方法返回的内容
  • 将过滤与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。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/342260.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Rust学习入门--【8】复合类型

复合类型&#xff08;compound type&#xff09; 可以将多个不同类型的值组合为一个类型。 Rust中提供了两种内置的复合数据类型&#xff1a;元组&#xff08;tuple&#xff09;和数组&#xff08;array&#xff09;。 元组类型 元组是一个具有 固定长度 的数据集合 —— 无…

按键输入驱动

目录 一、硬件原理 二、添加设备树 1、创建pinctrl 2、创建节点 3、检查 编译复制 三、修改工程模板​编辑 四、驱动编写 1、添加keyio函数 2、添加调用 3、驱动出口函数添加释放 4、添加原子操作 5、添加两个宏定义 6、初始化原始变量 7、打开操作 8、读操作 总体代…

自启动管理 - Win10

自启动管理 - Win10前言关闭开机自启方案1&#xff1a;在软件中设置方案2&#xff1a;在任务管理器设置方案3&#xff08;不推荐&#xff09;&#xff1a;通过注册表管理方案4&#xff1a;通过第三方工具管理工具1&#xff1a;360安全卫士工具2&#xff1a;Autoruns工具3&#…

性能测试概述

目录 一.什么是性能测试 1.生活中软件存在的性能问题 2.性能测试的概念 3.功能测试和性能测试的区别 4.什么样的软件表现是性能好的表现&#xff0c;什么样的软件是性能不好的表现 二.一个项目为什么要进行性能测试 三.性能测试常见术语以及衡量指标 1.专业术语&#x…

Docker的数据卷管理与容器互联

目录 一、Docker数据管理介绍 二、数据卷 1、数据卷概念 三、数据卷容器 1、数据卷容器的概念 2、数据卷容器示例 四、容器互联 1、容器互联概念 2、容器互联示例 一、Docker数据管理介绍 用户在使用Docker的过程中&#xff0c;往往需要能查看容器内应用产生的数据&…

基于transformer和图卷积网络的人体运动预测时空网络

效果演示&#xff1a; python行为识别行为骨骼框架检测动作识别动作检测行为动作分类近年来&#xff0c;人体运动预测已成为计算机视觉领域的一个活跃研究课题。然而&#xff0c;由于人体运动的复杂性和随机性&#xff0c;它仍然是一个具有挑战性的问题。在以前的工作中&#x…

[golang] 实现 jwt 方式登录

1 Jwt 和 Session 登录方案介绍 JSON Web Token&#xff08;缩写 JWT&#xff09;是目前流行的跨域认证解决方案。 原理是生存的凭证包含标题 header&#xff0c;有效负载 payload 和签名组成。用户信息payload中&#xff0c;后端接收时只验证凭证是否有效&#xff0c;有效就…

【Spark分布式内存计算框架——Spark Core】11. Spark 内核调度(下)

8.5 Spark 基本概念 Spark Application运行时&#xff0c;涵盖很多概念&#xff0c;主要如下表格&#xff1a; 官方文档&#xff1a;http://spark.apache.org/docs/2.4.5/cluster-overview.html#glossary Application&#xff1a;指的是用户编写的Spark应用程序/代码&#x…

leetcode练习二:排序

文章目录排序一、排序算法1.1 冒泡排序1.1.1 算法步骤1.1.2 算法分析1.1.3 代码实现&#xff1a;1.1.4 冒泡排序优化1.2 选择排序1.2.1 算法步骤1.2.2 算法分析1.2.3 代码实现1.3 插入排序1.3.1 算法步骤1.3.2 算法分析1.3.3 代码实现1.4 希尔排序1.4.1 算法步骤1.4.2 算法分析…

【网络基础】DNS是什么

本文不会直接引入复杂枯燥概念&#xff0c;用形象例子通俗讲解&#xff0c;旨在入门理解。 DNS作用 DNS是用来做域名解析的。 相当于把网址翻译成实际ip地址&#xff0c;供其他设备访问。 一个例子 有一个网站的服务器IP地址为1.1.1.1&#xff0c;用电脑访问该网站的话只需…

挂载Samba到Windows系统和Linux系统

1、搭建Samba服务 1.1安装Samba服务 yum -y install samba结果如下&#xff1a; 1.2配置Samba服务 修改Samba服务的配置文件 vim /etc/samba/smb.conf[sambadir]自定义路径 comment Samba Directories自定义描述 path /samba/dir自定义路径 [global]workgroup SAMBAsec…

ElasticSearch-学习笔记-阶段总结(易错点易混淆点归纳)

Java后端-学习路线-笔记汇总表【黑马程序员】ElasticSearch-学习笔记01【ElasticSearch基本介绍】【day01】ElasticSearch-学习笔记02【ElasticSearch索引库维护】ElasticSearch-学习笔记03【ElasticSearch集群】ElasticSearch-学习笔记04【Java客户端操作索引库】【day02】Ela…

基于python shapely的几何集合操作

前言shapely是基于笛卡尔坐标的几何对象操作和分析Python库。底层基于GEOS和JTS库。shapely无法读取和写数据文件&#xff0c;但可以基于应用广泛的一些格式和协议进行序列化(serialize)和去序列化(deserialize)操作。而且shapely不关注数据格式和坐标系统&#xff0c;但shapel…

05 react img css修改svg图片样式

react img css修改svg图片样式svg图片的相关理论定义优点前端引入svg图片的方式方式一&#xff1a;<svg>标签引入&#xff0c;内嵌到 HTML 中方式二&#xff0c;修改svg的颜色方式三&#xff1a;<img>标签引入1、元素模糊2、元素亮度3、元素投影4、元素的灰色程度5…

乐鑫特权隔离机制的 OTA 固件升级

固件空中升级 (OTA, Over-The-Air) 是任何联网设备的重要功能之一&#xff0c;支持开发人员通过远程更新固件&#xff0c;以发布新功能或修复错误。乐鑫特权隔离框架中包含两类应用程序&#xff1a;受保护的应用程序 (protected_app) 和用户应用程序 (user_app) &#xff0c;这…

互联网舆情监测系统的发展阶段,TOOM互联网舆情监测系统有哪些?

互联网舆情监测系统是一种利用计算机技术对互联网上的大量信息进行实时监测、分析和评估的工具&#xff0c;旨在了解公众对某一事件、话题或品牌等的态度、情感倾向和影响力等。通过对社交媒体、论坛、新闻媒体等多个渠道的数据采集和处理&#xff0c;系统能够实现舆情事件的追…

【学习总结】IMU预积分推导

本文仅用于记录自己学习总结。记录IMU预积分推导过程&#xff0c;不包含具体原理。 符号表示 RRR: 表示旋转矩阵 vvv: 表示速度 ppp: 表示位移 ExpExpExp: 指数映射&#xff0c;将旋转向量映射为旋转矩阵 w~\widetilde{w}w: 角速度观测值 f~\widetilde{f}f​: 加速度观测值 bg…

【Hello Linux】Linux工具介绍 (yum vim)

作者&#xff1a;小萌新 专栏&#xff1a;Linux 作者简介&#xff1a;大二学生 希望能和大家一起进步&#xff01; 本篇博客简介&#xff1a;介绍Linux的常用工具 yum和vim Linux工具介绍Linux中的软件管理工具 -- yum在windows下安装软件的方式在Linux下安装软件的方式认识yum…

安警官的IP地址是怎样定位到莽村附近的?

要说最近大火的电视剧非《狂飙》莫属。电视剧《狂飙》自开播以来&#xff0c;一举超过《三体》《去有风的地方》等先播电视剧&#xff0c;收视率一路“狂飙”&#xff0c;牢牢占据近期的收视冠军。 在剧中&#xff0c;张译扮演一名坚持公平、正义与理想的人民警察“安欣”&…