纲要
- 核心概念
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 而言,其核心安全逻辑正是通过一组特定的安全过滤器来实现的。整个工作流程可以概括为:
- 浏览器发起 HTTP 请求。
- 请求首先被
SecurityFilterChain(安全过滤器链)拦截。 - 过滤器链中的某个过滤器检查用户是否已认证:
- 若未认证,可能返回 401 或重定向到登录页;
- 若已认证,则继续交给下一个过滤器进行授权检查。
- 授权过滤器判断当前用户是否拥有访问该资源的权限:
- 授权不足时返回 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 |
处理标准的表单登录请求,从请求参数中提取 username 和 password,并触发 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 中测试以下场景:
- 访问
/api/public:无需任何凭证,直接返回公开信息。 - 访问
/api/private(未提供凭证):自定义认证过滤器发现没有有效参数,不做认证;随后自定义授权过滤器发现未认证,返回 403。 - 访问
/api/private?username=user&password=pass:认证过滤器提取参数,匹配成功,设置认证状态;授权过滤器通过,成功返回受保护资源。 - 访问
/api/private?username=user&password=wrong:认证过滤器因密码错误不设置认证;授权过滤器因未认证返回 403。
通过控制台日志可以看到过滤器链的调用顺序:
bash
CustomAuthenticationFilter: 用户 user 认证成功
CustomAuthorizationFilter: 授权通过,继续执行
总结
本文从 Spring Security 的过滤器概念出发,分析了过滤器链如何将认证与授权解耦为职责单一的组件,梳理了内置过滤器的工作流程。通过一个可运行的项目示例,演示了自定义认证和授权过滤器如何集成到 SecurityFilterChain 中。
理解过滤器链的设计,是掌握 Spring Security 高级定制的基础,也为后续深入 OAuth2 等安全协议铺平了道路。