【SpringSecurity】基本流程

【中文文档: Spring Security 中文文档 :: Spring Security Reference

【英文文档:Spring Security

以下内容只是记录springsecurity最简单的一种验证流程,所有配置基本都是默认的配置。

引入依赖

复制代码
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-security</artifactId>
</dependency>

版本是: springSecurity是6.2.4

启动类加入注解

在springboot 应用的启动类加上主键

复制代码
@EnableWebSecurity

配置账户密码

复制代码
spring:
  application:
    name: springbootmvc
  security:
    user:
      name: admin
      password: 123456

访问页面

简单分析流程

1、首先是spring-security 是通过一连串Filter 来实现相关功能的。

其中有个重要的过滤器是:DelegatingFilterProxy ,从它的名称deltegate大概猜到它是一个委托代理的对象,它自己其实不做什么,主要是委托给 它内部的一个Filter去做相关的操作,关键代码

断点信息显示:

看下被委托的Filter【delegateToUse】 的对象基本结构:

然后看下上图中被委托的内部类大体内容: WebMvcSecurityConfiguration

上图中的springSecurityFilterChain 是spring-security提供的一个默认实现:

DefaultSecurityFilterChain ,下面就进入这个类看下:

后面就循环执行上图中Filter集合中的所有的Filter。

下图对springSecurity开始的步骤涉及到的对象做了一个初步的分析。

学习记录:

如果密码验证成功的话: 默认情况下 springSecurity会重定向到 http://server:port/context-path/

然后在进入filter就要判断资源访问的授权了。

官方文档几大对象的总结

1、DelegatingFilterProxy

Spring 提供了一个名为 DelegatingFilterProxyFilter 实现,允许在 Servlet 容器的生命周期和 Spring 的 ApplicationContext 之间建立桥梁。Servlet容器允许通过使用自己的标准来注册 Filter 实例,但它不知道 Spring 定义的 Bean。你可以通过标准的Servlet容器机制来注册 DelegatingFilterProxy,但将所有工作委托给实现 Filter 的Spring Bean。

下面是 DelegatingFilterProxy 如何融入 Filter 实例和 FilterChain 的图片

DelegatingFilterProxyApplicationContext 查找 Bean Filter0 ,然后调用 Bean Filter0 。下面的列表显示了 DelegatingFilterProxy 的伪代码。

2、FilterChainProxy

Spring Security 的 Servlet 支持包含在 FilterChainProxy 中。FilterChainProxy 是 Spring Security 提供的一个特殊的 Filter,允许通过 SecurityFilterChain 委托给许多 Filter 实例。由于 FilterChainProxy 是一个Bean,它通常被包裹在 DelegatingFilterProxy 中。

下图显示了 FilterChainProxy 的作用。

3、SecurityFilterChain

SecurityFilterChainFilterChainProxy 用来确定当前请求应该调用哪些 Spring Security Filter 实例。

下图显示了 SecurityFilterChain 的作用。

SecurityFilterChain 中的 Security Filter 通常是Bean,但它们是用 FilterChainProxy 而不是 DelegatingFilterProxy 注册的。与直接向Servlet容器或 DelegatingFilterProxy 注册相比,FilterChainProxy 有很多优势。首先,它为 Spring Security 的所有 Servlet 支持提供了一个起点。由于这个原因,如果你试图对 Spring Security 的 Servlet 支持进行故障诊断,在 FilterChainProxy 中添加一个调试点是一个很好的开始。

其次,由于 FilterChainProxy 是 Spring Security 使用的核心,它可以执行一些不被视为可有可无的任务。 例如,它清除了 SecurityContext 以避免内存泄漏。它还应用Spring Security的 HttpFirewall 来保护应用程序免受某些类型的攻击。

此外,它在确定何时应该调用 SecurityFilterChain 方面提供了更大的灵活性。在Servlet容器中,Filter 实例仅基于URL被调用。 然而,FilterChainProxy 可以通过使用 RequestMatcher 接口,根据 HttpServletRequest 中的任何内容确定调用。

下图显示了多个 SecurityFilterChain 实例

Multiple SecurityFilterChain 图中, FilterChainProxy 决定应该使用哪个 SecurityFilterChain。只有第一个匹配的 SecurityFilterChain 被调用。如果请求的URL是 /api/messages/,它首先与 /api/**SecurityFilterChain0 模式匹配,所以只有 SecurityFilterChain0 被调用,尽管它也与 SecurityFilterChainn 匹配。如果请求的URL是 /messages/,它与 /api/**SecurityFilterChain0 模式不匹配,所以 FilterChainProxy 继续尝试每个 SecurityFilterChain。假设没有其他 SecurityFilterChain 实例相匹配,则调用 SecurityFilterChainn

请注意,SecurityFilterChain0 只配置了三个 security Filter 实例。然而,SecurityFilterChainn 却配置了四个 security Filter 实例。值得注意的是,每个 SecurityFilterChain 都可以是唯一的,并且可以单独配置。事实上,如果应用程序希望 Spring Security 忽略某些请求,那么一个 SecurityFilterChain 可能会有零个 security Filter 实例。

4、Security Filter

Security Filter 是通过 SecurityFilterChain API 插入 FilterChainProxy 中的。

这些 filter 可以用于许多不同的目的,如 认证授权漏洞保护 等等。filter 是按照特定的顺序执行的,以保证它们在正确的时间被调用,例如,执行认证的 Filter 应该在执行授权的 Filter 之前被调用。一般来说,没有必要知道 Spring Security 的 Filter 的顺序。但是,有些时候知道顺序是有好处的,如果你想知道它们,可以查看 FilterOrderRegistration 代码

为了解释上面这段话,让我们考虑以下 security 配置:

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(Customizer.withDefaults())
            .authorizeHttpRequests(authorize -> authorize
                .anyRequest().authenticated()
            )
            .httpBasic(Customizer.withDefaults())
            .formLogin(Customizer.withDefaults());
        return http.build();
    }

}

上述配置中的 Filter 顺序如下:

Filter 添加者
CsrfFilter HttpSecurity#csrf
UsernamePasswordAuthenticationFilter HttpSecurity#formLogin
BasicAuthenticationFilter HttpSecurity#httpBasic
AuthorizationFilter HttpSecurity#authorizeHttpRequests
  1. 首先,调用 CsrfFilter 来防止 CSRF 攻击

  2. 其次,认证 filter 被调用以认证请求。

  3. 第三,调用 AuthorizationFilter 来授权该请求。

添加自定义 Filter 到 Filter Chain

大多数情况下,默认的 security filter 足以为你的应用程序提供安全。然而,有时你可能想在 security filter chain 中添加一个自定义的 filter。

例如,假设你想添加一个 Filter,获得一个租户 id header 并检查当前用户是否有访问该租户的权限。前面的描述已经给了我们一个添加 filter 的线索,因为我们需要知道当前的用户,所以我们需要在认证 filter 之后添加它。

首先,创建一个 Filter

import java.io.IOException;

import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import org.springframework.security.access.AccessDeniedException;

public class TenantFilter implements Filter {

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

        String tenantId = request.getHeader("X-Tenant-Id"); 
        boolean hasAccess = isUserAllowed(tenantId); 
        if (hasAccess) {
            filterChain.doFilter(request, response); 
            return;
        }
        throw new AccessDeniedException("Access denied"); 
    }

}

上面的示例代码做了以下工作:

|---|-----------------------------------------|
| | 从请求头中获取租户ID。 |
| | 检查当前用户是否对租户ID有访问权。 |
| | 如果该用户有访问权,那么就调用链中其他的 filter。 |
| | 如果用户没有权限,则抛出一个 AccessDeniedException。 |

你可以从 OncePerRequestFilter 中继承,而不是实现 Filter,这是一个基类,用于每个请求只调用一次的 filter,并提供一个带有 HttpServletRequestHttpServletResponse 参数的 doFilterInternal 方法。现在,我们需要把这个 filter 添加到 security filter chain 中。

@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        // ...
        .addFilterBefore(new TenantFilter(), AuthorizationFilter.class); 
    return http.build();
}

|--------------------------------------------------------------------------------|
| 使用 HttpSecurity#addFilterBeforeAuthorizationFilter 之前添加 TenantFilter。 |

通过在 AuthorizationFilter 之前添加filter,我们确保 TenantFilter 在认证 filter 之后被调用。你也可以使用 HttpSecurity#addFilterAfter 将 filter 添加到某个特定的 filter 之后,或者使用 HttpSecurity#addFilterAt 将 filter 添加到 filter chain 中的某个位置。

就这样,现在 TenantFilter 将在过 filter chain 中被调用,并将检查当前用户是否对租户ID有访问权。

当你把你的 filter 声明为 Spring Bean 时要小心,可以用 @Component 注解它,也可以在配置中把它声明为 Bean,因为 Spring Boot 会自动 在嵌入式容器中注册它。这可能会导致 filter 被调用两次,一次由容器调用,一次由 Spring Security 调用,而且顺序不同。

如果你仍然想把你的 filter 声明为 Spring Bean,以利用依赖注入,避免重复调用,你可以通过声明 FilterRegistrationBean Bean 并将其 enabled 属性设置为 false 来告诉 Spring Boot 不要向容器注册它:

@Bean
public FilterRegistrationBean<TenantFilter> tenantFilterRegistration(TenantFilter filter) {
    FilterRegistrationBean<TenantFilter> registration = new FilterRegistrationBean<>(filter);
    registration.setEnabled(false);
    return registration;
}

处理 Security 异常

ExceptionTranslationFilter 允许将 AccessDeniedExceptionAuthenticationException 翻译成 HTTP 响应。

ExceptionTranslationFilter 作为 Security Filter 之一被插入到 FilterChainProxy 中。

下面的图片显示了 ExceptionTranslationFilter 与其他组件的关系。

  • 首先,ExceptionTranslationFilter 调用 FilterChain.doFilter(request, response) 来调用应用程序的其他部分。

  • 如果用户没有被认证,或者是一个 AuthenticationException,那么就 开始认证

    • SecurityContextHolder 被清理掉。

    • HttpServletRequest保存起来,这样一旦认证成功,它就可以用来重放原始请求。

    • AuthenticationEntryPoint 用于请求客户的凭证。例如,它可以重定向到一个登录页面或发送一个 WWW-Authenticate 头。

  • 否则,如果是 AccessDeniedException,那么就是 Access DeniedAccessDeniedHandler 被调用来处理拒绝访问(access denied)。

|---|----------------------------------------------------------------------------------------------------------|
| | 如果应用程序没有抛出 AccessDeniedExceptionAuthenticationException,那么 ExceptionTranslationFilter 就不会做任何事情。 |

ExceptionTranslationFilter 的伪代码看起来是这样的。

try {
	filterChain.doFilter(request, response); 
} catch (AccessDeniedException | AuthenticationException ex) {
	if (!authenticated || ex instanceof AuthenticationException) {
		startAuthentication(); 
	} else {
		accessDenied(); 
	}
}

正如 Filter(过滤器)回顾 中所述,调用 FilterChain.doFilter(request, response) 等同于调用应用程序的其他部分。这意味着,如果应用程序的另一部分(FilterSecurityInterceptor 或 method security)抛出一个 AuthenticationException 或 AccessDeniedException,它将在这里被捕获和处理。

如果用户没有被认证,或者是一个 AuthenticationException,则 开始认证。

否则,拒绝访问(Access Denied)

相关推荐
ygl615037324 分钟前
Vue3+SpringBoot3+Sa-Token+Redis+mysql8通用权限系统
java·spring boot·vue
Code哈哈笑27 分钟前
【Java 学习】构造器、static静态变量、static静态方法、static构造器、
java·开发语言·学习
是老余29 分钟前
Java三大特性:封装、继承、多态【详解】
java·开发语言
鸽鸽程序猿29 分钟前
【JavaEE】Maven的介绍及配置
java·java-ee·maven
尘浮生1 小时前
Java项目实战II基于微信小程序的南宁周边乡村游平台(开发文档+数据库+源码)
java·开发语言·数据库·spring boot·微信小程序·小程序·maven
耀耀_很无聊5 小时前
第1章 初识SpringMVC
java·spring·mvc
麻衣带我去上学5 小时前
Spring源码学习(一):Spring初始化入口
java·学习·spring
东阳马生架构5 小时前
MySQL底层概述—1.InnoDB内存结构
java·数据库·mysql
手握风云-6 小时前
数据结构(Java版)第一期:时间复杂度和空间复杂度
java·数据结构
坊钰6 小时前
【Java 数据结构】时间和空间复杂度
java·开发语言·数据结构·学习·算法