FilterSecurityInterceptor
定义与作用
FilterSecurityInterceptor 是 Spring Security 5.x 中负责 URL 级别授权决策 的核心过滤器。它位于整个安全过滤器链的末尾,是请求到达 Controller 之前的最后一道安全关卡。在 Spring Security 6.x 中,它的职责被 AuthorizationFilter 替代,但 5.x 的绝大多数生产系统仍依赖它执行最终的访问控制。
它的核心职责可概括为:
- 拦截请求:作为过滤器链的最后一个安全过滤器,拦截即将进入 Servlet 目标资源的请求。
- 获取权限配置:通过
SecurityMetadataSource获取当前请求资源所需的ConfigAttribute(如ROLE_ADMIN)。 - 获取当前身份:从
SecurityContextHolder中取出当前Authentication(包含用户身份和已有权限)。 - 委托决策:将上述信息交给
AccessDecisionManager,由其内部的AccessDecisionVoter投票决定是否放行。 - 执行决策:如果
AccessDecisionManager决定通过,则放行请求到目标资源;如果拒绝,则抛出AccessDeniedException,由上游的ExceptionTranslationFilter捕获并转译为 403 Forbidden。
位置说明:在 5.x 的过滤器链中,
FilterSecurityInterceptor位于ExceptionTranslationFilter之后。ExceptionTranslationFilter在FilterSecurityInterceptor的上游,专门捕获它抛出的AuthenticationException和AccessDeniedException。
核心原理
1. 工作流程
FilterSecurityInterceptor 的完整授权决策流程如下:
步骤拆解:
| 步骤 | 执行组件 | 说明 |
|---|---|---|
| ① | FilterSecurityInterceptor | 请求到达,准备执行授权检查 |
| ② | SecurityMetadataSource | 根据请求 URL 查找对应的 ConfigAttribute 列表(如 ROLE_ADMIN) |
| ③ | SecurityContextHolder | 获取当前线程中的 Authentication(可能来自 Session、JWT、Basic 等) |
| ④ | AccessDecisionManager | 调用 decide(authentication, filterInvocation, configAttributes),内部遍历 AccessDecisionVoter 进行投票 |
| ⑤ | 目标资源 | 决策通过,请求继续向下到达 Controller / Servlet |
| ⑥ | ExceptionTranslationFilter | 如果决策拒绝,AccessDecisionManager 或 FilterSecurityInterceptor 抛出 AccessDeniedException,由上游的 ExceptionTranslationFilter 捕获 |
2. 与 AccessDecisionManager 的协作
FilterSecurityInterceptor 本身并不直接判断"用户是否有权限",而是将判断委托给 AccessDecisionManager。FilterSecurityInterceptor 通过 AbstractSecurityInterceptor 的模板方法实现这一协作:
// 简化逻辑
protected InterceptorStatusToken beforeInvocation(Object object) {
// 1. 获取资源所需权限
Collection<ConfigAttribute> attributes =
this.obtainSecurityMetadataSource().getAttributes(object);
// 2. 获取当前认证信息
Authentication authentication =
SecurityContextHolder.getContext().getAuthentication();
// 3. 委托 AccessDecisionManager 决策
this.accessDecisionManager.decide(authentication, object, attributes);
// 4. 返回 token(用于 afterInvocation,URL 级别通常不处理)
return new InterceptorStatusToken(authentication, true, attributes, object);
}
AccessDecisionManager 的三种实现策略:
| 实现类 | 决策规则 | 适用场景 |
|---|---|---|
AffirmativeBased | 一票通过(只要有任意一个 Voter 投赞成票) | 默认策略,最常用 |
ConsensusBased | 多数通过(赞成票 > 反对票) | 需要多维度权衡 |
UnanimousBased | 一致通过(所有 Voter 必须赞成) | 严格安全场景 |
3. 与 ExceptionTranslationFilter 的交互
FilterSecurityInterceptor 与 ExceptionTranslationFilter 的交互时序如下:
关键点:
ExceptionTranslationFilter在FilterSecurityInterceptor的上游,通过try-catch包裹chain.doFilter()捕获所有下游异常。- 如果当前用户是匿名用户(
AnonymousAuthenticationToken),ExceptionTranslationFilter会将AccessDeniedException视为未认证处理,调用AuthenticationEntryPoint(返回 401 或重定向登录页),而不是直接返回 403。 - 如果当前用户已认证(非匿名),则直接调用
AccessDeniedHandler返回 403。
完整示例一:从默认授权到自定义 AccessDecisionManager
场景说明
某电商后台管理系统需要记录所有授权拒绝的日志(用于审计和风控)。默认的 AffirmativeBased 在拒绝时直接抛出 AccessDeniedException,没有任何日志痕迹。我们需要在不改变最终返回行为(仍返回 403)的前提下,在拒绝前打印日志。
操作前配置(默认行为)
@Configuration
@EnableWebSecurity
public class AdminSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers("/orders/**").hasAnyRole("ADMIN", "OPERATOR")
.antMatchers("/public/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.httpBasic();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("admin").password("{noop}admin").roles("ADMIN")
.and()
.withUser("operator").password("{noop}operator").roles("OPERATOR")
.and()
.withUser("user").password("{noop}user").roles("USER");
}
}
问题分析:默认配置下,Spring Security 会自动创建 AffirmativeBased 作为 AccessDecisionManager。当 FilterSecurityInterceptor 调用 decide() 时,如果 RoleVoter 投反对票,直接抛出 AccessDeniedException。虽然请求最终返回 403,但控制台没有任何记录,无法满足审计需求。
操作后配置(自定义 AccessDecisionManager)
自定义 AccessDecisionManager,在委托给默认实现之前/之后插入日志逻辑:
@Component
public class LoggingAccessDecisionManager implements AccessDecisionManager {
private final AccessDecisionManager delegate;
public LoggingAccessDecisionManager() {
List<AccessDecisionVoter<?>> voters = Arrays.asList(
new RoleVoter(),
new AuthenticatedVoter()
);
this.delegate = new AffirmativeBased(voters);
}
@Override
public void decide(Authentication authentication, Object object,
Collection<ConfigAttribute> configAttributes)
throws AccessDeniedException {
try {
// 委托给默认实现做决策
delegate.decide(authentication, object, configAttributes);
} catch (AccessDeniedException e) {
// 记录授权拒绝日志
String required = configAttributes.stream()
.map(ConfigAttribute::getAttribute)
.collect(Collectors.joining(", "));
System.out.println("[授权拒绝] 用户=" + authentication.getName()
+ ", 请求=" + object
+ ", 所需权限=[" + required + "]"
+ ", 当前权限=" + authentication.getAuthorities());
throw e;
}
}
@Override
public boolean supports(ConfigAttribute attribute) {
return delegate.supports(attribute);
}
@Override
public boolean supports(Class<?> clazz) {
return FilterInvocation.class.isAssignableFrom(clazz);
}
}
将自定义的 AccessDecisionManager 接入 HttpSecurity:
@Configuration
@EnableWebSecurity
public class AdminSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private LoggingAccessDecisionManager loggingAccessDecisionManager;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.accessDecisionManager(loggingAccessDecisionManager) // 接入自定义决策器
.antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers("/orders/**").hasAnyRole("ADMIN", "OPERATOR")
.antMatchers("/public/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.httpBasic();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("admin").password("{noop}admin").roles("ADMIN")
.and()
.withUser("operator").password("{noop}operator").roles("OPERATOR")
.and()
.withUser("user").password("{noop}user").roles("USER");
}
}
结果分析
启动应用后,分别用不同用户访问 /admin/dashboard:
| 用户 | 请求 | 控制台输出 | 响应 |
|---|---|---|---|
| user (ROLE_USER) | GET /admin/dashboard | [授权拒绝] 用户=user, 请求=FilterInvocation: .../admin/dashboard, 所需权限=[ROLE_ADMIN], 当前权限=[ROLE_USER] | 403 Forbidden |
| operator (ROLE_OPERATOR) | GET /admin/dashboard | [授权拒绝] 用户=operator, 请求=.../admin/dashboard, 所需权限=[ROLE_ADMIN], 当前权限=[ROLE_OPERATOR] | 403 Forbidden |
| admin (ROLE_ADMIN) | GET /admin/dashboard | 无日志(投票通过) | 200 OK |
关键点:
FilterSecurityInterceptor的调用链保持不变,只是在AccessDecisionManager层插入了日志切面。最终行为仍然是AccessDeniedException→ExceptionTranslationFilter→ 403,Spring Security 的异常处理链路没有被破坏。
完整示例二:动态权限加载(FilterInvocationSecurityMetadataSource)与 FilterSecurityInterceptor 配合
场景说明
某 SaaS 平台的管理后台权限需要从数据库动态加载,运营人员可以在后台调整某个 URL 所需的权限,无需重启应用。默认配置下,URL 授权规则在 HttpSecurity 中硬编码,任何修改都需要重新打包部署。
操作前配置(URL 规则硬编码)
@Configuration
@EnableWebSecurity
public class SaaSBackendConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/admin/**").hasRole("ADMIN") // 硬编码
.antMatchers("/finance/**").hasRole("FINANCE") // 硬编码
.antMatchers("/public/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("admin").password("{noop}admin").roles("ADMIN", "FINANCE")
.and()
.withUser("user").password("{noop}user").roles("USER");
}
}
问题分析:antMatchers("/admin/**").hasRole("ADMIN") 将规则写死在 Java 代码中。如果业务需要临时将 /finance/report 开放给 ADMIN 角色查看,必须修改代码、重新编译、重新部署。
操作后配置(自定义 FilterInvocationSecurityMetadataSource)
实现 FilterInvocationSecurityMetadataSource,从数据库(此处用内存模拟)读取 URL 与角色的映射:
@Service
public class DbPermissionService {
// 模拟数据库表:url → 所需角色列表
private static final Map<String, List<String>> PERMISSION_DB = new ConcurrentHashMap<>();
public DbPermissionService() {
// 初始化数据
PERMISSION_DB.put("/admin/dashboard", Arrays.asList("ADMIN"));
PERMISSION_DB.put("/admin/users", Arrays.asList("ADMIN"));
PERMISSION_DB.put("/finance/report", Arrays.asList("FINANCE"));
}
public List<String> findRolesByUrl(String url) {
// 精确匹配演示(生产环境可扩展为 Ant 模式匹配)
return PERMISSION_DB.getOrDefault(url, Collections.emptyList());
}
public void updateRoles(String url, List<String> roles) {
PERMISSION_DB.put(url, roles);
}
}
@Component
public class DynamicSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
@Autowired
private DbPermissionService permissionService;
@Override
public Collection<ConfigAttribute> getAttributes(Object object)
throws IllegalArgumentException {
FilterInvocation fi = (FilterInvocation) object;
String url = fi.getRequestUrl();
// 从数据库查询该 URL 所需的角色
List<String> roles = permissionService.findRolesByUrl(url);
if (roles.isEmpty()) {
// 无特殊配置,返回 ROLE_ANONYMOUS(表示走默认规则)
return SecurityConfig.createList("ROLE_ANONYMOUS");
}
String[] roleArray = roles.stream()
.map(r -> "ROLE_" + r)
.toArray(String[]::new);
return SecurityConfig.createList(roleArray);
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
@Override
public boolean supports(Class<?> clazz) {
return FilterInvocation.class.isAssignableFrom(clazz);
}
}
将自定义 SecurityMetadataSource 注入 FilterSecurityInterceptor:
@Configuration
@EnableWebSecurity
public class SaaSBackendConfig extends WebSecurityConfigurerAdapter {
@Autowired
private DynamicSecurityMetadataSource securityMetadataSource;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O object) {
// 替换默认的 SecurityMetadataSource
object.setSecurityMetadataSource(securityMetadataSource);
return object;
}
})
.antMatchers("/public/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("admin").password("{noop}admin").roles("ADMIN", "FINANCE")
.and()
.withUser("user").password("{noop}user").roles("USER");
}
}
结果分析
| 操作 | 请求 | 数据库权限 | 结果 |
|---|---|---|---|
| 初始状态 | admin 访问 /finance/report | ["FINANCE"] | 200 OK(admin 拥有 FINANCE) |
| 初始状态 | user 访问 /finance/report | ["FINANCE"] | 403 Forbidden(user 无 FINANCE) |
| 动态修改后 | admin 访问 /finance/report | 调用 updateRoles("/finance/report", ["ADMIN", "FINANCE"]) | 200 OK |
| 动态修改后 | user 访问 /finance/report | ["ADMIN", "FINANCE"] | 403 Forbidden(user 无对应角色) |
动态修改演示:
@RestController
public class PermissionAdminController {
@Autowired
private DbPermissionService permissionService;
@PostMapping("/admin/permission/update")
public String updatePermission(@RequestParam String url,
@RequestParam List<String> roles) {
permissionService.updateRoles(url, roles);
return "权限已更新,下次访问生效";
}
}
关键点:
FilterSecurityInterceptor每次请求都会调用SecurityMetadataSource.getAttributes()。因此只要数据源返回的内容发生变化,无需重启应用,下一次请求就会自动获取新的权限配置。这与HttpSecurity硬编码方案形成了鲜明对比。
易错场景与面试考点
易错一:AccessDeniedException 被自定义全局异常处理器捕获
问题现象:开发者在项目中定义了 @ControllerAdvice 或 HandlerExceptionResolver,并捕获了 AccessDeniedException:
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(AccessDeniedException.class)
public ResponseEntity<String> handleAccessDenied(AccessDeniedException e) {
return ResponseEntity.status(404).body("资源不存在");
}
}
后果:FilterSecurityInterceptor 在过滤器链中抛出的 AccessDeniedException,正常情况下应由上游的 ExceptionTranslationFilter 捕获,调用 AccessDeniedHandler 返回 403。但如果该异常由于某种原因(例如通过 Spring Boot 的 Error Dispatch 机制或自定义 FilterRegistrationBean 的 DispatcherType.ERROR 配置)进入了 DispatcherServlet,就可能被 GlobalExceptionHandler 捕获,返回 404 或 500,而不是预期的 403。
正确做法:
- 不要在全局异常处理器中捕获
AccessDeniedException(除非你明确知道自己在做什么)。 - 如果需要自定义 403 响应,应通过
ExceptionTranslationFilter配置AccessDeniedHandler:
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.exceptionHandling()
.accessDeniedHandler((request, response, accessDeniedException) -> {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":403,\"message\":\"权限不足\"}");
})
.and()
.authorizeRequests()
.anyRequest().authenticated();
}
- 如果确实需要全局异常处理器处理某些特定的
AccessDeniedException(如方法级别@PreAuthorize抛出的异常),应在GlobalExceptionHandler中透传FilterSecurityInterceptor产生的 URL 级别异常,或明确区分异常来源。
易错二:自定义 FilterSecurityInterceptor 时位置放错
问题现象:开发者使用 http.addFilterAt() 或 http.addFilterBefore() 添加自定义的 FilterSecurityInterceptor,但将其放置在过滤器链中间位置,而不是末尾:
// 错误配置
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.addFilterAfter(new CustomFilterSecurityInterceptor(),
UsernamePasswordAuthenticationFilter.class) // 位置错误!
.authorizeRequests()
.anyRequest().authenticated();
}
后果:FilterSecurityInterceptor 之后还有其他过滤器(如 SwitchUserFilter、自定义的审计过滤器、日志过滤器等)。如果这些过滤器中的操作涉及敏感资源,它们将在授权检查之前执行,导致攻击者可以绕过授权。
正确做法:
在 5.x 中,FilterSecurityInterceptor 应该是过滤器链中最后一个执行安全操作的过滤器。如果需要自定义,应使用 withObjectPostProcessor 修改内置实例,或确保替换后的位置仍在末尾:
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O object) {
// 安全地修改内置实例的属性
object.setSecurityMetadataSource(customMetadataSource);
return object;
}
})
.anyRequest().authenticated();
}
面试考点
Q1:FilterSecurityInterceptor 和 AuthorizationFilter(6.x)有什么区别?
| 维度 | FilterSecurityInterceptor (5.x) | AuthorizationFilter (6.x) |
|---|---|---|
| 决策机制 | 基于 AccessDecisionManager + AccessDecisionVoter 投票 | 基于 AuthorizationManager + AuthorizationDecision |
| 接口风格 | 继承 AbstractSecurityInterceptor,实现 Filter | 独立的 Filter 实现,职责更单一 |
| 异常处理 | 抛出 AccessDeniedException 由 ExceptionTranslationFilter 捕获 | 同样抛出异常,但 AuthorizationManager 的 check() 方法返回 AuthorizationDecision |
| 元数据来源 | SecurityMetadataSource(FilterInvocationSecurityMetadataSource) | RequestMatcherDelegatingAuthorizationManager 将 URL 映射到授权规则 |
| 配置方式 | HttpSecurity.authorizeRequests()(链式配置) | HttpSecurity.authorizeHttpRequests()(Lambda DSL) |
| 扩展方式 | 自定义 AccessDecisionManager、SecurityMetadataSource | 自定义 AuthorizationManager |
Q2:FilterSecurityInterceptor 的 invoke 方法中,beforeInvocation 和 afterInvocation 分别做什么?
FilterSecurityInterceptor 的 invoke 方法继承自 AbstractSecurityInterceptor,其逻辑如下:
public void invoke(FilterInvocation fi) throws IOException, ServletException {
// 1. beforeInvocation:执行授权决策
InterceptorStatusToken token = super.beforeInvocation(fi);
try {
// 2. 执行目标 FilterChain(请求继续到达 Controller)
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
} finally {
// 3. finallyInvocation:清理资源(如 RunAsManager 的临时身份)
super.finallyInvocation(token);
}
// 4. afterInvocation:在请求完成后执行
super.afterInvocation(token, null);
}
beforeInvocation:- 调用
SecurityMetadataSource.getAttributes()获取ConfigAttribute列表。 - 调用
AccessDecisionManager.decide()进行授权投票。 - 如果认证失败或未认证,抛出
AccessDeniedException或AuthenticationException。 - 如果检查通过,返回
InterceptorStatusToken,用于后续的afterInvocation。
- 调用
afterInvocation:- 在
FilterChain执行完成后调用。 - 在 URL 级别 的
FilterSecurityInterceptor中,afterInvocation通常不做任何事(URL 级别没有返回值可以过滤)。 - 在 方法级别 的
MethodSecurityInterceptor中,afterInvocation会调用AfterInvocationProvider(如@PostFilter的权限过滤逻辑),对方法的返回值进行后处理。
- 在
因此,
beforeInvocation是"准入控制",afterInvocation是"后处理控制"。对于FilterSecurityInterceptor,beforeInvocation是核心,afterInvocation在标准用法中基本为空操作。