Spring Security in Action 第九章 SpringSecurity常用过滤器介绍

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


本章包括

  • 使用过滤器链
  • 定义自定义过滤器
  • 使用实现过滤器接口的Spring Security类

在Spring Security中,HTTP filters(过滤器)对不同的HTTP请求进行过滤。在第三章到第五章,我们讨论了HTTP的认证和授权架构,我经常提到过滤器。在Spring Security中,一般来说,HTTP filters对请求进行过滤。这些过滤器形成了一个责任链。一个过滤器接收一个请求,执行其逻辑,并最终将请求委托给链中的下一个过滤器(图9.1)。

image-20230120110750070

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

这个想法很简单。当你去机场时,从进入航站楼到登上飞机,你要经过多个过滤器(图9.2)。

image-20230120110930690

图9.2 在机场,你要经过一个过滤链才能最终登上飞机。同样地,Spring Security也有一个过滤链,作用于应用程序收到的HTTP请求。

首先会检查你的机票,然后检查护照,而后,你要通过安检。在机场门口,可能会有更多的 "过滤器 "被应用。例如,在某些情况下,就在登机前,你的护照和签证会再一次被验证。这是对Spring Security中的过滤器链的一个很好的比喻。以同样的方式,你在Spring Security的过滤器链中定制过滤器,对HTTP请求采取行动。Spring Security提供了过滤器的实现,你可以通过自定义将其添加到过滤器链中,但你也可以定义自定义过滤器。

在本章中,我们将讨论如何定制作Spring Security中认证和授权架构一部分:过滤器。例如,你可能想通过为用户增加一个步骤来增强认证,如检查他们的电子邮件地址或使用一次性密码。你会发现应用程序使用认证的各种场景:从调试目的到识别用户的行为。使用今天的技术和机器学习算法可以改善应用程序,例如,通过学习用户的行为,知道是否有人入侵了他们的账户或冒充了用户。

知道如何定制HTTP过滤器的责任链是一项宝贵的技能。在实践中,应用程序有各种各样的要求,使用默认的配置是行不通的。你需要添加或替换现有的责任链的组成部分。在默认实现中,使用HTTP Basic认证方法,它允许你依靠用户名和密码。但在实际场景中,有很多情况下你需要比这更多的东西。 也许你需要实现不同的认证策略,通知外部系统一个授权事件,或者简单地记录一个成功或失败的认证,然后用于跟踪和审计(图9.3)。无论你的场景是什么,Spring Security都能为你提供灵活性,按照你的需要精确地建模过滤链。

image-20230120111423397

图9.3 你可以通过在现有过滤器之前、之后或在其位置添加新的过滤器来定制过滤器链。这样,你可以定制认证以及应用于请求和响应的整个过程。

9.1 在Spring Security架构中实现过滤器

在本节中,我们将讨论过滤器和过滤器链在Spring Security架构中的工作方式。你需要先了解这个总体概况,以便理解我们在本章接下来的章节中所进行的实现示例。你在前几章中了解到,认证过滤器拦截请求并将认证责任进一步委托给授权管理器。如果我们想在认证之前执行某些逻辑,我们可以在认证过滤器之前插入一个过滤器来实现。

Spring Security架构中的过滤器是典型的HTTP过滤器。我们可以通过实现javax.servlet包中的Filter接口来创建过滤器。和其他的HTTP过滤器一样,你需要覆盖doFilter()方法来实现其逻辑。这个方法接收ServletRequest、ServletResponse和FilterChain作为参数。

  • ServletRequest-代表HTTP请求。我们使用ServletRequest对象来检索关于请求的细节。
  • ServletResponse-代表HTTP响应。我们使用Servlet-Response对象来改变响应,然后再把它发回给客户端或沿着过滤链进一步发送。
  • FilterChain-代表过滤器的链。我们使用FilterChain对象将请求转发给链上的下一个过滤器。

过滤器链代表了一个过滤器的集合,它有一个确定的行动顺序。 Spring Security为我们提供了一些过滤器的实现和它们的顺序。在所提供的过滤器中

  • BasicAuthenticationFilter负责处理HTTP Basic认证,如果存在的话。
  • CsrfFilter负责跨站请求伪造(CSRF)保护,我们将在第10章讨论。
  • CorsFilter负责处理跨源资源共享(CORS)的授权规则,我们也会在第10章中讨论这个问题。

你不需要知道所有的过滤器,因为你可能不会从你的代码中直接接触到这些过滤器,但你确实需要了解过滤器链是如何工作的,并注意到一些实现。重要的是要理解,一个应用程序不一定在链上有所有这些过滤器的实例。该链或长或短,取决于你如何配置应用程序。如果想使用HTTP Basic认证方法,需要调用HttpSecurity类的httpBasic()方法,如果你调用httpBasic()方法,BasicAuthenticationFilter的一个实例会被添加到链中。

image-20230120144512390

图9.4 每个过滤器都有一个顺序号。这决定了过滤器应用于请求的顺序。你可以在Spring Security提供的过滤器的基础上添加自定义过滤器。

注意 如果多个过滤器有相同的位置,那么它们被调用的顺序就不会被定义。

image-20230120145542693

图9.5 你可能在链中有多个具有相同顺序值的过滤器。在这种情况下,Spring Security并不保证它们被调用的顺序。

9.2 在链中的现有过滤器之前添加一个过滤器

在这一节中,我们讨论在过滤器链中的现有过滤器之前应用自定义的HTTP过滤器。为了以实用的方式来处理这个问题,我们将在一个项目上做例子。通过这个例子,你将很容易学会实现一个自定义过滤器,并在过滤器链中的现有过滤器之前应用它。然后,你可以把这个例子改编成你在开发中可能会遇到的任何类似需求。

对于我们的第一个自定义过滤器的实现,我们要确保任何请求的header都有Request-Id。

我们假设我们的应用程序使用这个头来跟踪请求,并且这个头是强制性的。同时,我们希望在应用程序执行认证之前验证这些假设。认证过程可能涉及查询数据库或其他消耗资源的操作,如果请求的格式无效,我们不希望应用程序执行这些操作。我们如何做到这一点呢?要解决当前的需求只需要两个步骤,最后,过滤器链看起来就像图9.6中的那样。

image-20230120150531819

图9.6 在我们的例子中,我们添加了一个RequestValidationFilter,它在认证过滤器之前发挥作用。RequestValidationFilter确保如果请求验证失败,认证就不会发生。在我们的例子中,请求必须有一个名为Request-Id的强制标头。

1 实现过滤器。创建一个RequestValidationFilter类,检查请求中是否存在需要的头。

2 将过滤器添加到过滤器链中。在配置类中这样做,重写configure()方法。

代码清单9.1 实现一个自定义过滤器

package com.hashnode.proj0001firstspringsecurity.order;

import javax.servlet.*;
import java.io.IOException;

public class RequestValidationFilter implements Filter {
    
    
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
    
    
        
    }
}

在我们的例子中,我们检查RequestId头是否存在。如果存在,我们通过调用doFilter()方法将请求转发给链中的下一个过滤器。如果头不存在,我们就在响应上设置HTTP状态400坏请求,而不把它转发到链中的下一个过滤器(图9.7)。代码清单9.2展示了这个逻辑。

清单9.2 实现doFilter()方法中的逻辑

package com.hashnode.proj0001firstspringsecurity.order;

import cn.hutool.core.util.StrUtil;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class RequestValidationFilter implements Filter {
    
    
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
    
    
        HttpServletRequest httpRequest = (HttpServletRequest) servletRequest;
        HttpServletResponse httpResponse = (HttpServletResponse) servletResponse;
        String requestId = httpRequest.getHeader("RequestId");
        if (requestId == null || StrUtil.isBlank(requestId)) {
    
    
            httpResponse.setStatus(HttpServletResponse.SC_BAD_REQUEST);
            return;
        }
        filterChain.doFilter(servletRequest, servletResponse);
    }
}

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

  • 我们想添加到链中的自定义过滤器的一个过滤器–在我们的例子中,这是代码清单9.1中介绍的RequestValidationFilter类的一个实例。
  • 我们在哪个过滤器之前添加新的过滤器-在这个例子中,由于要求在认证之前执行过滤器逻辑,我们需要在认证过滤器之前添加我们的自定义过滤器实例。Basic- AuthenticationFilter类定义了认证过滤器的默认类型。

到目前为止,我们把处理认证的过滤器一般称为认证过滤器。在下一章中,你会发现Spring Security还配置了其他过滤器。在第10章中,我们将讨论跨站请求伪造(CSRF)保护和跨源资源共享(CORS),它们也依赖于过滤器。

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

代码清单9.3 在认证前配置自定义过滤器

package com.hashnode.proj0001firstspringsecurity.config;

import com.hashnode.proj0001firstspringsecurity.order.RequestValidationFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
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;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;

/**
 * @author Guowei Chi
 * @date 2023/1/19
 * @description:
 **/
@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {
    
    
    @Bean
    @Override
    public UserDetailsService userDetailsService(){
    
    
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();

        UserDetails user1 = User.withUsername("john")
                .password("12345")
                //具有ROLE_前缀,表示角色
                .roles("read")
                .build();

        UserDetails user2 = User.withUsername("jane")
                .password("12345")
                .roles("read","premium")
                .build();

        manager.createUser(user1);
        manager.createUser(user2);
        return manager;
    }

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


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

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

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

测试结果如下

curl -v http://localhost:8080/hello
...
< HTTP/1.1 400
...

curl -H "RequestId:12345" http://localhost:8080/hello
Hello!

9.3 在链中的现有过滤器之后添加一个过滤器

在这一节中,我们将讨论在过滤器链中的现有过滤器之后添加一个过滤器。当你想在过滤器链中已经存在的东西之后执行一些逻辑时,你会使用这种方法。让我们假设你必须在认证过程之后执行一些逻辑。如同在第9.1节中,我们实现了一个例子来告诉你如何做到这一点。你可以根据你的需要对它进行调整,以适应真实世界的场景。

在我们的例子中,我们通过在认证过滤器之后添加一个过滤器来记录所有成功的认证事件(图9.8)。我们认为绕过认证过滤器的是一个成功认证的事件,我们要记录它。继续第9.1节的例子,我们也记录通过HTTP头收到的HeaderID。

image-20230121091859693

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

下面列出了一个过滤器的定义,它记录通过认证过滤器的请求。

代码清单9.5 定义一个过滤器来记录请求

package com.hashnode.proj0001firstspringsecurity.order;


import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.logging.Logger;

public class AuthenticationLoggingFilter implements Filter {
    
    
    private final Logger logger = Logger.getLogger(AuthenticationLoggingFilter.class.getName());

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
    
    
        HttpServletRequest httpRequest = (HttpServletRequest) servletRequest;
        String requestId = httpRequest.getHeader("RequestId");
        logger.info("Successfully authenticated request with id " + requestId);
        filterChain.doFilter(servletRequest, servletResponse);
    }
}

为了在认证过滤器之后的链中添加自定义过滤器,你可以调用HttpSecurity的addFilterAfter()方法。下一个列表显示了实现的情况。

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

package com.hashnode.proj0001firstspringsecurity.config;

import com.hashnode.proj0001firstspringsecurity.order.AuthenticationLoggingFilter;
import com.hashnode.proj0001firstspringsecurity.order.RequestValidationFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
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;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;

/**
 * @author Guowei Chi
 * @date 2023/1/19
 * @description:
 **/
@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {
    
    
    @Bean
    @Override
    public UserDetailsService userDetailsService(){
    
    
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();

        UserDetails user1 = User.withUsername("john")
                .password("12345")
                //具有ROLE_前缀,表示角色
                .roles("read")
                .build();

        UserDetails user2 = User.withUsername("jane")
                .password("12345")
                .roles("read","premium")
                .build();

        manager.createUser(user1);
        manager.createUser(user2);
        return manager;
    }

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


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

测试结果如下

curl -H "RequestId:12345" http://localhost:8080/hello
Hello!

日志如下
INFO 5876 --- [nio-8080-exec-2] c.l.s.f.AuthenticationLoggingFilter:
Successfully authenticated request with id 12345

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

在这一节中,我们将讨论在过滤器链中的另一个过滤器的位置添加一个过滤器。特别是在为一个已经被Spring Security知道的过滤器所承担的责任提供不同的实现时,你会使用这种方法。一个典型的场景是认证。

让我们假设,你想实现不同的东西,而不是HTTP Basic认证流程。你需要采用另一种方法,而不是使用用户名和密码作为输入的凭证,应用程序在此基础上对用户进行认证。你可能遇到的一些场景的例子是

  • 基于静态header value的识别,用于认证
  • 使用对称密钥对认证请求进行签名
  • 在认证过程中使用一次性密码(OTP)。

在我们的第一种情况下,基于静态密钥的身份验证,客户端在HTTP的header中向应用程序发送一个字符串,这个字符串总是相同的。 应用程序将这些值存储在某个地方,很可能是在数据库。基于这个静态值,应用程序可以识别客户。

这种方法(图9.9)提供了与认证有关的弱安全性,但架构师和开发人员在后端应用程序之间的调用中经常选择它,因为它很简单。这些实现也执行得很快,因为这些不需要做复杂的计算,就像应用加密签名的情况一样。

image-20230121175924124

图9.9 请求包含一个带有静态密钥值的头。如果这个值与应用程序已知的值相匹配,它就接受这个请求。

在我们的第二个案例中,使用对称密钥来签署和验证请求,客户和服务器都知道一个密钥的值(客户和服务器共享密钥)。客户端使用该密钥对请求的一部分进行签名(例如,对特定头文件的值进行签名),而服务器则使用相同的密钥检查签名是否有效(图9.10)。服务器可以在数据库或保密库中为每个客户存储单独的密钥。同样,你也可以使用一对非对称密钥。

image-20230121180341638

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

最后,对于我们的第三种情况,在认证过程中使用OTP,用户通过信息或使用谷歌认证器等认证供应商的应用程序来接收OTP(图9.11)。

image-20230121180705837

图9.11 为了访问资源,客户必须使用一次性密码(OTP)。 客户从第三方认证服务器获得OTP。一般来说,当需要多因素认证时,应用程序在登录时使用这种方法。

让我们实现一个例子来演示如何应用一个自定义过滤器。为了保持案例的相关性但又简单明了,我们把重点放在配置上并考虑一个简单的认证逻辑。在我们的方案中,我们有一个静态密钥的值,它对所有请求都是一样的。为了进行认证,用户必须在授权头中添加正确的静态密钥值,如图9.12所示。你可以在项目sia-ch9-ex2中找到这个例子的代码。

image-20230121181209858

图9.12 客户端在HTTP请求的授权头中添加了一个静态密钥。 服务器在授权请求之前会检查它是否知道这个密钥。

我们首先实现过滤器类,名为StaticKeyAuthenticationFilter。这个类从属性文件中读取静态密钥的值并验证header的值是否与之相等。如果数值相同,过滤器将请求转发给过滤器链中的下一个组件。如果不一样,过滤器将401 Unauthorized的值设置为响应的HTTP状态,而不在过滤器链中转发请求。代码清单9.7定义了StaticKeyAuthenticationFilter类。

代码清单9.7 StaticKeyAuthenticationFilter类的定义

package com.hashnode.proj0001firstspringsecurity.order;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class StaticKeyAuthenticationFilter implements Filter {
    
    
    @Value("${authorization.key}")
    private String authorizationKey;

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
    
    
        HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
        HttpServletResponse httpServletResponse = (HttpServletResponse) servletResponse;

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

        if (authorizationKey.equals(authentication)) {
    
    
            filterChain.doFilter(httpServletRequest, httpServletResponse);
        } else {
    
    
            httpServletResponse.setStatus(
                    HttpServletResponse.SC_UNAUTHORIZED);
        }
    }
}

一旦我们定义了过滤器,我们就通过使用addFilterAt()方法将其添加到过滤器链的BasicAuthenticationFilter的位置(图9.13)。

image-20230121182213703

图9.13 我们在BasicAuthenticationFilter类的位置添加了我们的自定义认证过滤器,这意味着我们的自定义过滤器有相同的排序值。

但请记住我们在第9.1节中讨论的内容。当在一个特定的位置添加一个过滤器时,Spring Security并不假定它是该位置上唯一的一个。你可能会在链中的同一位置添加更多的过滤器。在这种情况下,Spring Security并不保证这些过滤器将以何种顺序发挥作用。一些程序员认为,当你在一个已知的位置应用一个文件时,它将被替换。但事实并非如此。我们必须确保不在链上添加我们不需要的过滤器。

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

在代码清单9.8中,你可以找到添加过滤器的配置类的定义。观察一下,我们在这里没有调用HttpSecurity类中的httpBasic()方法,如图9.14所示,因为我们不希望BasicAuthenticationFilter实例被添加到过滤器链。

image-20230121184214012

图9.14 在这里没有调用HttpSecurity类中的httpBasic()方法。

代码清单9.8 在配置类中添加过滤器

package com.hashnode.proj0001firstspringsecurity.config;

import com.hashnode.proj0001firstspringsecurity.order.AuthenticationLoggingFilter;
import com.hashnode.proj0001firstspringsecurity.order.RequestValidationFilter;
import com.hashnode.proj0001firstspringsecurity.order.StaticKeyAuthenticationFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
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;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;

/**
 * @author Guowei Chi
 * @date 2023/1/19
 * @description:
 **/
@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {
    
    
    @Autowired
    private StaticKeyAuthenticationFilter filter;
    
    @Bean
    @Override
    public UserDetailsService userDetailsService(){
    
    
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();

        UserDetails user1 = User.withUsername("john")
                .password("12345")
                //具有ROLE_前缀,表示角色
                .roles("read")
                .build();

        UserDetails user2 = User.withUsername("jane")
                .password("12345")
                .roles("read","premium")
                .build();

        manager.createUser(user1);
        manager.createUser(user2);
        return manager;
    }

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


    @Override
    protected void configure(HttpSecurity http) throws Exception {
    
    
        http.addFilterAt(filter,BasicAuthenticationFilter.class)
                .authorizeRequests().anyRequest().permitAll();
    }
}

为了测试这个应用程序,我们还需要一个api。为此,我们定义一个控制器,如代码清单9.4所示。你应该在appli-cation.properties文件中为服务器上的静态密钥添加一个值,如这段代码所示: authorization.key=SD9cICjl1e

注意 在属性文件中存储密码、密钥或任何其他不打算让所有人看到的数据,对于生产应用来说绝不是一个好主意。在我们的例子中,我们使用这种方法是为了简化,让你专注于我们的Spring Security配置。

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

...
< HTTP/1.1 401
...

在这种情况下,由于我们没有配置UserDetailsService,Spring Boot会自动配置一个,正如你在第2章中学到的。但在我们的场景中,你根本不需要UserDetailsService,因为用户的概念并不存在。我们只是验证请求调用服务器上的端点的用户是否知道一个给定的值。应用场景通常没有这么简单,通常需要一个UserDetailsService。但是,如果你预计到或有这样的情况,不需要这个组件,你可以禁用自动配置。要禁用默认的UserDetailsService的配置,你可以在主类上使用@SpringBootApplication注解的exclude属性,像这样。

@SpringBootApplication(exclude =
{
    
    UserDetailsServiceAutoConfiguration.class })

9.5 Spring Security提供的过滤器实现

在本节中,我们将讨论Spring Security提供的实现Filter接口的类。在本章的例子中,我们通过直接实现这个接口来定义过滤器。

Spring Security提供了一些实现Filter接口的抽象类,你可以为它们扩展你的过滤器定义。当你扩展这些类时,你的实现也可以从中受益。例如,你可以扩展GenericFilterBean类,它允许你使用在web.xml描述符文件中定义的初始化参数。 扩展GenericFilterBean的一个更有用的类是OncePerRequestFilter。当添加一个过滤器到链中时,框架并不保证它在每个请求中只被调用一次。OncePerRequestFilter,顾名思义,实现了确保过滤器的doFilter()方法在每个请求中只执行一次的逻辑。

如果你的应用中需要这样的功能,请使用Spring提供的类。我经常看到开发者扩展GenericFilterBean类,而不是在不需要GenericFilterBean类添加的自定义逻辑的功能中实现Filter接口。当问及原因时,他们似乎不知道。他们可能是在网上找到的例子中复制了这个代码。

为了清楚地说明如何使用这样一个类,我们来写一个例子。我们在第9.3节中实现的日志功能是使用OncePerRequestFilter的最佳案例。我们想避免多次记录相同的请求。Spring Security并不保证过滤器不会被多次调用,所以我们必须自己解决这个问题。最简单的方法是使用OncePerRequestFilter类来实现该过滤器。

在代码清单9.9中,你可以看到我对AuthenticationLoggingFilter类所作的修改。现在它没有像第9.3节中的例子那样直接实现Filter接口,而是扩展了OncePerRequestFilter类。我们在这里覆盖的方法是doFilterInternal()。

package com.hashnode.proj0001firstspringsecurity.order;


import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.logging.Logger;

public class AuthenticationLoggingFilter extends OncePerRequestFilter {
    
    
    private final Logger logger = Logger.getLogger(AuthenticationLoggingFilter.class.getName());



    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
    
    
        String requestId = request.getHeader("RequestId");
        logger.info("Successfully authenticated request with id " + requestId);
        filterChain.doFilter(request, response);
    }
}

关于OncePerRequestFilter类的一些简短的看法,你可能会发现很有用。

  • 它只支持HTTP请求,但这实际上是我们一直使用的。它的好处是,它对类型进行了转换,我们可以直接接收作为HttpServletRequest和HttpServletResponse的请求。还记得吗,在使用Filter接口时,我们必须对请求和响应进行转换。
  • 你可以实现逻辑来决定过滤器是否被应用。你可以通过重写shouldNotFilter(HttpServletRequest)方法来设置。默认情况下,该过滤器适用于所有请求。
  • 默认情况下,OncePerRequestFilter并不适用于异步请求或错误的dispatch requests。你可以通过覆盖shouldNotFilterAsyncDispatch()和shouldNotFilterErrorDispatch()的方法来改变这种行为。

如果你发现OncePerRequestFilter的这些特性在你的实现中很有用,我建议你使用这个类来定义你的过滤器。

总结

  • Web应用架构的第一层,即拦截HTTP请求,是一个过滤器链。至于Spring安全架构中的其他组件,你可以根据你的要求对其进行定制。
  • 你可以通过在现有过滤器之前、之后或在现有过滤器的位置添加新的过滤器来定制过滤器链。
  • 你可以在一个现有的过滤器的同一位置有多个过滤器。在这种情况下,过滤器的执行顺序没有被定义。
  • 改变过滤链可以帮助你定制认证和授权,以精确匹配你的应用要求。

猜你喜欢

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