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(匿名身份被视为已认证的一种特殊状态)
| 属性 | 默认值 | 说明 |
|---|---|---|
key | anonymousUserKey(可配置) | 用于校验 Token 是否由本过滤器生成,防止伪造 |
principal | anonymousUser | 匿名用户标识 |
authorities | ROLE_ANONYMOUS | 匿名用户默认角色 |
authenticated | true | 匿名身份被标记为已认证,但授权层可通过 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/list | hasAnyRole("GUEST", "USER", "ADMIN") → GUEST 匹配 | 200,返回商品列表(不含价格) |
| 匿名访客 | GET /products/123/price | hasAnyRole("USER", "ADMIN") → GUEST 不匹配 | 403(或触发登录,取决于 ExceptionTranslationFilter) |
| 已登录 user | GET /products/123/price | hasAnyRole("USER", "ADMIN") → USER 匹配 | 200,返回价格 |
| 已登录 user | POST /orders | hasAnyRole("USER", "ADMIN") → USER 匹配 | 200,允许下单 |
| 匿名访客 | POST /orders | hasAnyRole("USER", "ADMIN") → GUEST 不匹配 | 403 |
| 已登录 admin | GET /admin/dashboard | hasRole("ADMIN") → 匹配 | 200 |
| 已登录 user | GET /admin/dashboard | hasRole("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() | null | AnonymousAuthenticationToken |
getAuthentication().getName() | NullPointerException | anonymousUser |
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在代码中精确区分。