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

    • 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
    • 最佳实践

BasicAuthenticationFilter

定义与作用

BasicAuthenticationFilter 是 Spring Security 过滤器链中处理 HTTP Basic 认证 的核心过滤器。它位于 UsernamePasswordAuthenticationFilter 之后、RequestCacheAwareFilter 之前,负责从请求头的 Authorization: Basic <base64(username:password)> 中提取凭据,构造 UsernamePasswordAuthenticationToken,并委托给 AuthenticationManager 进行认证。认证成功后,将 Authentication 存入 SecurityContext,供后续过滤器链使用。


核心原理

1. Authorization Header 解析

HTTP Basic 认证使用标准格式:

Authorization: Basic <base64(username:password)>

BasicAuthenticationFilter 的解析逻辑:

// BasicAuthenticationFilter 核心逻辑(简化)
protected void doFilterInternal(HttpServletRequest request, 
                                HttpServletResponse response, 
                                FilterChain chain) {
    String header = request.getHeader("Authorization");
    
    if (header == null || !header.startsWith("Basic ")) {
        chain.doFilter(request, response);  // 无 Basic Header,放行
        return;
    }
    
    try {
        // 1. Base64 解码
        String base64Token = header.substring(6);
        String token = new String(Base64.getDecoder().decode(base64Token), 
                                  StandardCharsets.UTF_8);
        
        // 2. 分割 username:password(注意:password 中不能包含冒号,或需特殊处理)
        int delim = token.indexOf(':');
        String username = token.substring(0, delim);
        String password = token.substring(delim + 1);
        
        // 3. 构造未认证的 Token
        UsernamePasswordAuthenticationToken authRequest =
            new UsernamePasswordAuthenticationToken(username, password);
        authRequest.setDetails(
            authenticationDetailsSource.buildDetails(request)
        );
        
        // 4. 调用 AuthenticationManager 认证
        Authentication authResult = 
            this.getAuthenticationManager().authenticate(authRequest);
        
        // 5. 存入 SecurityContext
        SecurityContextHolder.getContext().setAuthentication(authResult);
        
    } catch (AuthenticationException ex) {
        // 6. 认证失败,调用 AuthenticationEntryPoint
        SecurityContextHolder.clearContext();
        this.authenticationEntryPoint.commence(request, response, ex);
        return;
    }
    
    chain.doFilter(request, response);
}

2. 与表单登录的区别


完整示例一:保护 REST API 端点

场景说明

为内部管理 API 启用 HTTP Basic 认证,供命令行工具(curl、Postman)和内部微服务调用。所有 /api/admin/** 请求必须携带 Basic 凭据。

操作前配置(无认证,API 暴露)

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests(auth -> auth
            .requestMatchers("/api/admin/**").permitAll()
            .anyRequest().authenticated()
        )
        .formLogin(Customizer.withDefaults());
    return http.build();
}

问题分析:/api/admin/** 使用 permitAll(),无需任何认证即可访问,导致管理接口暴露给任意客户端,存在严重安全风险。

操作后配置(启用 Basic 认证)

@Configuration
@EnableWebSecurity
public class BasicAuthApiConfig {

    @Bean
    @Order(1)
    public SecurityFilterChain adminApiSecurity(HttpSecurity http) throws Exception {
        http
            .securityMatcher("/api/admin/**")
            .authorizeHttpRequests(auth -> auth
                .anyRequest().hasRole("ADMIN")
            )
            .httpBasic(Customizer.withDefaults())  // 启用 BasicAuthenticationFilter
            .sessionManagement(session -> 
                session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            );  // 无状态,不创建 Session
        return http.build();
    }

    @Bean
    @Order(2)
    public SecurityFilterChain defaultSecurity(HttpSecurity http) throws Exception {
        http
            .securityMatcher("/**")
            .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
            .formLogin(Customizer.withDefaults())
            .sessionManagement(session -> 
                session.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
            );
        return http.build();
    }

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

测试验证与结果分析

请求命令/Header响应原因
GET /api/admin/stats无 Authorization401 UnauthorizedBasicAuthenticationFilter 未检测到 Header,由 AuthenticationEntryPoint 返回 401
GET /api/admin/statsAuthorization: Basic YWRtaW46YWRtaW4xMjM=(admin:admin123)200 OK认证成功,SecurityContext 中存入 ROLE_ADMIN
GET /api/admin/statsAuthorization: Basic dXNlcjp1c2VyMTIz(user:user123)403 Forbidden认证成功但 hasRole("ADMIN") 拒绝,由 ExceptionTranslationFilter 转译为 403
GET /api/admin/statsAuthorization: Basic YWRtaW46d3JvbmdwYXNz401 Unauthorized密码错误,DaoAuthenticationProvider 抛出 BadCredentialsException
GET /web/home无 Authorization(通过 Cookie Session)302 到 /loginWeb 链使用表单登录,无 Session 则重定向
# curl 测试命令
curl -u admin:admin123 http://localhost:8080/api/admin/stats
# -u 参数自动构造 Authorization: Basic 头

curl -u user:user123 http://localhost:8080/api/admin/stats
# 返回 403(认证成功,但权限不足)

完整示例二:自定义 AuthenticationEntryPoint(返回 401 JSON)

场景说明

API 客户端(如前端 SPA 或第三方服务)调用 Basic 认证接口时,如果未携带凭据或凭据错误,默认返回 401 和 WWW-Authenticate: Basic realm="..." Header。部分客户端(如前端 Fetch API)处理此响应不便,需要返回 JSON 格式的 401 错误。

操作前配置(默认响应)

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .securityMatcher("/api/**")
        .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
        .httpBasic(Customizer.withDefaults());  // 默认 BasicAuthenticationEntryPoint
    return http.build();
}

问题分析:默认 BasicAuthenticationEntryPoint 返回 401 + WWW-Authenticate: Basic realm="Realm" 响应头。这是 HTTP 标准行为,浏览器遇到此响应会弹出内置的 Basic 登录对话框。但在前后端分离场景中,前端不希望浏览器弹出对话框,而是希望自己处理 401 并显示自定义登录界面。

操作后配置(自定义 JSON 响应,不发送 WWW-Authenticate)

@Configuration
@EnableWebSecurity
public class CustomBasicAuthConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .securityMatcher("/api/**")
            .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
            .httpBasic(basic -> basic
                .authenticationEntryPoint(new CustomBasicAuthenticationEntryPoint())
            )
            .sessionManagement(session -> 
                session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            );
        return http.build();
    }

    // 自定义 EntryPoint:返回 JSON,不发送 WWW-Authenticate(避免浏览器弹窗)
    public static class CustomBasicAuthenticationEntryPoint 
            implements AuthenticationEntryPoint {
        
        @Override
        public void commence(HttpServletRequest request, 
                            HttpServletResponse response,
                            AuthenticationException authException) 
                            throws IOException {
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            response.setContentType("application/json;charset=UTF-8");
            // 不添加 WWW-Authenticate 头,避免浏览器弹窗
            response.getWriter().write(
                "{\"code\":401,\"message\":\"缺少有效凭据,请提供 Basic Authentication\"}"
            );
        }
    }

    @Bean
    public UserDetailsService userDetailsService() {
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        manager.createUser(User.withDefaultPasswordEncoder()
            .username("apiuser")
            .password("apipass")
            .roles("API_USER")
            .build());
        return manager;
    }
}

前端配合代码

// 前端 Fetch API 调用(无浏览器弹窗)
async function callApi() {
    const credentials = btoa('apiuser:apipass');
    const response = await fetch('/api/data', {
        headers: {
            'Authorization': `Basic ${credentials}`
        }
    });
    
    if (response.status === 401) {
        const error = await response.json();
        console.log(error.message);  // "缺少有效凭据,请提供 Basic Authentication"
        // 前端显示自定义登录界面,而不是浏览器弹窗
    }
}

结果分析

请求响应头响应体浏览器行为
无 Authorization 调用 /api/data无 WWW-Authenticate401 JSON不弹窗,前端可处理
错误凭据调用 /api/data无 WWW-Authenticate401 JSON不弹窗,前端可处理
正确凭据调用 /api/data正常业务响应头200 业务数据正常处理

易错场景:BasicAuthenticationFilter 与 Session 的意外交互

问题现象

开发者配置了 httpBasic() 和 STATELESS 策略,期望完全无状态。但发现某些请求后,服务器仍然会创建 JSESSIONID Cookie,或者 BasicAuthenticationFilter 在认证成功后,后续的 SecurityContextPersistenceFilter 将 SecurityContext 写入了 Session。

错误配置

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .securityMatcher("/api/**")
        .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
        .httpBasic(Customizer.withDefaults())
        .sessionManagement(session -> 
            session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        );
    return http.build();
}

表面上看起来正确,但以下情况仍会产生 Session:

  1. 其他过滤器创建 Session:例如 CsrfFilter 使用 HttpSessionCsrfTokenRepository(默认),访问页面时会创建 Session。
  2. Servlet 容器自动创建 Session:如果应用的其他部分(如 JSP 页面)触发了 Session 创建。
  3. SecurityContextPersistenceFilter 配置了 Session Repository:即使 STATELESS,SecurityContextPersistenceFilter 的 loadContext() 可能仍然发现已有 Session,并在请求结束时尝试保存。

正确做法

真正的无状态 API 需要同时关闭所有可能创建 Session 的机制:

@Bean
@Order(1)
public SecurityFilterChain apiSecurity(HttpSecurity http) throws Exception {
    http
        .securityMatcher("/api/**")
        .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
        .httpBasic(Customizer.withDefaults())
        // 1. 禁用 Session 创建
        .sessionManagement(session -> 
            session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        )
        // 2. 禁用 CSRF(API 不需要,且避免 CSRF 创建 Session)
        .csrf(csrf -> csrf.disable())
        // 3. 禁用 SecurityContext 的 Session 持久化
        .securityContext(context -> context
            .securityContextRepository(new NullSecurityContextRepository())
        );
    return http.build();
}

面试考点:BasicAuthenticationFilter 认证成功后,如果同一个客户端再次发送请求(携带相同 Basic Header),过滤器会再次执行完整的认证流程吗?是否可以避免重复认证? 答:BasicAuthenticationFilter 在每次请求中都会重新解析 Authorization Header 并调用 AuthenticationManager.authenticate() 进行完整认证。它不会像表单登录那样将认证结果存入 Session 供后续复用。如果配置了 STATELESS Session 策略,每次请求都会经过完整的 UserDetailsService.loadUserByUsername() + PasswordEncoder.matches() 流程。要优化性能,可以考虑:① 在 BasicAuthenticationFilter 前添加自定义缓存过滤器,缓存已认证的 Header 到 Authentication 的映射;② 使用无状态的 JWT 替代 Basic 认证,减少数据库查询。但 Spring Security 本身不提供 Basic 认证结果的缓存机制,这是设计者有意为之——Basic 认证不适合高并发高频场景。

上一页
LogoutFilter
下一页
CsrfFilter