安全过滤器扩展
概述
Spring Security 的核心架构是 Filter Chain(过滤器链)。每个请求经过一系列 Filter 的过滤,进行认证、授权、CSRF 防护等处理。
自定义 Filter 是扩展 Spring Security 最灵活的方式,可以插入任何自定义的安全逻辑。
Filter 接口对比
| 接口 | 说明 |
|---|---|
jakarta.servlet.Filter | Servlet 标准接口,每次请求都调用 |
OncePerRequestFilter | Spring 提供,确保每个请求只执行一次(推荐) |
GenericFilterBean | Spring 提供,支持 init-param 注入 |
AbstractAuthenticationProcessingFilter | 认证过滤器基类 |
一、OncePerRequestFilter(推荐)
java
@Component
public class RequestTimingFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws IOException, ServletException {
long start = System.currentTimeMillis();
try {
chain.doFilter(request, response);
} finally {
long elapsed = System.currentTimeMillis() - start;
if (elapsed > 1000) {
log.warn("Slow request: {} {} took {}ms",
request.getMethod(), request.getRequestURI(), elapsed);
}
}
}
}与普通 Filter 的区别:
- 确保在同一个请求的多次 forward 中只执行一次
- 支持
shouldNotFilter()方法控制过滤范围
java
@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
String path = request.getRequestURI();
return path.startsWith("/public/") || path.startsWith("/static/");
}二、GenericFilterBean
java
@Component
public class EnvironmentFilter extends GenericFilterBean {
private String allowedEnvironment;
@Override
public void initFilterBean() {
// 从 filterConfig 或 Spring 环境中获取参数
allowedEnvironment = getFilterConfig()
.orElse(getEnvironment())
.map(env -> env.getProperty("app.allowed.env"))
.orElse("production");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
String env = request.getParameter("env");
if (env != null && !env.equals(allowedEnvironment)) {
((HttpServletResponse) response).sendError(403, "Environment not allowed");
return;
}
chain.doFilter(request, response);
}
}三、Filter 排序
3.1 使用 @Order
java
@Order(1)
@Component
public class FirstFilter extends OncePerRequestFilter {
// 最先执行
}
@Order(2)
@Component
public class SecondFilter extends OncePerRequestFilter {
// 第二个执行
}3.2 Spring Security Filter 顺序
Spring Security 内置 Filter 在链中的顺序(数字越小越靠前):
| 顺序 | Filter | 职责 |
|---|---|---|
-100 | ChannelProcessingFilter | HTTPS 重定向 |
-50 | SecurityContextPersistenceFilter | 从 Session 恢复 SecurityContext |
0 | LogoutFilter | 登出处理 |
50 | UsernamePasswordAuthenticationFilter | 表单登录 |
100 | BasicAuthenticationFilter | HTTP Basic 认证 |
150 | RequestCacheAwareFilter | 登录后重定向 |
200 | AnonymousAuthenticationFilter | 匿名用户令牌 |
300 | SessionManagementFilter | Session 固定攻击防护 |
400 | ExceptionTranslationFilter | 异常转换 |
500 | FilterSecurityInterceptor | 授权决策 |
3.3 在 HttpSecurity 中插入 Filter
java
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.addFilterBefore(new CustomFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilterAfter(new PostLoginFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilterAt(new ReplaceFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}3 种插入方式:
| 方法 | 效果 |
|---|---|
addFilterBefore(filter, class) | 在指定 Filter 之前执行 |
addFilterAfter(filter, class) | 在指定 Filter 之后执行 |
addFilterAt(filter, class) | 在指定 Filter 的位置执行(不替换,原来的也在) |
四、SecurityFilterChain 多链
java
@Configuration
@EnableWebSecurity
public class MultiChainConfig {
// 链 1:API 安全配置
@Order(1)
@Bean
public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/api/**") // 仅匹配 /api/**
.authorizeHttpRequests(auth -> auth
.anyRequest().authenticated()
)
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
return http.build();
}
// 链 2:管理后台配置
@Order(2)
@Bean
public SecurityFilterChain adminFilterChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/admin/**") // 仅匹配 /admin/**
.authorizeHttpRequests(auth -> auth
.requestMatchers("/admin/login").permitAll()
.anyRequest().hasRole("ADMIN")
)
.formLogin(Customizer.withDefaults());
return http.build();
}
// 链 3:公共资源配置(默认)
@Bean
public SecurityFilterChain publicFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**", "/static/**").permitAll()
.anyRequest().authenticated()
)
.formLogin(Customizer.withDefaults());
return http.build();
}
}五、实战:API 签名认证 Filter
5.1 场景
移动端 App 调用 API 时,使用 AppId + Secret 进行签名认证,防止请求被篡改。
5.2 签名规则
请求头:
X-App-Id: app_123456
X-Timestamp: 1696924800
X-Nonce: random_string
X-Signature: SHA256(AppId + Timestamp + Nonce + URI + Body + Secret)
服务端验证:
1. 检查 Timestamp 是否在 5 分钟内(防重放)
2. 检查 Nonce 是否已使用(幂等)
3. 计算签名并与 X-Signature 比对5.3 代码实现
java
@Component
public class ApiSignatureFilter extends OncePerRequestFilter {
@Autowired
private AppSecretService appSecretService;
@Autowired
private NonceService nonceService;
private static final long MAX_CLOCK_SKEW_MS = 5 * 60 * 1000; // 5 分钟
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws IOException, ServletException {
String appId = request.getHeader("X-App-Id");
String timestamp = request.getHeader("X-Timestamp");
String nonce = request.getHeader("X-Nonce");
String signature = request.getHeader("X-Signature");
// 检查必填参数
if (appId == null || timestamp == null || nonce == null || signature == null) {
sendError(response, 400, "Missing signature headers");
return;
}
// 1. 时间戳检查(防重放)
long ts = Long.parseLong(timestamp);
if (Math.abs(System.currentTimeMillis() - ts) > MAX_CLOCK_SKEW_MS) {
sendError(response, 401, "Request expired");
return;
}
// 2. Nonce 检查(防重放)
if (!nonceService.checkAndMarkUsed(nonce, ts)) {
sendError(response, 401, "Nonce already used");
return;
}
// 3. 获取 App Secret
String secret = appSecretService.getSecretByAppId(appId);
if (secret == null) {
sendError(response, 401, "Invalid AppId");
return;
}
// 4. 计算签名
String body = getRequestBody(request);
String signContent = appId + timestamp + nonce + request.getRequestURI() + body + secret;
String expectedSign = SHA256Util.hash(signContent);
// 5. 比对签名
if (!expectedSign.equals(signature)) {
sendError(response, 401, "Invalid signature");
return;
}
// 6. 认证通过,将 AppId 注入 SecurityContext
UsernamePasswordAuthenticationToken auth =
UsernamePasswordAuthenticationToken.authenticated(
new ApiClient(appId), null,
AuthorityUtils.createAuthorityList("ROLE_API_CLIENT")
);
SecurityContextHolder.getContext().setAuthentication(auth);
chain.doFilter(request, response);
}
@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
String path = request.getRequestURI();
return path.startsWith("/public/") || path.startsWith("/auth/");
}
private String getRequestBody(HttpServletRequest request) {
try {
return request.getReader().lines().collect(Collectors.joining());
} catch (IOException e) {
return "";
}
}
private void sendError(HttpServletResponse response, int status, String msg) throws IOException {
response.setStatus(status);
response.setContentType("application/json");
response.getWriter().write("{\"error\":\"" + msg + "\"}");
}
}5.4 注册
java
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
private ApiSignatureFilter apiSignatureFilter;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// API 签名验证放在 UsernamePasswordAuthenticationFilter 之前
.addFilterBefore(apiSignatureFilter, UsernamePasswordAuthenticationFilter.class)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/**").hasRole("API_CLIENT")
.anyRequest().authenticated()
);
return http.build();
}
}六、总结
| 知识点 | 说明 |
|---|---|
OncePerRequestFilter | 确保每个请求只执行一次,推荐用于自定义 Filter |
GenericFilterBean | 支持 Servlet init-param 注入 |
| Filter 排序 | @Order 注解或 addFilterBefore/After/At |
| 多链 | 多个 SecurityFilterChain Bean,通过 @Order 和 securityMatcher 匹配 |
| 签名认证 | AppId + Timestamp + Nonce + Signature 四要素 |
| 防重放 | Timestamp 窗口 + Nonce 幂等检查 |
参考链接: