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 | 无 Authorization | 401 Unauthorized | BasicAuthenticationFilter 未检测到 Header,由 AuthenticationEntryPoint 返回 401 |
GET /api/admin/stats | Authorization: Basic YWRtaW46YWRtaW4xMjM=(admin:admin123) | 200 OK | 认证成功,SecurityContext 中存入 ROLE_ADMIN |
GET /api/admin/stats | Authorization: Basic dXNlcjp1c2VyMTIz(user:user123) | 403 Forbidden | 认证成功但 hasRole("ADMIN") 拒绝,由 ExceptionTranslationFilter 转译为 403 |
GET /api/admin/stats | Authorization: Basic YWRtaW46d3JvbmdwYXNz | 401 Unauthorized | 密码错误,DaoAuthenticationProvider 抛出 BadCredentialsException |
GET /web/home | 无 Authorization(通过 Cookie Session) | 302 到 /login | Web 链使用表单登录,无 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-Authenticate | 401 JSON | 不弹窗,前端可处理 |
错误凭据调用 /api/data | 无 WWW-Authenticate | 401 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:
- 其他过滤器创建 Session:例如
CsrfFilter使用HttpSessionCsrfTokenRepository(默认),访问页面时会创建 Session。 - Servlet 容器自动创建 Session:如果应用的其他部分(如 JSP 页面)触发了 Session 创建。
- 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在每次请求中都会重新解析AuthorizationHeader 并调用AuthenticationManager.authenticate()进行完整认证。它不会像表单登录那样将认证结果存入 Session 供后续复用。如果配置了STATELESSSession 策略,每次请求都会经过完整的UserDetailsService.loadUserByUsername()+PasswordEncoder.matches()流程。要优化性能,可以考虑:① 在BasicAuthenticationFilter前添加自定义缓存过滤器,缓存已认证的 Header 到Authentication的映射;② 使用无状态的 JWT 替代 Basic 认证,减少数据库查询。但 Spring Security 本身不提供 Basic 认证结果的缓存机制,这是设计者有意为之——Basic 认证不适合高并发高频场景。