Java框架快速入门: Spring Security+OAuth2之过滤器与过滤器链

纲要

  • 核心概念
    • Filter(过滤器):在请求到达 Controller 之前进行拦截、读取信息、修改请求/响应或执行安全检查的组件。
    • FilterChain(过滤器链):由多个职责单一的过滤器按顺序组成的链式处理结构,实现认证、授权、登录重定向等逻辑的分离。
  • 核心流程
    • 请求到达 → 过滤器链拦截 → 认证检查(未登录则重定向至登录页或返回 401)→ 认证成功 → 授权检查(权限不足则 403)→ 放行至 Controller。
    • Spring Security 通过一系列内置过滤器协同工作,而不是在一个过滤器中完成所有任务。
  • 涉及的内置过滤器
    • BasicAuthenticationFilter:处理 HTTP Basic 认证。
    • UsernamePasswordAuthenticationFilter:处理表单登录,提取用户名密码并触发认证。
    • DefaultLoginPageGeneratingFilter:生成默认登录页。
    • LogoutPageGeneratingFilter:生成默认登出页。
    • FilterSecurityInterceptor:执行最终的授权决策,决定请求是否被允许。

Spring Security 的认证与授权机制是构建安全应用的基石,而理解其底层的 过滤器(Filter)过滤器链(FilterChain) 设计,对于深度掌握框架行为至关重要。本文将剖析过滤器的基本概念、Spring Security 如何利用过滤器链实现认证与授权,并通过可运行的示例代码演示自定义过滤器的集成。

过滤器与过滤器链概述

在 Servlet 容器中,过滤器是一种能够在请求到达目标资源(如 Controller)之前或响应返回客户端之前,对请求/响应进行拦截和处理的组件。它像一道"筛网",可以读取请求中的信息(如 Header、参数、Body),甚至可以修改请求内容或直接阻断不符合条件的请求。

对于 Spring Security 而言,其核心安全逻辑正是通过一组特定的安全过滤器来实现的。整个工作流程可以概括为:

  1. 浏览器发起 HTTP 请求。
  2. 请求首先被 SecurityFilterChain(安全过滤器链)拦截。
  3. 过滤器链中的某个过滤器检查用户是否已认证:
    • 若未认证,可能返回 401 或重定向到登录页;
    • 若已认证,则继续交给下一个过滤器进行授权检查。
  4. 授权过滤器判断当前用户是否拥有访问该资源的权限:
    • 授权不足时返回 403(禁止访问);
    • 授权通过则放行,请求最终到达 @RestController@Controller

下图展示了典型的过滤器链处理流程:
Controller 授权过滤器 认证过滤器 SecurityFilterChain 浏览器 Controller 授权过滤器 认证过滤器 SecurityFilterChain 浏览器 #mermaid-svg-aWbwK6O5z9WiXUgF{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-aWbwK6O5z9WiXUgF .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-aWbwK6O5z9WiXUgF .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-aWbwK6O5z9WiXUgF .error-icon{fill:#552222;}#mermaid-svg-aWbwK6O5z9WiXUgF .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-aWbwK6O5z9WiXUgF .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-aWbwK6O5z9WiXUgF .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-aWbwK6O5z9WiXUgF .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-aWbwK6O5z9WiXUgF .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-aWbwK6O5z9WiXUgF .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-aWbwK6O5z9WiXUgF .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-aWbwK6O5z9WiXUgF .marker{fill:#333333;stroke:#333333;}#mermaid-svg-aWbwK6O5z9WiXUgF .marker.cross{stroke:#333333;}#mermaid-svg-aWbwK6O5z9WiXUgF svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-aWbwK6O5z9WiXUgF p{margin:0;}#mermaid-svg-aWbwK6O5z9WiXUgF .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-aWbwK6O5z9WiXUgF text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-aWbwK6O5z9WiXUgF .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-aWbwK6O5z9WiXUgF .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-aWbwK6O5z9WiXUgF .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-aWbwK6O5z9WiXUgF .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-aWbwK6O5z9WiXUgF #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-aWbwK6O5z9WiXUgF .sequenceNumber{fill:white;}#mermaid-svg-aWbwK6O5z9WiXUgF #sequencenumber{fill:#333;}#mermaid-svg-aWbwK6O5z9WiXUgF #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-aWbwK6O5z9WiXUgF .messageText{fill:#333;stroke:none;}#mermaid-svg-aWbwK6O5z9WiXUgF .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-aWbwK6O5z9WiXUgF .labelText,#mermaid-svg-aWbwK6O5z9WiXUgF .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-aWbwK6O5z9WiXUgF .loopText,#mermaid-svg-aWbwK6O5z9WiXUgF .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-aWbwK6O5z9WiXUgF .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-aWbwK6O5z9WiXUgF .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-aWbwK6O5z9WiXUgF .noteText,#mermaid-svg-aWbwK6O5z9WiXUgF .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-aWbwK6O5z9WiXUgF .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-aWbwK6O5z9WiXUgF .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-aWbwK6O5z9WiXUgF .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-aWbwK6O5z9WiXUgF .actorPopupMenu{position:absolute;}#mermaid-svg-aWbwK6O5z9WiXUgF .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-aWbwK6O5z9WiXUgF .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-aWbwK6O5z9WiXUgF .actor-man circle,#mermaid-svg-aWbwK6O5z9WiXUgF line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-aWbwK6O5z9WiXUgF :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} alt权限不足授权通过 alt未认证已认证 请求资源检查用户是否认证?401 或重定向至登录页已认证,进入授权检查403 Forbidden放行请求返回资源

单一过滤器 vs. 过滤器链

如果将所有认证和授权的逻辑全部塞进一个过滤器,代码会变得臃肿且难以维护。Spring Security 采用 职责链模式,将不同的安全职责拆分为多个独立的过滤器,每个过滤器只专注于一件事情:

  • 认证过滤器:负责提取凭证(如用户名/密码),执行认证逻辑;
  • 授权过滤器:负责校验已认证用户是否有权限访问资源;
  • 登录页生成过滤器:在未认证时将请求导航到登录页面;
  • 登出过滤器:处理用户注销行为;
  • ......

这些过滤器顺序串联,形成一条链。请求依次经过每个过滤器,前一个过滤器处理完成后将请求传递给下一个。这种设计使得复杂的安全逻辑可以被清晰地组合,也便于扩展和替换。

Spring Security 内置过滤器一览

在 Spring Security 的过滤器链中,包含十几个甚至更多的内置过滤器,它们会在应用启动时自动注册。以下是一些常见的、会在日志中频繁出现的过滤器:

过滤器类 职责
BasicAuthenticationFilter 处理 HTTP Basic 认证,从请求头 Authorization 中提取 Base64 编码的用户名和密码进行验证。
UsernamePasswordAuthenticationFilter 处理标准的表单登录请求,从请求参数中提取 usernamepassword,并触发 AuthenticationManager 进行认证。
DefaultLoginPageGeneratingFilter 当未提供自定义登录页时,自动生成默认的 /login 页面(HTML 表单)。
LogoutPageGeneratingFilter 生成默认的注销确认页面。
FilterSecurityInterceptor 认证成功后的授权决策点,根据配置的权限规则决定是否允许当前请求访问,是最终的"守门员"。

在实际运行中,很多过滤器在遍历链时只是检查一下当前请求是否与自己相关,若不相关则直接调用 filterChain.doFilter() 放行;只有当请求匹配时(例如 UsernamePasswordAuthenticationFilter 只拦截 /login 的 POST 请求),它们才会真正执行核心逻辑。

项目结构

接下来通过一个可运行的 Spring Boot 项目来直观感受过滤器的行为。项目的基本结构如下:

dir 复制代码
demo-security-filter/
├── src/main/java/com/example/demo/
│   ├── DemoApplication.java
│   ├── config/SecurityConfig.java
│   ├── filter/CustomAuthenticationFilter.java
│   ├── filter/CustomAuthorizationFilter.java
│   └── controller/HelloController.java
└── pom.xml

完整代码实现

1. Maven 依赖(pom.xml)

确保引入 Spring Boot 与 Spring Security Starter:

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
         https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.0</version>
        <relativePath/>
    </parent>
    <groupId>com.example</groupId>
    <artifactId>demo-security-filter</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>demo-security-filter</name>
    <description>Spring Security Filter Chain Demo</description>
    <properties>
        <java.version>17</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

2. 启动类

java 复制代码
package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

3. 自定义认证过滤器

该过滤器模拟从请求参数中提取用户名和密码,并与预设值进行比对。若匹配则构造一个 UsernamePasswordAuthenticationToken 并放入 SecurityContextHolder,表示认证成功。

java 复制代码
package com.example.demo.filter;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;
import java.util.Collections;

public class CustomAuthenticationFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain)
            throws ServletException, IOException {

        // 从请求参数中提取用户名和密码(仅作演示,实际应从安全方式获取)
        String username = request.getParameter("username");
        String password = request.getParameter("password");

        if (username != null && password != null && "user".equals(username) && "pass".equals(password)) {
            // 创建认证对象并赋予 ROLE_USER 权限
            Authentication auth = new UsernamePasswordAuthenticationToken(
                    username,
                    null,
                    Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER"))
            );
            SecurityContextHolder.getContext().setAuthentication(auth);
            System.out.println("CustomAuthenticationFilter: 用户 " + username + " 认证成功");
        } else {
            System.out.println("CustomAuthenticationFilter: 未提供有效凭证,放行(后续过滤器将返回401或403)");
        }

        // 无论是否认证成功,都继续执行过滤器链
        filterChain.doFilter(request, response);
    }
}

4. 自定义授权过滤器

该过滤器检查当前请求的 URI 是否需要特定权限,并判断 SecurityContextHolder 中的认证对象是否持有相应角色。若权限不足,直接返回 403。

java 复制代码
package com.example.demo.filter;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

public class CustomAuthorizationFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain)
            throws ServletException, IOException {

        String requestURI = request.getRequestURI();
        // 仅对 /api/private 路径进行权限校验
        if (requestURI.startsWith("/api/private")) {
            Authentication auth = SecurityContextHolder.getContext().getAuthentication();
            if (auth == null || !auth.isAuthenticated() ||
                    auth.getAuthorities().stream().noneMatch(a -> a.getAuthority().equals("ROLE_USER"))) {
                response.sendError(HttpServletResponse.SC_FORBIDDEN, "Access Denied");
                System.out.println("CustomAuthorizationFilter: 访问 " + requestURI + " 被拒绝 (403)");
                return; // 中断过滤器链
            }
        }

        System.out.println("CustomAuthorizationFilter: 授权通过,继续执行");
        filterChain.doFilter(request, response);
    }
}

5. 安全配置类

将自定义过滤器注册到 Spring Security 的过滤器链中,并禁用 CSRF(便于演示),同时开放 /api/public 端点无需认证。

java 复制代码
package com.example.demo.config;

import com.example.demo.filter.CustomAuthenticationFilter;
import com.example.demo.filter.CustomAuthorizationFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            // 禁用 CSRF 以简化演示
            .csrf(csrf -> csrf.disable())
            // 配置 URL 访问规则
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/public").permitAll()
                .anyRequest().authenticated()
            )
            // 将自定义认证过滤器添加在 UsernamePasswordAuthenticationFilter 之前
            .addFilterBefore(new CustomAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
            // 将自定义授权过滤器添加在认证过滤器之后(这里简单放在其后即可)
            .addFilterAfter(new CustomAuthorizationFilter(), CustomAuthenticationFilter.class);

        return http.build();
    }
}

6. 测试控制器

java 复制代码
package com.example.demo.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {

    @GetMapping("/api/public")
    public String publicEndpoint() {
        return "这是一个公开端点,无需认证。";
    }

    @GetMapping("/api/private")
    public String privateEndpoint() {
        return "欢迎访问受保护资源!只有已认证且拥有 ROLE_USER 权限的用户才能看到此信息。";
    }
}

运行与测试

启动应用后,可在浏览器或 Postman 中测试以下场景:

  1. 访问 /api/public:无需任何凭证,直接返回公开信息。
  2. 访问 /api/private(未提供凭证):自定义认证过滤器发现没有有效参数,不做认证;随后自定义授权过滤器发现未认证,返回 403。
  3. 访问 /api/private?username=user&password=pass:认证过滤器提取参数,匹配成功,设置认证状态;授权过滤器通过,成功返回受保护资源。
  4. 访问 /api/private?username=user&password=wrong:认证过滤器因密码错误不设置认证;授权过滤器因未认证返回 403。

通过控制台日志可以看到过滤器链的调用顺序:

bash 复制代码
CustomAuthenticationFilter: 用户 user 认证成功
CustomAuthorizationFilter: 授权通过,继续执行

总结

本文从 Spring Security 的过滤器概念出发,分析了过滤器链如何将认证与授权解耦为职责单一的组件,梳理了内置过滤器的工作流程。通过一个可运行的项目示例,演示了自定义认证和授权过滤器如何集成到 SecurityFilterChain 中。

理解过滤器链的设计,是掌握 Spring Security 高级定制的基础,也为后续深入 OAuth2 等安全协议铺平了道路。

相关推荐
谢尔登1 小时前
01_Java基础速通
java·开发语言
前端炒粉1 小时前
DOM 常用 API 速查表
开发语言·javascript·ecmascript
lemon_sjdk2 小时前
javafx-Image图像(2)
java·javafx
小七在进步2 小时前
C++入门(2)
java·jvm·c++
To0R2 小时前
Agent 上线前必须过的五道门(附 Spring Boot 实现)
java
vipxieliang2 小时前
固定电话验证详解:区号、号码、分机号的完整验证
java·spring boot
步行cgn2 小时前
Spring Boot 主入口类上的 @Enable 和 @Scan 注解详解
java·spring boot·后端
lhldsg2 小时前
全民健身解决方案软件开发实战:从架构设计到部署指南
java·前端·数据库·小程序
两点王爷2 小时前
Java 与前端加载 MVT 数据:从服务端切片到浏览器渲染
java·前端·状态模式