乐途乐途
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
  • 学习路径
  • 第1章 Spring Security 基础

    • 本章定位
    • Spring Security 是什么
    • DelegatingFilterProxy
    • 安全过滤器链
    • SecurityFilterChain
    • 过滤器执行顺序
  • 第2章 认证

    • 本章定位
    • Authentication
    • 认证流程
    • AuthenticationManager
    • ProviderManager
    • DaoAuthenticationProvider
    • UserDetails
    • UserDetailsService
    • PasswordEncoder
    • BCryptPasswordEncoder
    • DelegatingPasswordEncoder
    • 表单登录
    • SecurityContext
    • SecurityContextHolder
    • UsernamePasswordAuthenticationToken
  • 第3章 授权

    • 本章定位
    • 授权模型
    • GrantedAuthority
    • AccessDecisionManager
    • AccessDecisionVoter
    • URL 级别授权
    • 方法级别安全
    • @PreAuthorize
    • @PostAuthorize
    • @PreFilter
    • @PostFilter
    • @Secured
    • RoleHierarchy
  • 第4章 过滤器链

    • 本章定位
    • FilterChainProxy
    • SecurityContextHolderFilter
    • LogoutFilter
    • BasicAuthenticationFilter
    • CsrfFilter
    • CorsFilter
    • HeaderWriterFilter
    • AnonymousAuthenticationFilter
    • RequestCacheAwareFilter
    • ExceptionTranslationFilter
    • FilterSecurityInterceptor
  • 第5章 会话管理

    • 本章定位
    • 会话管理
    • SessionFixation
    • 会话并发控制
    • SessionCreationPolicy
    • RememberMe
  • 第6章 JWT

    • 本章定位
    • JWT
    • JwtDecoder
    • JWT 认证
    • JwtAuthenticationConverter
  • 第7章 OAuth2

    • 本章定位
    • OAuth2 基础
    • OAuth2 Client
    • OAuth2 Resource Server
    • 第三方登录配置
  • 第8章 攻击防护

    • 本章定位
    • CSRF 跨站请求伪造防护
    • CORS 跨域防护
    • Clickjacking 点击劫持防护
    • 安全响应头
    • Session Fixation 会话固定防护
  • 第9章 测试

    • 本章定位
    • 安全测试
    • @WithMockUser
    • 最佳实践

AnonymousAuthenticationFilter

定义与作用

AnonymousAuthenticationFilter 是 Spring Security 过滤器链中负责为未认证用户填充匿名身份的过滤器。它位于 SecurityContextHolderAwareRequestFilter 之后、ExceptionTranslationFilter 之前。当请求经过前面的认证过滤器(如 UsernamePasswordAuthenticationFilter、BasicAuthenticationFilter)后仍未建立认证信息时,AnonymousAuthenticationFilter 会检测 SecurityContextHolder 是否为空,如果为空则创建一个 AnonymousAuthenticationToken 填入,避免后续过滤器因 Authentication 为 null 而抛出 NullPointerException。


核心原理

1. AnonymousAuthenticationToken 结构

// AnonymousAuthenticationToken 构造(简化)
AnonymousAuthenticationToken token = new AnonymousAuthenticationToken(
    "anonymousUserKey",           // 唯一标识 key(防止伪造)
    "anonymousUser",              // principal(主体)
    AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS")  // 权限
);
// isAuthenticated() 返回 true(匿名身份被视为已认证的一种特殊状态)
属性默认值说明
keyanonymousUserKey(可配置)用于校验 Token 是否由本过滤器生成,防止伪造
principalanonymousUser匿名用户标识
authoritiesROLE_ANONYMOUS匿名用户默认角色
authenticatedtrue匿名身份被标记为已认证,但授权层可通过 isAnonymous() 区分

2. 在授权决策中的角色

AnonymousAuthenticationToken 虽然 isAuthenticated() == true,但授权层可以通过以下方式识别匿名用户:

// 在 AccessDecisionManager 或自定义 Voter 中
Authentication auth = SecurityContextHolder.getContext().getAuthentication();

if (auth instanceof AnonymousAuthenticationToken) {
    // 匿名用户
}

// 或在 SpEL 表达式中
// anonymous() → 匹配 AnonymousAuthenticationToken
// authenticated() → 不匹配匿名用户(因为匿名不是真正的认证)

3. 配置项

// AnonymousConfigurer 配置(简化)
http.anonymous(anonymous -> anonymous
    .principal("guest")                           // 自定义主体名
    .authorities("ROLE_GUEST", "ROLE_VISITOR")   // 自定义角色
    .key(UUID.randomUUID().toString())            // 自定义 key(增强安全性)
);

完整示例一:自定义匿名用户权限实现访客模式

场景说明

应用需要支持访客模式:未登录用户可以访问部分内容(如浏览商品列表),但不能查看价格或下订单。通过自定义匿名用户的角色,在授权规则中区分访客和已登录用户。

操作前配置(默认匿名角色,无法实现精细控制)

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests(auth -> auth
            .requestMatchers("/products/**").permitAll()  // 完全开放,无法区分访客和登录用户
            .requestMatchers("/orders/**").authenticated()
            .anyRequest().authenticated()
        )
        .formLogin(Customizer.withDefaults())
        .anonymous(Customizer.withDefaults());  // 默认 ROLE_ANONYMOUS
    return http.build();
}

问题分析:permitAll() 允许所有人(包括匿名用户),但无法区分"已登录的普通用户"和"匿名访客"。如果希望已登录用户能看到商品价格,而匿名访客只能看到商品名称,permitAll() 无法满足这种差异化授权。

操作后配置(自定义匿名角色 + 差异化授权)

@Configuration
@EnableWebSecurity
public class GuestModeConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                // 商品列表:访客和登录用户都能看
                .requestMatchers(HttpMethod.GET, "/products/list").hasAnyRole("GUEST", "USER", "ADMIN")
                // 商品价格:仅登录用户(非访客)
                .requestMatchers(HttpMethod.GET, "/products/*/price").hasAnyRole("USER", "ADMIN")
                // 下单:仅登录用户
                .requestMatchers(HttpMethod.POST, "/orders").hasAnyRole("USER", "ADMIN")
                // 管理后台:仅管理员
                .requestMatchers("/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .formLogin(Customizer.withDefaults())
            .anonymous(anonymous -> anonymous
                .principal("guest_visitor")
                .authorities("ROLE_GUEST")  // 自定义匿名角色
            );
        return http.build();
    }

    @Bean
    public UserDetailsService userDetailsService() {
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        manager.createUser(User.withDefaultPasswordEncoder()
            .username("user")
            .password("password")
            .roles("USER")
            .build());
        manager.createUser(User.withDefaultPasswordEncoder()
            .username("admin")
            .password("password")
            .roles("ADMIN")
            .build());
        return manager;
    }
}

测试验证与结果分析

用户身份请求授权检查结果
匿名访客GET /products/listhasAnyRole("GUEST", "USER", "ADMIN") → GUEST 匹配200,返回商品列表(不含价格)
匿名访客GET /products/123/pricehasAnyRole("USER", "ADMIN") → GUEST 不匹配403(或触发登录,取决于 ExceptionTranslationFilter)
已登录 userGET /products/123/pricehasAnyRole("USER", "ADMIN") → USER 匹配200,返回价格
已登录 userPOST /ordershasAnyRole("USER", "ADMIN") → USER 匹配200,允许下单
匿名访客POST /ordershasAnyRole("USER", "ADMIN") → GUEST 不匹配403
已登录 adminGET /admin/dashboardhasRole("ADMIN") → 匹配200
已登录 userGET /admin/dashboardhasRole("ADMIN") → 不匹配403

完整示例二:匿名访问与 permitAll() 的行为差异

场景说明

开发者混淆了 permitAll() 和匿名认证的关系,认为 permitAll() 意味着不需要 AnonymousAuthenticationFilter。实际上两者独立工作:permitAll() 是授权决策,允许任何人通过;AnonymousAuthenticationFilter 是身份填充,确保 SecurityContext 不为空。

操作前配置(禁用 AnonymousAuthenticationFilter,导致 NPE)

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests(auth -> auth
            .requestMatchers("/public/**").permitAll()
            .anyRequest().authenticated()
        )
        .formLogin(Customizer.withDefaults())
        .anonymous(anonymous -> anonymous.disable());  // 禁用匿名过滤器
    return http.build();
}

问题分析:禁用 AnonymousAuthenticationFilter 后,未认证用户的 SecurityContextHolder.getContext().getAuthentication() 为 null。如果应用中有代码(如自定义过滤器、AOP 切面、Controller 中)直接调用 getAuthentication().getName(),会抛出 NullPointerException。

更隐蔽的问题是:某些 Spring Security 组件期望 Authentication 始终非空。例如 ExceptionTranslationFilter 处理 AccessDeniedException 时,需要判断 isAnonymous() 来决定是返回 401 还是 403。如果没有匿名 Token,这种判断逻辑可能异常。

操作后配置(保持匿名过滤器启用,正确处理 null 场景)

@Configuration
@EnableWebSecurity
public class SafeNullHandlingConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/public/**").permitAll()
                .requestMatchers("/api/**").authenticated()
                .anyRequest().authenticated()
            )
            .formLogin(Customizer.withDefaults())
            // 保持匿名过滤器启用(默认已启用,此处显式说明)
            .anonymous(Customizer.withDefaults());
        return http.build();
    }

    // 在自定义组件中安全获取当前用户(无论是否登录)
    @Component
    public class CurrentUserResolver {
        
        public String getCurrentUsernameSafely() {
            Authentication auth = SecurityContextHolder.getContext().getAuthentication();
            
            if (auth == null) {
                // 理论上不会发生(AnonymousAuthenticationFilter 已填充)
                return "system";
            }
            
            if (auth instanceof AnonymousAuthenticationToken) {
                return "anonymous";
            }
            
            return auth.getName();
        }
        
        public boolean isAnonymousUser() {
            Authentication auth = SecurityContextHolder.getContext().getAuthentication();
            return auth instanceof AnonymousAuthenticationToken;
        }
        
        public boolean isAuthenticatedUser() {
            Authentication auth = SecurityContextHolder.getContext().getAuthentication();
            return auth != null && 
                   auth.isAuthenticated() && 
                   !(auth instanceof AnonymousAuthenticationToken);
        }
    }
}

测试验证与结果分析

代码调用禁用 AnonymousAuthenticationFilter启用 AnonymousAuthenticationFilter
getAuthentication()nullAnonymousAuthenticationToken
getAuthentication().getName()NullPointerExceptionanonymousUser
isAnonymousUser()需额外 null 检查直接判断 instanceof
ExceptionTranslationFilter 处理匿名访问可能行为异常(缺少 Token 类型判断)正常区分 401/403
permitAll() 路径认证为 null认证为 AnonymousAuthenticationToken

易错场景:过滤器顺序不当导致匿名 Token 被错误覆盖

问题现象

开发者在 AnonymousAuthenticationFilter 之后添加了一个自定义过滤器 CustomAuthFilter,试图从某种自定义 Header 中解析用户身份。但发现匿名用户访问时,即使 CustomAuthFilter 没有解析到有效身份,SecurityContextHolder 中的 AnonymousAuthenticationToken 也被清除了,后续过滤器遇到 null。

错误配置

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
        .formLogin(Customizer.withDefaults())
        .addFilterAfter(new CustomAuthFilter(), AnonymousAuthenticationFilter.class);
    return http.build();
}

public class CustomAuthFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, 
                                    HttpServletResponse response, 
                                    FilterChain chain) {
        String customToken = request.getHeader("X-Custom-Token");
        if (customToken != null) {
            // 解析并设置认证
            Authentication auth = parseToken(customToken);
            SecurityContextHolder.getContext().setAuthentication(auth);
        } else {
            // 错误:没有自定义 Token 时清空了 SecurityContext!
            SecurityContextHolder.clearContext();
        }
        chain.doFilter(request, response);
    }
}

问题分析:AnonymousAuthenticationFilter 在 CustomAuthFilter 之前执行,已经设置了 AnonymousAuthenticationToken。但 CustomAuthFilter 在没有 X-Custom-Token 时调用 SecurityContextHolder.clearContext(),将匿名 Token 清除。后续过滤器(如 ExceptionTranslationFilter)获取到 null,导致行为异常。

正确做法

自定义过滤器不应在没有匹配到凭据时盲目清除 SecurityContext,而应保留已有的认证信息(无论是表单登录后的 Session 认证还是匿名 Token):

public class CustomAuthFilter extends OncePerRequestFilter {
    
    @Override
    protected void doFilterInternal(HttpServletRequest request, 
                                    HttpServletResponse response, 
                                    FilterChain chain) throws ServletException, IOException {
        String customToken = request.getHeader("X-Custom-Token");
        
        // 仅当存在自定义 Token 且当前无认证(或匿名)时才处理
        if (customToken != null && customToken.startsWith("Custom ")) {
            Authentication existingAuth = SecurityContextHolder.getContext().getAuthentication();
            
            // 如果当前已有非匿名认证(如 Session 登录),不覆盖
            if (existingAuth == null || existingAuth instanceof AnonymousAuthenticationToken) {
                Authentication auth = parseToken(customToken);
                if (auth != null && auth.isAuthenticated()) {
                    SecurityContextHolder.getContext().setAuthentication(auth);
                }
            }
        }
        // 没有 X-Custom-Token 时:保留 SecurityContext 中的现有内容
        // 不做任何修改!
        
        chain.doFilter(request, response);
    }
    
    private Authentication parseToken(String token) {
        // 解析逻辑...
        return new UsernamePasswordAuthenticationToken(
            "customUser", null, List.of(new SimpleGrantedAuthority("ROLE_CUSTOM"))
        );
    }
}

面试考点:AnonymousAuthenticationFilter 创建的 AnonymousAuthenticationToken 的 isAuthenticated() 返回 true,为什么 authenticated() 授权表达式却不匹配匿名用户? 答:AnonymousAuthenticationToken 的 isAuthenticated() 确实返回 true,这是为了避免它被其他认证过滤器再次处理。但 Spring Security 的授权层(如 AuthenticatedVoter)会特殊识别 AnonymousAuthenticationToken 类型——authenticated() 表达式在底层检查的是 authentication.isAuthenticated() && !AnonymousAuthenticationToken.class.isAssignableFrom(authentication.getClass())。即授权层将匿名身份视为一种"技术性的已认证",但在业务语义上仍属于未登录用户。因此 anonymous() 匹配匿名用户,authenticated() 不匹配,permitAll() 匹配所有人。开发者可通过 Authentication instanceof AnonymousAuthenticationToken 在代码中精确区分。

上一页
HeaderWriterFilter
下一页
RequestCacheAwareFilter