Spring Security 真正的核心不是某一个注解,而是整条安全过滤器链。很多"为什么登录后还是 401""为什么权限明明有却被拦"这类问题,最后都要回到:
理解这条链路,才能真正排查 Spring Security 问题。
Spring Security 会在请求进入业务 Controller 之前,先经过一组安全过滤器,负责:
这意味着安全逻辑不是"控制器里手写 if 判断",而是由框架统一在入口层处理。
1. 统一入口
所有请求 → 过滤器链 → Controller
↓
安全逻辑统一处理2. 关注点分离
// × 不推荐:在 Controller 中处理安全逻辑
@GetMapping("/orders")
public List<Order> getOrders(Authentication auth) {
if (!auth.getAuthorities().contains("ORDER_READ")) {
throw new AccessDeniedException();
}
return orderService.findAll();
}
// √ 推荐:在过滤器链中配置
@GetMapping("/orders")
@PreAuthorize("hasAuthority('ORDER_READ')")
public List<Order> getOrders() {
return orderService.findAll();
}3. 可插拔设计
Spring Security 的过滤器链由一系列有序的过滤器组成,每个过滤器负责特定的安全功能:
请求 → SecurityContextPersistenceFilter
→ LogoutFilter
→ UsernamePasswordAuthenticationFilter
→ DefaultLoginPageGeneratingFilter
→ DefaultLogoutPageGeneratingFilter
→ BasicAuthenticationFilter
→ RequestCacheAwareFilter
→ SecurityContextHolderAwareRequestFilter
→ SessionManagementFilter
→ ExceptionTranslationFilter
→ FilterSecurityInterceptor
→ Controller上面的链条是 Spring Security 5.x 的经典命名。在 6.x 中有两处更名: → (职责拆分); → ( 的新授权实现)。职责与顺序基本不变,下文按经典命名讲解。
职责:管理 SecurityContext 的生命周期
工作流程:
请求进入:
1. 从 Session 中加载 SecurityContext(如果存在)
2. 设置到 SecurityContextHolder
请求结束:
1. 从 SecurityContextHolder 获取 SecurityContext
2. 保存到 Session
3. 清空 SecurityContextHolder关键代码:
public class SecurityContextPersistenceFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) {
HttpRequestResponseHolder holder =
new HttpRequestResponseHolder(request, response);
// 请求开始:从仓库加载 SecurityContext
SecurityContext contextBeforeChainExecution =
repo.loadContext(holder);
try {
// 设置到 ThreadLocal
SecurityContextHolder.setContext(contextBeforeChainExecution);
// 继续过滤器链
filterChain.doFilter(holder.getRequest(), holder.getResponse());
} finally {
// 请求结束:保存 SecurityContext
SecurityContext contextAfterChainExecution =
SecurityContextHolder.getContext();
repo.saveContext(contextAfterChainExecution,
holder.getRequest(), holder.getResponse());
// 清空 ThreadLocal,防止内存泄漏
SecurityContextHolder.clearContext();
}
}
}注意事项:
职责:处理表单登录认证
工作流程:
POST /login
↓
1. 提取用户名和密码
2. 构造 UsernamePasswordAuthenticationToken
3. 调用 AuthenticationManager 认证
4. 认证成功:保存到 SecurityContext
5. 认证失败:抛出异常关键代码:
public class UsernamePasswordAuthenticationFilter
extends AbstractAuthenticationProcessingFilter {
public UsernamePasswordAuthenticationFilter() {
super(new AntPathRequestMatcher("/login", "POST"));
}
@Override
public Authentication attemptAuthentication(
HttpServletRequest request,
HttpServletResponse response) {
// 1. 提取用户名密码
String username = obtainUsername(request);
String password = obtainPassword(request);
// 2. 构造未认证的 Token
UsernamePasswordAuthenticationToken authRequest =
new UsernamePasswordAuthenticationToken(username, password);
// 3. 调用 AuthenticationManager 认证
return this.getAuthenticationManager().authenticate(authRequest);
}
// 认证成功回调
@Override
protected void successfulAuthentication(
HttpServletRequest request,
HttpServletResponse response,
FilterChain chain,
Authentication authResult) {
// 保存到 SecurityContext
SecurityContextHolder.getContext().setAuthentication(authResult);
// 调用成功处理器
successHandler.onAuthenticationSuccess(request, response, authResult);
}
}自定义登录路径:
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http)
throws Exception {
http
.formLogin(form -> form
.loginPage("/login")
.loginProcessingUrl("/auth/login")
.usernameParameter("username")
.passwordParameter("password")
.successHandler(new SimpleUrlAuthenticationSuccessHandler("/home"))
.failureHandler(new SimpleUrlAuthenticationFailureHandler("/login?error"))
);
return http.build();
}职责:处理 HTTP Basic 认证
工作原理:
请求头: Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=
↓
Base64 解码
↓
username:password
↓
调用 AuthenticationManager 认证关键代码:
public class BasicAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) {
// 1. 提取 Authorization 头
String header = request.getHeader("Authorization");
if (header == null || !header.startsWith("Basic ")) {
filterChain.doFilter(request, response);
return;
}
// 2. 解码 Base64
String[] tokens = extractAndDecodeHeader(header, request);
String username = tokens[0];
String password = tokens[1];
// 3. 构造认证 Token
UsernamePasswordAuthenticationToken authRequest =
new UsernamePasswordAuthenticationToken(username, password);
// 4. 认证
Authentication authResult =
authenticationManager.authenticate(authRequest);
// 5. 保存到 SecurityContext
SecurityContextHolder.getContext().setAuthentication(authResult);
filterChain.doFilter(request, response);
}
}启用 Basic 认证:
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http)
throws Exception {
http
.authorizeHttpRequests(authz -> authz
.anyRequest().authenticated()
)
.httpBasic(Customizer.withDefaults());
return http.build();
}职责:处理用户登出
工作流程:
POST /logout
↓
1. 验证 CSRF Token(如果启用)
2. 执行登出处理器链
- SecurityContextLogoutHandler:清空 SecurityContext
- CookieClearingLogoutHandler:清除 Cookie
- CsrfLogoutHandler:清除 CSRF Token
3. 调用登出成功处理器关键代码:
public class LogoutFilter extends GenericFilter {
private LogoutHandler handler;
private LogoutSuccessHandler logoutSuccessHandler;
@Override
public void doFilter(
ServletRequest request,
ServletResponse response,
FilterChain chain) {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse res = (HttpServletResponse) response;
// 检查是否是登出请求
if (requiresLogout(req)) {
// 执行登出处理器链
handler.logout(req, res,
SecurityContextHolder.getContext().getAuthentication());
// 调用成功处理器
logoutSuccessHandler.onLogoutSuccess(req, res,
SecurityContextHolder.getContext().getAuthentication());
return;
}
chain.doFilter(request, response);
}
}自定义登出配置:
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http)
throws Exception {
http
.logout(logout -> logout
.logoutUrl("/auth/logout")
.logoutSuccessUrl("/login?logout")
.addLogoutHandler(new CookieClearingLogoutHandler("JSESSIONID", "remember-me"))
.deleteCookies("JSESSIONID")
);
return http.build();
}职责:处理认证和授权异常
工作流程:
捕获异常:
↓
AuthenticationException (认证失败)
↓
AuthenticationEntryPoint.commence()
→ 重定向到登录页或返回 401
AccessDeniedException (授权失败)
↓
AccessDeniedHandler.handle()
→ 返回 403 或自定义响应关键代码:
public class ExceptionTranslationFilter extends GenericFilter {
private AuthenticationEntryPoint authenticationEntryPoint;
private AccessDeniedHandler accessDeniedHandler;
@Override
public void doFilter(
ServletRequest request,
ServletResponse response,
FilterChain chain) {
try {
chain.doFilter(request, response);
} catch (AuthenticationException ex) {
// 认证异常:启动认证流程
authenticationEntryPoint.commence(
(HttpServletRequest) request,
(HttpServletResponse) response,
ex
);
} catch (AccessDeniedException ex) {
// 授权异常:检查是否已认证
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth == null || auth.isAuthenticated() == false) {
// 未认证:启动认证流程
authenticationEntryPoint.commence(
(HttpServletRequest) request,
(HttpServletResponse) response,
new InsufficientAuthenticationException("Not authenticated", ex)
);
} else {
// 已认证但权限不足:调用 AccessDeniedHandler
accessDeniedHandler.handle(
(HttpServletRequest) request,
(HttpServletResponse) response,
ex
);
}
}
}
}自定义异常处理:
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http)
throws Exception {
http
.exceptionHandling(exceptions -> exceptions
.authenticationEntryPoint((request, response, authException) -> {
// 返回 JSON 而非重定向
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write(
"{\"code\":401,\"message\":\"未登录或登录已过期\"}"
);
})
.accessDeniedHandler((request, response, accessDeniedException) -> {
// 权限不足返回 JSON
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.getWriter().write(
"{\"code\":403,\"message\":\"权限不足\"}"
);
})
);
return http.build();
}职责:最终授权决策
工作原理:
请求 → FilterSecurityInterceptor
↓
检查配置的授权规则
↓
调用 AccessDecisionManager.decide()
↓
投票机制:
- AffirmativeBased:一票通过
- ConsensusBased:少数服从多数
- UnanimousBased:全票通过
↓
通过:继续到 Controller
拒绝:抛出 AccessDeniedException关键代码:
public class FilterSecurityInterceptor extends AbstractSecurityInterceptor
implements Filter {
@Override
public void doFilter(
ServletRequest request,
ServletResponse response,
FilterChain chain) {
FilterInvocation fi = new FilterInvocation(request, response, chain);
invoke(fi);
}
public void invoke(FilterInvocation fi) {
// 检查是否已处理过
if (fi.getRequest() != null && fi.getRequest().getAttribute(FILTER_APPLIED) != null) {
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
return;
}
// 标记为已处理
fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
// 执行授权检查
InterceptorStatusToken token = super.beforeInvocation(fi);
try {
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
} finally {
super.afterInvocation(token, null);
}
}
}配置授权规则:
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http)
throws Exception {
http
.authorizeHttpRequests(authz -> authz
.requestMatchers("/public/**").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers("/user/**").hasAnyRole("USER", "ADMIN")
.anyRequest().authenticated()
);
return http.build();
}过滤器链是有顺序的。顺序错了,常见问题包括:
这也是为什么自定义过滤器时,经常要显式指定加在谁前面或后面。
添加自定义过滤器:
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http)
throws Exception {
http
// 在指定过滤器之前添加
.addFilterBefore(jwtAuthenticationFilter(),
UsernamePasswordAuthenticationFilter.class)
// 在指定过滤器之后添加
.addFilterAfter(apiKeyFilter(),
BasicAuthenticationFilter.class)
// 在指定位置添加
.addFilterAt(customFilter(),
UsernamePasswordAuthenticationFilter.class);
return http.build();
}过滤器位置常量:
// Spring Security 内部的过滤器顺序登记表(节选示意)
// 实际实现为包私有的 FilterOrderRegistration(6.x;5.x 中为 FilterComparator),
// 开发者不能直接引用,自定义过滤器应通过 addFilterBefore/addFilterAfter 定位
final class FilterOrderRegistration {
public static final int FIRST = Integer.MIN_VALUE;
public static final int CHANNEL_FILTER = FIRST + 100;
public static final int SECURITY_CONTEXT_FILTER = CHANNEL_FILTER + 100;
public static final int CONCURRENT_SESSION_FILTER = SECURITY_CONTEXT_FILTER + 100;
public static final int LOGOUT_FILTER = CONCURRENT_SESSION_FILTER + 100;
public static final int X509_FILTER = LOGOUT_FILTER + 100;
public static final int PRE_AUTH_FILTER = X509_FILTER + 100;
public static final int CAS_FILTER = PRE_AUTH_FILTER + 100;
public static final int FORM_LOGIN_FILTER = CAS_FILTER + 100;
public static final int OPENID_FILTER = FORM_LOGIN_FILTER + 100;
public static final int LOGIN_PAGE_FILTER = OPENID_FILTER + 100;
public static final int DIGEST_FILTER = LOGIN_PAGE_FILTER + 100;
public static final int BASIC_FILTER = DIGEST_FILTER + 100;
public static final int REQUEST_CACHE_FILTER = BASIC_FILTER + 100;
public static final int SERVLET_API_SUPPORT_FILTER = REQUEST_CACHE_FILTER + 100;
public static final int JAAS_API_SUPPORT_FILTER = SERVLET_API_SUPPORT_FILTER + 100;
public static final int REMEMBER_ME_FILTER = JAAS_API_SUPPORT_FILTER + 100;
public static final int ANONYMOUS_FILTER = REMEMBER_ME_FILTER + 100;
public static final int SESSION_MANAGEMENT_FILTER = ANONYMOUS_FILTER + 100;
public static final int EXCEPTION_TRANSLATION_FILTER = SESSION_MANAGEMENT_FILTER + 100;
public static final int FILTER_SECURITY_INTERCEPTOR = EXCEPTION_TRANSLATION_FILTER + 100;
public static final int SWITCH_USER_FILTER = FILTER_SECURITY_INTERCEPTOR + 100;
public static final int LAST = Integer.MAX_VALUE;
}可以理解为当前请求的安全上下文。里面最关键的是 对象,它通常描述:
后续很多授权动作,本质上都是基于这个上下文完成的。
public interface SecurityContext extends Serializable {
// 获取当前认证对象
Authentication getAuthentication();
// 设置认证对象
void setAuthentication(Authentication authentication);
}是 Spring Security 中最核心的概念之一:
public interface Authentication extends Principal, Serializable {
// 获取权限列表
Collection<? extends GrantedAuthority> getAuthorities();
// 获取凭证(通常是密码,认证后会清除)
Object getCredentials();
// 获取额外信息(如 IP、Session ID 等)
Object getDetails();
// 获取主体(通常是用户名)
Object getPrincipal();
// 是否已认证
boolean isAuthenticated();
// 设置认证状态
void setAuthenticated(boolean isAuthenticated);
}用于用户名密码认证:
// 未认证状态
UsernamePasswordAuthenticationToken unauthenticated =
new UsernamePasswordAuthenticationToken("username", "password");
// 已认证状态
UsernamePasswordAuthenticationToken authenticated =
new UsernamePasswordAuthenticationToken(
"username", // principal:用户名或用户对象
null, // credentials:认证后清除
authorities // authorities:权限列表
);用于 JWT 资源服务器认证( 场景,由认证转换器构建):
// 解析 JWT 后构造
JwtAuthenticationToken token = new JwtAuthenticationToken(
jwt, // JWT 对象
authorities // 从 JWT 中提取的权限
);用于预认证场景(如 X.509 证书):
PreAuthenticatedAuthenticationToken token =
new PreAuthenticatedAuthenticationToken(
principal, // 主体对象
credentials, // 凭证
authorities // 权限
);用于匿名用户:
AnonymousAuthenticationToken anonymous =
new AnonymousAuthenticationToken(
"key", // 匿名用户标识
"anonymousUser", // 用户名
Arrays.asList(new SimpleGrantedAuthority("ROLE_ANONYMOUS"))
);是 SecurityContext 的持有者,使用 ThreadLocal 存储每个线程的安全上下文:
public class SecurityContextHolder {
// 默认使用 ThreadLocal 模式
private static final ThreadLocalSecurityContextHolderStrategy strategy =
new ThreadLocalSecurityContextHolderStrategy();
// 获取当前上下文
public static SecurityContext getContext() {
return strategy.getContext();
}
// 设置当前上下文
public static void setContext(SecurityContext context) {
strategy.setContext(context);
}
// 清空当前上下文
public static void clearContext() {
strategy.clearContext();
}
}Spring Security 提供了三种存储策略:
1. ThreadLocalSecurityContextHolderStrategy(默认)
// 每个线程独立的 SecurityContext
private static final ThreadLocal<SecurityContext> contextHolder =
new ThreadLocal<>();
public SecurityContext getContext() {
SecurityContext ctx = contextHolder.get();
if (ctx == null) {
ctx = createEmptyContext();
contextHolder.set(ctx);
}
return ctx;
}适用场景:
2. InheritableThreadLocalSecurityContextHolderStrategy
// 子线程继承父线程的 SecurityContext
private static final InheritableThreadLocal<SecurityContext> contextHolder =
new InheritableThreadLocal<>();适用场景:
注意事项:
3. GlobalSecurityContextHolderStrategy
// 全局共享一个 SecurityContext
private static SecurityContext context;适用场景:
配置策略:
// 在应用启动时配置
SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_THREADLOCAL);
// 或
SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);
// 或
SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_GLOBAL);一个典型流程通常如下:
如果上下文没写进去,后面所有权限判断都会失败。
1. 用户提交登录表单
POST /login?username=user&password=pass
↓
2. UsernamePasswordAuthenticationFilter 拦截
- 提取用户名密码
- 构造 UsernamePasswordAuthenticationToken
↓
3. AuthenticationManager 认证
- 委托 ProviderManager
- 遍历 AuthenticationProvider 列表
- 找到支持的 Provider(如 DaoAuthenticationProvider)
↓
4. DaoAuthenticationProvider 认证
- 调用 UserDetailsService.loadUserByUsername()
- 获取用户信息(UserDetails)
- 校验密码(passwordEncoder.matches())
- 构造已认证的 UsernamePasswordAuthenticationToken
↓
5. 写入 SecurityContext
SecurityContextHolder.getContext().setAuthentication(authentication);
↓
6. 调用成功处理器
- 重定向到首页或返回 JSON1. 请求携带 JWT
GET /api/user
Header: Authorization: Bearer eyJhbGciOiJSUzI1NiJ9...
↓
2. JwtAuthenticationFilter 拦截
- 提取 Authorization 头
- 解析 JWT
- 验证签名和过期时间
↓
3. 构造 Authentication
- 从 JWT 中提取用户信息
- 从 JWT 中提取权限列表
- 构造 JwtAuthenticationToken
↓
4. 写入 SecurityContext
SecurityContextHolder.getContext().setAuthentication(authentication);
↓
5. 继续过滤器链
- FilterSecurityInterceptor 进行授权检查
- 到达 Controller在前后端分离项目里,常见做法是:
如果顺序或写入逻辑出了问题,就会表现为:
@Component
public class JwtTokenProvider {
@Value("${jwt.secret}")
private String secretKey;
@Value("${jwt.expiration}")
private long expiration;
private Key getSigningKey() {
byte[] keyBytes = Decoders.BASE64.decode(secretKey);
return Keys.hmacShaKeyFor(keyBytes);
}
// 生成 Token
public String generateToken(Authentication authentication) {
UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();
Date now = new Date();
Date expiryDate = new Date(now.getTime() + expiration);
return Jwts.builder()
.setSubject(userPrincipal.getId())
.claim("username", userPrincipal.getUsername())
.claim("roles", userPrincipal.getAuthorities())
.setIssuedAt(now)
.setExpiration(expiryDate)
.signWith(getSigningKey(), SignatureAlgorithm.HS512)
.compact();
}
// 解析 Token
public Claims parseToken(String token) {
try {
return Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token)
.getBody();
} catch (JwtException ex) {
throw new InvalidTokenException("Invalid JWT token", ex);
}
}
// 验证 Token
public boolean validateToken(String token) {
try {
Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token);
return true;
} catch (JwtException ex) {
return false;
}
}
}public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtTokenProvider tokenProvider;
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
try {
// 1. 从请求头提取 JWT
String jwt = getJwtFromRequest(request);
if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) {
// 2. 解析 JWT 获取用户 ID
Claims claims = tokenProvider.parseToken(jwt);
String userId = claims.getSubject();
// 3. 加载用户信息
UserDetails userDetails =
userDetailsService.loadUserByUsername(userId);
// 4. 构造 Authentication
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities()
);
// 5. 设置详细信息
authentication.setDetails(
new WebAuthenticationDetailsSource().buildDetails(request)
);
// 6. 写入 SecurityContext
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception ex) {
logger.error("Could not set user authentication in security context", ex);
}
filterChain.doFilter(request, response);
}
private String getJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() {
return new JwtAuthenticationFilter();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http)
throws Exception {
http
.csrf(csrf -> csrf.disable())
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(authz -> authz
.requestMatchers("/auth/**").permitAll()
.anyRequest().authenticated()
)
// 添加 JWT 过滤器
.addFilterBefore(jwtAuthenticationFilter(),
UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public AuthenticationManager authenticationManager(
UserDetailsService userDetailsService,
PasswordEncoder passwordEncoder) {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(userDetailsService);
provider.setPasswordEncoder(passwordEncoder);
return new ProviderManager(provider);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}@RestController
@RequestMapping("/auth")
public class AuthController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtTokenProvider tokenProvider;
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest request) {
// 1. 认证
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
request.getUsername(),
request.getPassword()
)
);
// 2. 写入 SecurityContext
SecurityContextHolder.getContext().setAuthentication(authentication);
// 3. 生成 JWT
String token = tokenProvider.generateToken(authentication);
return ResponseEntity.ok(new JwtAuthenticationResponse(token));
}
}过滤器链是有顺序的。顺序错了,常见问题包括:
这也是为什么自定义过滤器时,经常要显式指定加在谁前面或后面。
// × 错误:JWT 过滤器在 SecurityContextPersistenceFilter 之前
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) {
http
.addFilterBefore(jwtFilter(), SecurityContextPersistenceFilter.class);
// 问题:SecurityContext 还没初始化,无法设置 Authentication
return http.build();
}
// √ 正确:JWT 过滤器在 UsernamePasswordAuthenticationFilter 之前
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) {
http
.addFilterBefore(jwtFilter(), UsernamePasswordAuthenticationFilter.class);
// 此时 SecurityContext 已加载,可以设置 Authentication
return http.build();
}// × 错误:ExceptionTranslationFilter 在 FilterSecurityInterceptor 之后
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) {
http
.addFilterAfter(exceptionTranslationFilter(), FilterSecurityInterceptor.class);
// 问题:FilterSecurityInterceptor 抛出的异常无法被捕获
return http.build();
}
// √ 正确:ExceptionTranslationFilter 应该在 FilterSecurityInterceptor 之前
// Spring Security 默认配置已经正确处理,无需手动添加// × 错误:过滤器不调用 filterChain.doFilter()
public class BadFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) {
// 处理逻辑
// 问题:忘记调用 filterChain.doFilter(),请求被拦截
return;
}
}
// √ 正确:必须调用 filterChain.doFilter()
public class GoodFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
// 处理逻辑
filterChain.doFilter(request, response); // 必须调用
}
}下面是一个简化的 JWT 认证过滤器示意:
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String authorization = request.getHeader("Authorization");
if (authorization != null && authorization.startsWith("Bearer ")) {
String token = authorization.substring(7);
String username = parseUsername(token);
List<GrantedAuthority> authorities = List.of(
new SimpleGrantedAuthority("order:read")
);
Authentication authentication =
new UsernamePasswordAuthenticationToken(username, null, authorities);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
private String parseUsername(String token) {
return "demo-user";
}
}这个例子体现了两件最关键的事:
public class DeviceAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private DeviceSessionService deviceSessionService;
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
// 1. 获取当前认证信息
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.isAuthenticated()) {
// 2. 提取设备信息
String deviceId = request.getHeader("X-Device-Id");
String userAgent = request.getHeader("User-Agent");
String ipAddress = getClientIpAddress(request);
// 3. 验证设备
DeviceValidationResult result = deviceSessionService.validateDevice(
authentication.getName(),
deviceId,
userAgent,
ipAddress
);
if (!result.isValid()) {
// 4. 设备验证失败,清空认证
SecurityContextHolder.clearContext();
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(
"{\"error\":\"" + result.getError() + "\"}"
);
return;
}
// 5. 更新设备最后使用时间
deviceSessionService.updateLastUsed(deviceId);
}
filterChain.doFilter(request, response);
}
private String getClientIpAddress(HttpServletRequest request) {
String xForwardedFor = request.getHeader("X-Forwarded-For");
if (xForwardedFor != null && !xForwardedFor.isEmpty()) {
return xForwardedFor.split(",")[0].trim();
}
return request.getRemoteAddr();
}
}public class ApiKeyAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private ApiKeyService apiKeyService;
private String headerName = "X-API-Key";
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
// 1. 提取 API Key
String apiKey = request.getHeader(headerName);
if (StringUtils.hasText(apiKey)) {
// 2. 验证 API Key
ApiKeyPrincipal principal = apiKeyService.validate(apiKey);
if (principal != null) {
// 3. 构造 Authentication
PreAuthenticatedAuthenticationToken authentication =
new PreAuthenticatedAuthenticationToken(
principal,
null,
principal.getAuthorities()
);
// 4. 写入 SecurityContext
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
filterChain.doFilter(request, response);
}
}这类问题通常不只是"前端没传 Token",还可能是:
排查步骤:
// 1. 在过滤器中添加日志
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String jwt = getJwtFromRequest(request);
logger.info("JWT from request: {}", jwt);
// ... 认证逻辑
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
logger.info("Authentication after set: {}", auth);
filterChain.doFilter(request, response);
}
// 2. 在 Controller 中检查 SecurityContext
@GetMapping("/user/profile")
public ResponseEntity<?> getProfile() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
logger.info("Authentication in controller: {}", auth);
if (auth == null || !auth.isAuthenticated()) {
return ResponseEntity.status(401).body("Not authenticated");
}
// ...
}
// 3. 检查过滤器链顺序
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) {
http
// 确保在正确的位置添加过滤器
.addFilterBefore(jwtFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}常见原因:
// × 在 SecurityContextPersistenceFilter 之前,SecurityContext 未初始化
.addFilterBefore(jwtFilter(), SecurityContextPersistenceFilter.class)
// √ 在 UsernamePasswordAuthenticationFilter 之前
.addFilterBefore(jwtFilter(), UsernamePasswordAuthenticationFilter.class)// × 使用 JWT 但开启了 Session
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED))
// √ JWT 模式应该无状态
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))// × OPTIONS 请求被拦截
.authorizeHttpRequests(authz -> authz.anyRequest().authenticated())
// √ 放行 OPTIONS 请求
.authorizeHttpRequests(authz -> authz
.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
.anyRequest().authenticated()
)请求能进入系统,但在授权阶段被拒绝,通常要回头看:
排查步骤:
// 1. 打印当前认证信息
@GetMapping("/debug/auth")
public ResponseEntity<?> debugAuth() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
Map<String, Object> info = new HashMap<>();
info.put("principal", auth.getPrincipal());
info.put("authorities", auth.getAuthorities());
info.put("authenticated", auth.isAuthenticated());
return ResponseEntity.ok(info);
}
// 2. 检查权限配置
@GetMapping("/admin/users")
@PreAuthorize("hasRole('ADMIN')") // 会添加 ROLE_ 前缀
public List<User> getUsers() {
// ...
}
@GetMapping("/admin/orders")
@PreAuthorize("hasAuthority('ROLE_ADMIN')") // 不会添加前缀
public List<Order> getOrders() {
// ...
}常见原因:
// UserDetails 中的权限
authorities.add(new SimpleGrantedAuthority("ADMIN"));
// × 错误:hasRole 会添加 ROLE_ 前缀,实际检查 ROLE_ADMIN
.hasRole("ADMIN")
// √ 正确方式 1:UserDetails 中添加 ROLE_ 前缀
authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
// √ 正确方式 2:使用 hasAuthority
.hasAuthority("ADMIN")// × 错误:返回的是字符串
public Collection<? extends GrantedAuthority> getAuthorities() {
return Arrays.asList("ADMIN", "USER");
}
// √ 正确:返回 GrantedAuthority
public Collection<? extends GrantedAuthority> getAuthorities() {
return Arrays.asList(
new SimpleGrantedAuthority("ROLE_ADMIN"),
new SimpleGrantedAuthority("ROLE_USER")
);
}@Configuration
@EnableWebSecurity
// × 缺少 @EnableMethodSecurity
public class SecurityConfig {
// ...
}
// √ 启用方法级权限
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
// ...
}这类情况通常说明:
排查步骤:
// 1. 检查方法权限表达式
@GetMapping("/orders/{id}")
@PreAuthorize("hasPermission(#id, 'order', 'read')")
public Order getOrder(@PathVariable Long id) {
// ...
}
// 2. 实现 PermissionEvaluator
@Component
public class CustomPermissionEvaluator implements PermissionEvaluator {
@Autowired
private OrderService orderService;
@Override
public boolean hasPermission(
Authentication authentication,
Object targetId,
Object permission) {
Long orderId = (Long) targetId;
String requiredPermission = (String) permission;
// 获取当前用户
UserPrincipal user = (UserPrincipal) authentication.getPrincipal();
// 检查权限
return orderService.hasPermission(user.getId(), orderId, requiredPermission);
}
}
// 3. 注册 PermissionEvaluator
@Configuration
@EnableMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig {
@Bean
public PermissionEvaluator permissionEvaluator() {
return new CustomPermissionEvaluator();
}
}在异步方法中,SecurityContext 可能丢失:
// × 问题:异步方法中 SecurityContext 为空
@Async
public void asyncTask() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
// auth 为 null
}
// √ 解决方案 1:传递认证参数
@Async
public void asyncTask(Authentication auth) {
// 使用传递的 auth
}
// √ 解决方案 2:使用 DelegatingSecurityContextRunnable
@Async
public void asyncTask() {
// 配置异步任务时使用
DelegatingSecurityContextRunnable runnable =
new DelegatingSecurityContextRunnable(
() -> { /* 任务逻辑 */ },
SecurityContextHolder.getContext()
);
runnable.run();
}
// √ 解决方案 3:配置异步使用 INHERITABLETHREADLOCAL
SecurityContextHolder.setStrategyName(
SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);安全链路问题排查时,优先看:
调试技巧:
// 1. 启用 Spring Security 调试日志
logging:
level:
org.springframework.security: DEBUG
// 2. 添加全局过滤器打印调试信息
@Bean
public Filter debugFilter() {
return new OncePerRequestFilter() {
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
logger.info("=== Request Start ===");
logger.info("URI: {}", request.getRequestURI());
logger.info("Method: {}", request.getMethod());
logger.info("Authorization: {}", request.getHeader("Authorization"));
filterChain.doFilter(request, response);
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
logger.info("Authentication: {}", auth);
logger.info("Authorities: {}",
auth != null ? auth.getAuthorities() : "null");
logger.info("=== Request End ===");
}
};
}
// 3. 将调试过滤器添加到链的最前面
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) {
http
.addFilterBefore(debugFilter(), ChannelProcessingFilter.class);
return http.build();
}最佳实践:
// 1. 使用日志记录关键安全事件
@Slf4j
public class SecurityAuditFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null && auth.isAuthenticated()) {
log.info("User: {}, URI: {}, Method: {}, IP: {}",
auth.getName(),
request.getRequestURI(),
request.getMethod(),
request.getRemoteAddr()
);
}
filterChain.doFilter(request, response);
}
}
// 2. 统一异常处理
@RestControllerAdvice
public class SecurityExceptionHandler {
@ExceptionHandler(AccessDeniedException.class)
public ResponseEntity<?> handleAccessDenied(AccessDeniedException ex) {
log.warn("Access denied: {}", ex.getMessage());
return ResponseEntity.status(403)
.body(Map.of("error", "Access denied"));
}
@ExceptionHandler(AuthenticationCredentialsNotFoundException.class)
public ResponseEntity<?> handleNoAuth(AuthenticationCredentialsNotFoundException ex) {
log.warn("Authentication not found: {}", ex.getMessage());
return ResponseEntity.status(401)
.body(Map.of("error", "Not authenticated"));
}
}
// 3. 使用配置类管理安全配置
@ConfigurationProperties(prefix = "security")
@Data
public class SecurityProperties {
private List<String> publicPaths = new ArrayList<>();
private boolean enableCsrf = true;
private boolean enableCors = true;
}
@Bean
public SecurityFilterChain securityFilterChain(
HttpSecurity http,
SecurityProperties properties) throws Exception {
http.authorizeHttpRequests(authz -> {
// 放行公共路径
for (String path : properties.getPublicPaths()) {
authz.requestMatchers(path).permitAll();
}
// 其他请求需要认证
authz.anyRequest().authenticated();
});
if (!properties.isEnableCsrf()) {
http.csrf(csrf -> csrf.disable());
}
return http.build();
}问题:
正确理解:
问题:
// × 在错误的位置添加
.addFilterAfter(jwtFilter(), FilterSecurityInterceptor.class)正确做法:
// √ 在正确的位置添加
.addFilterBefore(jwtFilter(), UsernamePasswordAuthenticationFilter.class)理解过滤器职责:
问题:
正确排查:
// 在 Controller 中检查
@GetMapping("/debug")
public ResponseEntity<?> debug() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
return ResponseEntity.ok(Map.of(
"authenticated", auth != null && auth.isAuthenticated(),
"authorities", auth != null ? auth.getAuthorities() : null,
"principal", auth != null ? auth.getPrincipal() : null
));
}问题:
正确做法:
// × 错误:在业务逻辑中清空 SecurityContext
@GetMapping("/logout")
public void logout() {
SecurityContextHolder.clearContext(); // 不应该在这里做
}
// √ 正确:使用 Spring Security 的登出机制
@PostMapping("/logout")
public void logout(HttpServletRequest request, HttpServletResponse response) {
// Spring Security 会自动处理
new SecurityContextLogoutHandler().logout(request, response, null);
}问题:
// × 异步方法中 SecurityContext 为空
@Async
public void asyncMethod() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
// auth 为 null!
}原因:
解决方案:
// 方案 1:传递认证参数
@Async
public void asyncMethod(Authentication auth) {
// 使用传递的参数
}
// 方案 2:使用 DelegatingSecurityContextAsyncTaskExecutor
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("async-");
executor.initialize();
// 包装为支持 SecurityContext 的 Executor
return new DelegatingSecurityContextAsyncTaskExecutor(executor);
}
}
// 方案 3:使用 INHERITABLETHREADLOCAL
SecurityContextHolder.setStrategyName(
SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);1. Spring Security 为什么强调过滤器链?
答:Spring Security 使用过滤器链是因为:
2. SecurityContext 里通常放什么?
答:SecurityContext 主要存放:
SecurityContext 通过 SecurityContextHolder 使用 ThreadLocal 存储,确保每个请求线程有独立的安全上下文。
3. JWT 模式下认证信息是怎么进入上下文的?
答:JWT 模式下的认证流程:
4. 为什么过滤器顺序会影响认证授权结果?
答:过滤器顺序关键的原因:
错误的顺序会导致:
5. 描述 Spring Security 认证流程的完整链路。
答:完整认证流程(以表单登录为例):
1. 用户提交登录表单
POST /login
2. UsernamePasswordAuthenticationFilter 拦截请求
- 提取用户名和密码
- 构造 UsernamePasswordAuthenticationToken(未认证状态)
3. 调用 AuthenticationManager.authenticate()
- 委托给 ProviderManager
- 遍历 AuthenticationProvider 列表
4. DaoAuthenticationProvider 处理认证
- 调用 UserDetailsService.loadUserByUsername()
- 获取 UserDetails(包含密码和权限)
- 使用 PasswordEncoder 验证密码
- 构造 UsernamePasswordAuthenticationToken(已认证状态)
5. 认证成功回调
- 将 Authentication 写入 SecurityContext
- SecurityContextPersistenceFilter 将 SecurityContext 保存到 Session
- 调用 AuthenticationSuccessHandler
6. 后续请求
- SecurityContextPersistenceFilter 从 Session 加载 SecurityContext
- FilterSecurityInterceptor 基于认证信息进行授权检查6. SecurityContextHolder 的三种存储策略有什么区别?
答:
| 策略 | 实现 | 特点 | 适用场景 |
|---|---|---|---|
| MODE_THREADLOCAL | ThreadLocal | 每个线程独立的 SecurityContext | Web 应用,一个请求一个线程 |
| MODE_INHERITABLETHREADLOCAL | InheritableThreadLocal | 子线程继承父线程的 SecurityContext | 需要在子线程中访问父线程认证信息的场景 |
| MODE_GLOBAL | 静态变量 | 全局共享一个 SecurityContext | 单线程应用,非 Web 应用 |
MODE_THREADLOCAL(默认):
// 每个线程独立
private static final ThreadLocal<SecurityContext> contextHolder =
new ThreadLocal<>();优点:线程安全,性能好。 注意:异步场景下需要特殊处理。
MODE_INHERITABLETHREADLOCAL:
// 子线程继承
private static final InheritableThreadLocal<SecurityContext> contextHolder =
new InheritableThreadLocal<>();优点:子线程可访问父线程的认证信息。 注意:线程池场景下可能出问题(线程复用导致数据混乱),需配合 DelegatingSecurityContextRunnable。
MODE_GLOBAL:
// 全局共享
private static SecurityContext context;优点:简单直接。 注意:多线程不安全,仅适用于单线程场景。
7. 如何实现自定义的认证过滤器?
答:实现自定义认证过滤器的步骤:
1. 继承 OncePerRequestFilter:
public class CustomAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
// 1. 提取认证信息
String credentials = extractCredentials(request);
// 2. 如果没有凭证,继续过滤器链
if (credentials == null) {
filterChain.doFilter(request, response);
return;
}
// 3. 认证
Authentication auth = authenticate(credentials);
// 4. 写入 SecurityContext
SecurityContextHolder.getContext().setAuthentication(auth);
// 5. 继续过滤器链
filterChain.doFilter(request, response);
}
private String extractCredentials(HttpServletRequest request) {
// 从请求头、参数等位置提取凭证
return request.getHeader("X-Custom-Auth");
}
private Authentication authenticate(String credentials) {
// 验证凭证并构造 Authentication
// ...
}
}2. 配置到过滤器链:
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http)
throws Exception {
http
.addFilterBefore(
new CustomAuthenticationFilter(),
UsernamePasswordAuthenticationFilter.class
);
return http.build();
}关键点:
8. 如何在异步方法中访问 SecurityContext?
答:
问题原因: Spring Security 默认使用 ThreadLocal 存储 SecurityContext,异步方法在新线程中执行,无法访问原线程的 ThreadLocal。
解决方案:
方案 1:传递认证参数
@Async
public void asyncMethod(Authentication auth) {
// 使用传递的参数
String username = auth.getName();
}方案 2:使用 DelegatingSecurityContextAsyncTaskExecutor
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.initialize();
// 包装为支持 SecurityContext 的 Executor
return new DelegatingSecurityContextAsyncTaskExecutor(executor);
}
}方案 3:使用 INHERITABLETHREADLOCAL
// 在应用启动时配置
SecurityContextHolder.setStrategyName(
SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);注意:线程池场景需谨慎使用,可能造成数据混乱。
方案 4:手动传递和设置 SecurityContext
@Async
public void asyncMethod(SecurityContext context) {
// 在新线程中设置 SecurityContext
SecurityContextHolder.setContext(context);
try {
// 业务逻辑
} finally {
// 清理
SecurityContextHolder.clearContext();
}
}9. 如何排查"登录后仍 401"的问题?
答:
排查步骤:
1. 确认 Token 是否传递:
// 在过滤器中打印日志
@Override
protected void doFilterInternal(...) {
String token = request.getHeader("Authorization");
log.info("Authorization header: {}", token);
// ...
}2. 检查过滤器是否执行:
// 在自定义过滤器入口处打印日志
log.info("JwtAuthenticationFilter executed for URI: {}", request.getRequestURI());3. 检查 SecurityContext 是否设置成功:
// 设置后立即检查
SecurityContextHolder.getContext().setAuthentication(authentication);
Authentication stored = SecurityContextHolder.getContext().getAuthentication();
log.info("Authentication stored: {}", stored);4. 在 Controller 中检查:
@GetMapping("/debug")
public ResponseEntity<?> debug() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
return ResponseEntity.ok(Map.of(
"authenticated", auth != null && auth.isAuthenticated(),
"authorities", auth != null ? auth.getAuthorities() : null
));
}5. 检查过滤器顺序:
// 确保在正确位置添加
.addFilterBefore(jwtFilter(), UsernamePasswordAuthenticationFilter.class)常见原因:
10. 如何排查"权限足够但仍 403"的问题?
答:
排查步骤:
1. 打印当前认证信息:
@GetMapping("/debug/auth")
public ResponseEntity<?> debugAuth() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
return ResponseEntity.ok(Map.of(
"principal", auth.getPrincipal(),
"authorities", auth.getAuthorities(),
"details", auth.getDetails()
));
}2. 检查权限配置:
// 检查是用 hasRole 还是 hasAuthority
@PreAuthorize("hasRole('ADMIN')") // 会添加 ROLE_ 前缀
@PreAuthorize("hasAuthority('ROLE_ADMIN')") // 不会添加前缀3. 检查 UserDetails 中的权限:
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
// 确保权限格式正确
return Arrays.asList(
new SimpleGrantedAuthority("ROLE_ADMIN") // hasRole 需要前缀
);
}4. 检查方法级权限是否启用:
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true) // 必须启用
public class SecurityConfig {
// ...
}5. 检查 URL 配置和方法注解冲突:
// URL 配置
.authorizeHttpRequests(authz -> authz
.requestMatchers("/admin/**").hasRole("ADMIN")
)
// 方法注解
@GetMapping("/admin/users")
@PreAuthorize("hasAuthority('USER')") // 可能与 URL 配置冲突常见原因:
11. 如何实现"记住我"功能?
答:
方案 1:使用 Spring Security 内置的 Remember-Me
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http)
throws Exception {
http
.rememberMe(remember -> remember
.key("uniqueAndSecret")
.tokenValiditySeconds(14 * 24 * 60 * 60) // 14 天
.rememberMeParameter("remember-me")
.rememberMeCookieName("remember-me")
.userDetailsService(userDetailsService)
);
return http.build();
}
// 登录表单添加 checkbox
<input type="checkbox" name="remember-me"> 记住我工作原理:
方案 2:自定义持久化 Token
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http)
throws Exception {
http
.rememberMe(remember -> remember
.tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(14 * 24 * 60 * 60)
);
return http.build();
}
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource);
return tokenRepository;
}
// 需要创建表
CREATE TABLE persistent_logins (
username VARCHAR(64) NOT NULL,
series VARCHAR(64) PRIMARY KEY,
token VARCHAR(64) NOT NULL,
last_used TIMESTAMP NOT NULL
);方案 3:使用 JWT 实现"记住我"
// 登录时根据用户选择生成不同有效期的 Token
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest request) {
// 认证...
long expiration = request.isRememberMe()
? 14 * 24 * 60 * 60 * 1000L // 14 天
: 30 * 60 * 1000L; // 30 分钟
String token = jwtTokenProvider.generateToken(authentication, expiration);
return ResponseEntity.ok(new JwtResponse(token));
}安全考虑:
12. 如何实现多因素认证(MFA)?
答:
实现方案:
1. 认证流程设计
用户名密码登录
↓
第一步认证成功
↓
检查是否启用 MFA
↓
是 → 发送验证码 → 输入验证码 → 第二步认证
否 → 直接登录成功2. 实现代码
// 认证状态枚举
public enum AuthenticationStage {
FIRST_FACTOR_COMPLETED, // 第一步认证完成
SECOND_FACTOR_REQUIRED, // 需要第二步认证
FULLY_AUTHENTICATED // 完全认证
}
// 认证详情
public class MfaAuthenticationDetails {
private AuthenticationStage stage;
private String userId;
private String tempToken; // 第一步认证后的临时 Token
private Instant authenticatedAt;
}
// 第一步认证
@PostMapping("/auth/login")
public ResponseEntity<?> login(@RequestBody LoginRequest request) {
// 1. 用户名密码认证
Authentication auth = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
request.getUsername(),
request.getPassword()
)
);
User user = (User) auth.getPrincipal();
// 2. 检查是否启用 MFA
if (user.isMfaEnabled()) {
// 生成临时 Token
String tempToken = mfaService.generateTempToken(user.getId());
// 发送验证码
mfaService.sendCode(user.getId(), user.getMfaType());
// 返回临时 Token
return ResponseEntity.ok(Map.of(
"stage", "MFA_REQUIRED",
"tempToken", tempToken,
"mfaType", user.getMfaType()
));
}
// 3. 未启用 MFA,直接返回 Token
String accessToken = jwtTokenProvider.generateToken(auth);
return ResponseEntity.ok(Map.of(
"stage", "AUTHENTICATED",
"accessToken", accessToken
));
}
// 第二步认证
@PostMapping("/auth/mfa/verify")
public ResponseEntity<?> verifyMfa(@RequestBody MfaVerifyRequest request) {
// 1. 验证临时 Token
String userId = mfaService.validateTempToken(request.getTempToken());
// 2. 验证 MFA 代码
boolean valid = mfaService.verifyCode(userId, request.getCode());
if (!valid) {
throw new InvalidMfaCodeException("Invalid MFA code");
}
// 3. 认证成功,生成正式 Token
UserDetails user = userDetailsService.loadUserByUsername(userId);
Authentication auth = new UsernamePasswordAuthenticationToken(
user, null, user.getAuthorities()
);
String accessToken = jwtTokenProvider.generateToken(auth);
return ResponseEntity.ok(Map.of(
"stage", "AUTHENTICATED",
"accessToken", accessToken
));
}
// MFA 服务
@Service
public class MfaService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
// 生成临时 Token
public String generateTempToken(String userId) {
String token = UUID.randomUUID().toString();
String key = "mfa:temp:" + token;
redisTemplate.opsForValue().set(
key, userId,
5, TimeUnit.MINUTES // 5 分钟有效
);
return token;
}
// 验证临时 Token
public String validateTempToken(String token) {
String key = "mfa:temp:" + token;
String userId = redisTemplate.opsForValue().get(key);
if (userId == null) {
throw new InvalidTokenException("Temp token expired or invalid");
}
// 删除临时 Token(一次性使用)
redisTemplate.delete(key);
return userId;
}
// 发送验证码
public void sendCode(String userId, MfaType type) {
String code = generateCode();
String key = "mfa:code:" + userId;
// 存储验证码
redisTemplate.opsForValue().set(
key, code,
5, TimeUnit.MINUTES
);
// 发送
switch (type) {
case SMS:
smsService.send(userId, code);
break;
case EMAIL:
emailService.send(userId, code);
break;
case TOTP:
// TOTP 由客户端应用生成,无需发送
break;
}
}
// 验证验证码
public boolean verifyCode(String userId, String code) {
String key = "mfa:code:" + userId;
String storedCode = redisTemplate.opsForValue().get(key);
if (storedCode != null && storedCode.equals(code)) {
redisTemplate.delete(key);
return true;
}
return false;
}
}3. Spring Security 集成
// 自定义认证过滤器
public class MfaAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
// 检查是否需要 MFA
String tempToken = request.getHeader("X-Temp-Token");
if (tempToken != null) {
// 临时认证,只允许访问 MFA 相关接口
String uri = request.getRequestURI();
if (!uri.startsWith("/auth/mfa/")) {
response.sendError(403, "MFA required");
return;
}
}
filterChain.doFilter(request, response);
}
}安全考虑:
| 特性 | 旧版(Spring Security 5.x) | Spring Security 6.x |
|---|---|---|
| 过滤器链 | FilterChainProxy | 不变;SecurityFilterChain Bean |
| SecurityContext | ThreadLocal | 不变;虚拟线程下需注意 ThreadLocal 传播 |
| 自定义过滤器 | 手动 | 不变;推荐 OncePerRequestFilter |
| 虚拟线程 | 无 | 虚拟线程卸载/挂载保留 ThreadLocal;跨线程异步才需传播 |