程序员开发实例大全宝库

网站首页 > 编程文章 正文

Spring Security 实现过滤器(1)(springsecurity过滤器配置)

zazugpt 2025-05-09 22:47:07 编程文章 3 ℃ 0 评论

在 Spring Security 中,HTTP 过滤器将应用于 HTTP 请求的不同职责委托给他人。在前面文章中,我们讨论了 HTTP 基本身份认证和授权体系结构,我经常提到过滤器。您了解了一个名为身份认证过滤器的组件,该组件将身份认证责任委托给身份认证管理器。您还了解到,在成功的身份认证之后,某个过滤器负责授权配置。在 Spring Security 中,通常,HTTP 过滤器管理必须应用于请求的每个职责。过滤器形成了一个责任链。过滤器接收请求,执行其逻辑,并最终将请求委托给链中的下一个过滤器 ( 图 1 )。


过滤器链接收请求。每个过滤器使用一个管理器将特定的逻辑应用于请求,并最终将请求沿链进一步委托给下一个过滤器。

这个想法很简单。当你去机场时,从进入航站楼到登机,你要经过多个检查 (图 2 )。你先出示机票,然后验证护照,然后过安检。在机场门口,可能会应用更多的“过滤器”。例如,在某些情况下,在登机前,你的护照和签证会被再次验证。这与 Spring Security 中的过滤器链非常相似。以同样的方式,您可以在过滤器链中使用 Spring Security 对 HTTP 请求进行自定义过滤器。Spring Security 提供了过滤器实现,您可以通过自定义将其添加到过滤器链中,但是您也可以定义自定义过滤器。


在机场,你经过一个过滤链,最终登上飞机。同样,Spring Security 有一个过滤器链,用于处理应用程序接收到的 HTTP 请求。

在过滤器实现系列文章中,我们将讨论如何自定义过滤器,这些过滤器是 Spring Security 中身份认证和授权体系结构的一部分。例如,您可能希望通过为用户增加一个步骤来增强身份认证,比如检查他们的电子邮件地址或使用一次性密码。您还可以添加用于审核身份认证事件的功能。您将发现应用程序使用审计身份认证的各种场景:从调试目的到识别用户行为。使用当今的技术和机器学习算法可以改善应用程序,例如,通过学习用户的行为,知道是否有人入侵了他们的账户或假冒用户。

了解如何自定义 HTTP 过滤器职责链是一项有价值的技能。在实践中,应用程序有各种各样的需求,其中使用默认配置不再有效。您需要添加或替换链的现有组件。对于默认实现,您使用 HTTP Basic 身份认证方法,该方法允许您依赖于用户名和密码。但是在实际情况中,在很多情况下,您需要的不仅仅是这些。也许您需要实现不同的身份认证策略,将授权事件通知外部系统,或者简单地记录成功或失败的身份认证,稍后将用于跟踪和审计( 图 3 )。无论您的场景是什么,Spring Security 为您提供了根据需要精确建模过滤器链的灵活性。


在 Spring Security 架构中实现过滤器

在本节中,我们将讨论 Spring Security 架构中过滤器和过滤器链的工作方式。您首先需要这个概述来理解本文下一节中将要讨论的实现示例。您在前面的文章中了解到,身份认证过滤器拦截请求并将身份认证责任进一步委托给授权管理器。如果希望在身份认证之前执行某些逻辑,可以通过在身份认证过滤器之前插入一个过滤器来实现。

Spring Security 架构中的过滤器是典型的 HTTP 过滤器。 我们可以通过从 javax.servlet 包中实现 Filter 接口来创建过滤器。 与其他任何 HTTP 过滤器一样,您需要重写 doFilter() 方法以实现其逻辑。 此方法接收 ServletRequestServletResponse FilterChain 作为参数:

  • ServletRequest -- 表示 HTTP 请求。我们使用 ServletRequest 对象来检索关于请求的详细信息。
  • ServletResponse -- 表示 HTTP 响应。我们使用 ServletResponse 对象在将响应发送回客户端或沿着过滤器链进一步发送之前更改响应。
  • FilterChain -- 表示过滤器链。我们使用 FilterChain 对象将请求转发给链中的下一个过滤器。


过滤器链表示一组过滤器,它们按已定义的顺序执行操作。Spring Security 为我们提供了一些过滤器实现和它们的顺序。在提供的过滤器中

  • BasicAuthenticationFilter 负责 HTTP Basic 身份认证(如果存在)。
  • CsrfFilter 负责跨站点请求伪造 ( CSRF ) 保护,我们将在后面文章讨论。
  • CorsFilter 负责跨来源资源共享 ( CORS ) 授权规则,我们也将在后面文章讨论。.

您不需要知道所有的过滤器,因为您可能不会直接从代码中接触这些过滤器,但您确实需要了解过滤器链是如何工作的,并了解一些实现。在这本系列文章中,我只解释了那些对我们讨论的各种主题至关重要的过滤器。

应用程序不一定具有链中所有这些过滤器的实例,理解这一点很重要。链的长度取决于您如何配置应用程序。例如,在前面文章中,您了解到如果您想要使用 HTTP Basic 身份认证方法,您需要调用 HttpSecurity 类的 httpBasic() 方法。如果调用 httpBasic() 方法,BasicAuthenticationFilter 的实例就会添加到链中。类似地,根据所编写的配置,过滤器链的定义也会受到影响。


每个过滤器都有一个序号。这决定了过滤器应用于请求的顺序。您可以添加自定义过滤器以及 Spring Security 提供的过滤器。

为链添加一个新的过滤器( 图 4 )。或者,您可以在已知的之前、之后或已知的位置添加过滤器。事实上,每个位置都是一个索引(一个数字),你可能会发现它也被称为“顺序”。

您可以在同一位置添加两个或多个过滤器 ( 图 5 )。在本文后面小节中,我们将遇到可能发生这种情况,这通常会给开发人员中造成混淆。

注意

如果多个过滤器具有相同的位序号,则没有定义它们被调用的顺序。


在链中现有过滤器之前添加过滤器

在本节中,我们将讨论在过滤器链中现有过滤器之前应用自定义 HTTP 过滤器。您可能会发现在某些情况下这是有用的。为了以实际的方式解决这个问题,我们将为我们的示例处理一个项目。通过这个示例,您将很容易地学会实现自定义过滤器,并在过滤器链中现有过滤器之前应用它。然后,您可以根据在生产应用程序中发现的任何类似需求调整此示例。

对于我们的第一个自定义过滤器实现,让我们考虑一个简单的方案。 我们要确保任何请求都有一个名为 Request-Id 的请求头。 我们假设我们的应用程序使用此请求头来跟踪请求,并且此请求头是必需的。 同时,我们希望在应用程序执行身份认证之前验证这些假设。 如果请求的格式无效,则身份认证过程可能涉及查询数据库或其他我们不希望应用程序执行的占用资源的操作。 我们如何做到这一点? 要解决当前需求仅需两步,最后,筛选器链看起来如图 6 所示:

  1. 实现过滤器。创建一个 RequestValidationFilter 类,用于检查请求中是否存在所需的请求头。
  2. 将过滤器添加到过滤器链中。在配置类中执行此操作,覆盖 configure() 方法。


对于我们的示例,我们添加了 RequestValidationFilter ,它在身份认证过滤器之前起作用。RequestValidationFilter 确保在请求认证失败时不会发生身份认证。在我们的例子中,请求必须有一个名为 Request-Id 的请求头。

为了完成第1步(实现过滤器),我们定义了一个自定义过滤器。下一个清单显示了实现。

清单 1 实现自定义过滤器

// 定义过滤器,该类实现 Filter 接口并重写 doFilter() 方法。
public class RequestValidationFilter   implements Filter {

  @Override
  public void doFilter(
     ServletRequest servletRequest, 
     ServletResponse servletResponse, 
     FilterChain filterChain) 
     throws IOException, ServletException {
     // ...
  }
}

doFilter() 方法内部,我们编写了过滤器的逻辑。在我们的示例中,我们检查 Request-Id 头是否存在。如果存在,我们通过调用 doFilter() 方法将请求转发给链中的下一个过滤器。如果报头不存在,我们将响应设置为 HTTP 状态 400 Bad Request,而不将其转发到链中的下一个过滤器 ( 图 7 )。清单 2 给出了逻辑。


我们在身份认证之前添加的自定义过滤器会检查 Request-Id 头是否存在。如果请求中存在报头,应用程序将转发请求以进行身份认证。如果报头不存在,应用程序设置 HTTP 状态 400 Bad Request 并返回给客户端。

清单 2 在 doFilter() 方法中实现逻辑

@Override
public void doFilter(
  ServletRequest request, 
  ServletResponse response, 
  FilterChain filterChain) 
    throws IOException, 
           ServletException {

  var httpRequest = (HttpServletRequest) request;
  var httpResponse = (HttpServletResponse) response;

  String requestId = httpRequest.getHeader("Request-Id");

             // 如果报头丢失,HTTP 状态将更改为 400 Bad Request,并且请求不会转发到链中的下一个过滤器。
  if (requestId == null || requestId.isBlank()) {
      httpResponse.setStatus(HttpServletResponse.SC_BAD_REQUEST);
      return;
  }
// 如果存在报头,则将请求转发到链中的下一个过滤器。
  filterChain.doFilter(request, response);

}

为了实现第 2 步,在配置类中应用过滤器,我们使用 HttpSecurity 对象的 addFilterBefore() 方法,因为我们希望应用程序在身份验证之前执行这个自定义过滤器。这个方法接收两个参数:

要添加到链中的自定义过滤器实例 在我们的示例中,这是清单 1 中所示的 RequestValidationFilter 类的一个实例。

在此之前添加新实例的过滤器类型 对于本例,因为需求是在身份认证之前执行过滤器逻辑,所以我们需要在身份认证过滤器之前添加自定义过滤器实例。类
Basic-AuthenticationFilter
定义了认证过滤器的默认类型。

到目前为止,我们通常将处理身份认证的过滤器称为身份认证过滤器。您将在后面文章中发现 Spring Security 还配置了其他过滤器。还有我们将讨论跨站点请求伪造 ( CSRF ) 保护和跨站资源共享 ( CORS ),它们也依赖于过滤器。

清单 3 显示了如何在配置类的身份认证过滤器之前添加自定义过滤器。为了使示例更简单,我使用 permitAll() 方法允许所有未经身份认证的请求。

清单 3 在认证前配置自定义过滤器

@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http.addFilterBefore( //在过滤器链中的身份认证过滤器之前添加自定义过滤器的实例
            new RequestValidationFilter(),
            BasicAuthenticationFilter.class)
        .authorizeRequests()
            .anyRequest().permitAll();
  }
}

我们还需要一个控制器类和一个端点来测试功能。下一个清单定义了控制器类。

清单 4 控制器类

@RestController
public class HelloController {

  @GetMapping("/hello")
  public String hello() {
    return "Hello!";
  }
}

现在可以运行和测试应用程序。在没有报头的情况下调用端点将生成 HTTP 状态为 400 Bad Request 的响应。如果向请求添加报头,则响应状态变为 HTTP 200 OK,您还将看到响应体 Hello ! 要调用没有 Request-Id 请求头的端点,我们使用cURL命令:

curl -v http://localhost:8080/hello

这个调用生成以下(截断的)响应

...
< HTTP/1.1 400
...

要调用端点并提供 Request-Id 请求头,我们使用 cURL 命令:

curl -H "Request-Id:12345" http://localhost:8080/hello

这个调用生成以下(截断的)响应:

Hello!

在链中现有过滤器之后添加过滤器

在本节中,我们将讨论在过滤器链中现有过滤器之后添加过滤器。当您想在过滤器链中已经存在的内容之后执行某些逻辑时,可以使用此方法。让我们假设您必须在身份认证过程之后执行一些逻辑。这方面的例子可以是在特定的身份认证事件之后通知不同的系统,或者仅仅是为了记录和跟踪目的 ( 图 8 )。正如 <在 Spring Security 架构中实现过滤器> 节所述,我们实现了一个示例来展示如何做到这一点。您可以根据实际场景的需要对其进行调整。

对于我们的示例,我们通过在身份认证过滤器之后添加一个过滤器来记录所有成功的身份认证事件 ( 图 8 )。我们认为,绕过身份认证过滤器的事件表示已成功通过身份认证的事件,我们希望将其记录下来。继续 <在 Spring Security 架构中实现过滤器> 节的示例,我们还将记录通过 HTTP 头接收到的请求ID。


我们在 BasicAuthenticationFilter 之后添加
AuthenticationLoggingFilter
,以记录应用程序认证的请求。

下面的清单给出了一个过滤器的定义,该过滤器记录通过身份认证过滤器的请求。

清单 5 定义一个过滤器来记录请求

public class AuthenticationLoggingFilter implements Filter {

  private final Logger logger =
          Logger.getLogger(
          AuthenticationLoggingFilter.class.getName());

  @Override
  public void doFilter(
    ServletRequest request, 
    ServletResponse response, 
    FilterChain filterChain) 
      throws IOException, ServletException {

      var httpRequest = (HttpServletRequest) request;

      var requestId = 
        httpRequest.getHeader("Request-Id"); // 从请求头获取请求ID

      logger.info("Successfully authenticated request with id " +  requestId);  // 使用请求ID的值记录事件

      filterChain.doFilter(request, response); // 将请求转发到链中的下一个过滤器
  }
}

要在身份认证过滤器之后的链中添加自定义过滤器,可以调用 HttpSecurityaddFilterAfter() 方法。下一个清单显示了实现。

清单 6 在过滤器链中的现有过滤器之后添加自定义过滤器

@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http.addFilterBefore(  
            new RequestValidationFilter(),
            BasicAuthenticationFilter.class)
        .addFilterAfter( //将 AuthenticationLoggingFilter 的实例添加到身份验证过滤器之后的过滤器链中
            new AuthenticationLoggingFilter(),
            BasicAuthenticationFilter.class)
        .authorizeRequests()
            .anyRequest().permitAll();
    }
}

运行应用程序并调用端点,我们观察到对于每一次对端点的成功调用,应用程序将在控制台中打印一个日志行。调用:

curl -H "Request-Id:12345" http://localhost:8080/hello

响应体:

Hello!

在控制台中,您可以看到类似这样的行:

INFO 5876 --- [nio-8080-exec-2] y.t.s.f.AuthenticationLoggingFilter: Successfully authenticated request with id 12345

在链中另一个过滤器的位置添加过滤器

在本节中,我们讨论在过滤器链中另一个的位置添过滤器。使用此方法,特别是在为 SpringSecurity 已知的过滤器已经承担的责任提供不同的实现时。一个典型的场景是身份认证。

我们假设您希望实现一些不同的东西,而不是 HTTP 基本身份认证流程。您需要应用另一种方法,而不是使用用户名和密码作为应用程序验证用户的输入凭据。您可能遇到的一些场景示例是

  • 基于静态请求头值的标识来身份认证
  • 使用对称密钥对身份认证请求进行签名
  • 在身份认证过程中使用一次性密码(OTP)

在我们的第一个场景中,基于用于身份验证的静态键的标识,客户端在HTTP请求的头中向应用程序发送一个字符串,这总是相同的。应用程序将这些值存储在某个地方,最有可能是在数据库或秘钥库中。基于这个静态值,应用程序标识客户端。

这种方法 ( 图 9 ) 提供了与身份认证相关的弱安全性,但是架构师和开发人员经常在后端应用程序之间调用时选择它,因为它很简单。实现执行速度也很快,因为它们不需要像应用加密签名那样进行复杂的计算。通过这种方式,用于身份认证的静态键代表了一种妥协,即开发人员在安全性方面更多地依赖于基础设施级别,并且不会让端点完全不受保护。


该请求包含一个带有静态键值的请求头。如果此值与应用程序已知的值匹配,则接受请求。

在我们的第二个场景中,使用对称密钥来签名和验证请求,客户端和服务器都知道密钥的值(客户端和服务器共享密钥)。客户端使用此密钥对请求的一部分进行签名 ( 例如,对特定请求头的值进行签名 ) ,而服务器使用相同的密钥检查签名是否有效 ( 图 10 )。服务器可以将每个客户端的单独密钥存储在数据库或秘钥库中。类似地,您可以使用一对非对称密钥。


Authorization 头包含一个值,该值使用客户端和服务器都知道的密钥 ( 或服务器拥有公共密钥对的私钥 ) 签名。应用程序检查签名,如果正确,则允许请求。

最后,对于我们的第三个场景,在身份认证过程中使用 OTP,用户通过消息或使用谷歌 Authenticator 之类的身份认证提供者应用程序接收OTP ( 图 11 )。


要访问资源,客户端必须使用一次性密码 ( OTP )。客户端向第三方认证服务器获取 OTP。通常,当需要多因素身份认证时,应用程序在登录时使用这种方法。

让我们实现一个示例来演示如何应用自定义过滤器。为了使案例既相关又简单,我们将重点放在配置上,并考虑一个简单的身份认证逻辑。在我们的场景中,我们有一个静态键的值,它对所有请求都是相同的。用户进行身份认证时,必须在 Authorization 头中添加正确的静态键值,如图 12 所示。


我们首先实现名为
StaticKeyAuthenticationFilter
的过滤器类。该类从属性文件中读取静态键的值,并验证 Authorization 头的值是否相等。如果值相同,则过滤器将请求转发给过滤器链中的下一个组件。如果不是,过滤器将值 401 Unauthorized 设置为响应的 HTTP 状态,而不转发请求到过滤器链。清单 7 定义了
StaticKeyAuthenticationFilter
类。在后面文章,也就是下一个实践练习中,我们将研究并实现一个解决方案,其中我们也将应用加密签名进行身份认证。

清单 7
StaticKeyAuthenticationFilter
类的定义


@Component // 为了允许我们从属性文件中注入值,需要在Spring上下文中添加类的一个实例
public class StaticKeyAuthenticationFilter 
  implements Filter { //通过实现 Filter 接口和重写 doFilter() 方法定义身份认证逻辑

  @Value("${authorization.key}")  //使用 @Value 注解从属性文件中获取静态键的值
  private String authorizationKey;

  @Override
  public void doFilter(ServletRequest request, 
                       ServletResponse response, 
                       FilterChain filterChain) 
    throws IOException, ServletException {

    var httpRequest = (HttpServletRequest) request;
    var httpResponse = (HttpServletResponse) response;

    String authentication =
           httpRequest.getHeader("Authorization");

    if (authorizationKey.equals(authentication)) {
        filterChain.doFilter(request, response);
    } else {
        httpResponse.setStatus(
                         HttpServletResponse.SC_UNAUTHORIZED);
    }
  }
}

一旦定义了过滤器,就可以使用 addFilterAt() 方法将它添加到过滤器链中
Basic-AuthenticationFilter
类的位置( 图 13 )。


如果使用 HTTP Basic 作为身份认证方法,我们将在类 BasicAuthenticationFilter 所在的位置添加自定义身份认证过滤器。这意味着我们的自定义过滤器具有相同的排序值。

但请记住我们在前面小节中讨论过的内容。当在特定位置添加过滤器时,Spring Security 并不假定它是该位置的唯一过滤器。您可以在链中的相同位置添加更多过滤器。在这种情况下,Spring Security 不能保证这些操作的顺序。我再说一遍,因为我见过很多人对这是怎么回事感到困惑。一些开发人员认为,当您在一个已知的位置应用一个过滤器时,它将被替换。事实并非如此!我们必须确保不向链中添加不需要的过滤器。

注意

我建议您不要在链的同一位置添加多个过滤器。当您在相同位置添加更多过滤器时,它们的使用顺序没有定义。有一个确定的过滤器调用顺序是有意义的。有一个已知的序号可以使您的应用程序更容易理解和维护。

在清单 8 中,您可以找到添加过滤器的配置类的定义。请注意,这里我们没有调用来自 HttpSecurity 类的 httpBasic() 方法,因为我们不希望 BasicAuthenticationFilter 实例被添加到过滤器链中。

清单 8 在配置类中添加过滤器

@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {

  // 从Spring上下文注入过滤器的实例
  @Autowired
  private StaticKeyAuthenticationFilter filter;

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http.addFilterAt(filter,   //在过滤器链中基本身份认证过滤器的位置添加过滤器
           BasicAuthenticationFilter.class)
        .authorizeRequests()
           .anyRequest().permitAll();
  }
}

为了测试应用程序,我们还需要一个端点。为此,我们定义一个控制器,如清单 4 所示。您应该在应用程序的服务器上为静态键添加一个值。属性文件,如下代码片段所示:

authorization.key=SD9cICjl1e

注意

对于生产应用程序来说,在属性文件中存储密码、密钥或任何其他不应该被所有人看到的数据从来都不是一个好主意。在我们的示例中,我们使用这种方法是为了简单,并允许您将重点放在我们所做的 Spring Security 配置上。但在现实场景中,请确保使用一个密钥库来存储这类细节。

现在可以测试应用程序了。我们期望应用程序允许具有正确的 Authorization 头值的请求,并拒绝其他请求,在响应上返回 HTTP 401 Unauthorized 状态。下面的代码片段展示了用于测试应用程序的 curl 调用。如果使用服务器端为 Authorization 头设置的相同值,则调用成功,您将看到响应体 Hello! 调用

curl -H "Authorization:SD9cICjl1e" http:/ /localhost:8080/hello

返回的响应体:

Hello!

在下面的调用中,如果 Authorization 头丢失或不正确,则响应状态为 HTTP 401 Unauthorized:

curl -v http://localhost:8080/hello

响应状态:

...
< HTTP/1.1 401
...

在本例中,因为我们没有配置 UserDetailsService, Spring Boot会自动配置一个 UserDetailsService ,就像你在前面文章学到的那样。但是在我们的场景中,您根本不需要 UserDetailsService,因为用户的概念并不存在。我们只验证请求调用服务器上端点的用户是否知道给定的值。应用程序场景通常不会这么简单,通常需要一个 UserDetailsService。但是,如果您预料到或遇到不需要此组件的情况,则可以禁用自动配置。要禁用默认 UserDetailsService 的配置,你可以在主类上使用 @SpringBootApplication 注解的 exclude 属性,如下所示:

@SpringBootApplication(exclude = {UserDetailsServiceAutoConfiguration.class })

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表