Spring Security¶
面试追问地图¶
| 主问题 | 必讲关键点 | 下一层追问 |
|---|---|---|
| 过滤器链 | DelegatingFilterProxy、FilterChainProxy、SecurityFilterChain | 多条链如何匹配、过滤器顺序 |
| 认证流程 | Authentication、AuthenticationManager、ProviderManager | 多种认证方式如何扩展 |
| 安全上下文 | SecurityContextHolder、ThreadLocal、请求结束清理 | 异步线程如何传递身份 |
| 401/403 | 未认证 vs 权限不足 | 为什么业务异常处理器接不到 |
| 密码编码 | 慢哈希、DelegatingPasswordEncoder、算法迁移 | {id} 前缀、何时升级哈希 |
| 请求授权 | RequestMatcher、GrantedAuthority、@PreAuthorize |
URL 放行后为何仍需对象级鉴权 |
| JWT 资源服务器 | Bearer Token、签名和声明校验、无服务端会话 | Token 撤销、密钥轮换 |
| CSRF/CORS | 凭证是否自动携带、预检请求、配置顺序 | Cookie JWT 是否仍有 CSRF |
Spring Security 解决的是应用安全链路的编排;认证协议、JWT 和 OAuth 2.0 的安全边界见安全认证。
一、整体架构¶
Spring Security 的过滤器链如何工作?¶
频次 ★★★★★ · 难度 🔴 · 高频:全厂
Servlet 容器只认识标准 Filter,Spring Bean 则由容器管理,两者通过以下链路衔接:
Servlet FilterChain
-> DelegatingFilterProxy
-> FilterChainProxy
-> 第一个匹配请求的 SecurityFilterChain
-> 认证、异常转换、授权等 Security Filter
-> DispatcherServlet
DelegatingFilterProxy:在 Servlet 生命周期与 Spring 容器之间做桥接,把请求委托给 Spring Bean。FilterChainProxy:Spring Security 的总入口,选择安全链、应用防火墙并在请求结束时清理安全上下文。SecurityFilterChain:一组带RequestMatcher的安全过滤器。可以为/api/**、管理端和静态资源配置不同链。
多条链只执行第一个匹配项,所以必须把范围更窄的链放在前面,并配置兜底链。把 /api/** 链排在 /** 后面,会导致前者永远无法命中。
@Bean
@Order(1)
SecurityFilterChain apiChain(HttpSecurity http) throws Exception {
return http
.securityMatcher("/api/**")
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
.oauth2ResourceServer(oauth -> oauth.jwt(Customizer.withDefaults()))
.build();
}
常见追问: - 自定义 JWT Filter 应该放在哪?→ 放在授权过滤器之前,并保证只执行一次;更推荐直接使用 Resource Server 的 Bearer Token 支持,而不是重复实现协议解析。 - 为什么过滤器顺序重要?→ 必须先加载上下文、再认证、最后授权;顺序颠倒时授权阶段看不到已认证身份。
用户名密码认证流程是什么?¶
频次 ★★★★★ · 难度 🔴 · 高频:全厂
- 认证过滤器从请求提取凭证,构造尚未认证的
Authentication。 - 过滤器调用
AuthenticationManager.authenticate()。 - 常用实现
ProviderManager遍历支持该 Token 类型的AuthenticationProvider。 - Provider 读取用户、校验密码,返回已认证且带权限的
Authentication。 - 成功结果写入
SecurityContext;失败交给失败处理器。
ProviderManager 负责路由,AuthenticationProvider 负责一种认证机制。用户名密码、短信验证码、LDAP 可以各自实现 Provider,而不是在一个过滤器里堆条件分支。
常见追问:
- UserDetailsService 会校验密码吗?→ 不会,它只加载用户;密码匹配由 DaoAuthenticationProvider 配合 PasswordEncoder 完成。
- Authentication 中有什么?→ principal、credentials、authorities 和认证状态;成功后通常清除凭证,减少密码残留。
SecurityContext 为什么通常存在线程本地变量中?¶
频次 ★★★★ · 难度 🟡
SecurityContextHolder 默认用 ThreadLocal 保存当前请求的 SecurityContext,业务代码无需层层传递身份参数,同一请求线程内都能读取 Authentication。请求结束必须清理,否则线程池复用会把上一个用户身份泄漏给下一个请求;FilterChainProxy 会负责清理。
异步任务切换线程后,普通 ThreadLocal 不会自动传递。可使用 DelegatingSecurityContextExecutor、DelegatingSecurityContextRunnable,或在提交任务时显式捕获并在执行后清理上下文。不要仅切换为 MODE_INHERITABLETHREADLOCAL 就交给线程池:线程池线程通常早已创建,不会为每个任务重新继承。
通用概念:这与 MDC、事务上下文的传播问题相同,都是“捕获 → 传递 → 恢复 → 清理”的上下文生命周期,底层原理见并发编程的 ThreadLocal。
二、异常与密码¶
AuthenticationEntryPoint 和 AccessDeniedHandler 分别处理什么?¶
频次 ★★★★ · 难度 🟡
| 场景 | 处理组件 | 常见响应 |
|---|---|---|
| 用户未认证,需要提供凭证 | AuthenticationEntryPoint |
401 或跳转登录页 |
| 用户已认证,但权限不足 | AccessDeniedHandler |
403 |
ExceptionTranslationFilter 把过滤器链中的认证/授权异常转换为 HTTP 响应。异常发生在 DispatcherServlet 之前,因此普通 @ControllerAdvice 通常接不到;API 项目应分别配置 JSON 格式的 EntryPoint 和 Handler,不能把所有失败都返回 200。
PasswordEncoder 为什么使用委托模式?¶
频次 ★★★★ · 难度 🟡
密码哈希参数会随硬件和安全要求演进。DelegatingPasswordEncoder 用 {id}encodedPassword 保存算法标识,例如 {bcrypt}...,校验旧密码时按 id 路由到旧算法,新密码按当前默认算法生成,从而支持渐进迁移。
密码应使用 BCrypt、PBKDF2、scrypt 或 Argon2 等带盐慢哈希,不能使用 MD5、SHA-256 等快速摘要。登录成功后若 upgradeEncoding() 判断参数已落后,可在本次认证后重新编码并更新存储。
三、授权¶
请求级授权和方法级授权如何配合?¶
频次 ★★★★★ · 难度 🟡 · 高频:全厂
- 请求级授权:在
authorizeHttpRequests中按路径、HTTP 方法和身份做入口拦截。 - 方法级授权:启用
@EnableMethodSecurity,用@PreAuthorize等表达式保护 Service 方法。 - 数据级授权:在业务查询中限制资源归属、租户或数据范围,不能靠角色字符串代替。
@PreAuthorize("hasAuthority('order:refund') and #operatorId == authentication.name")
public void refund(Long orderId, String operatorId) {
// 仍需在业务层校验订单归属和状态
}
URL 权限只能说明“能否进入接口”,不能证明用户有权操作某个订单。水平越权和对象归属校验见安全认证。
hasRole 和 hasAuthority 有什么区别?¶
hasRole("ADMIN") 默认检查 ROLE_ADMIN,会自动加 ROLE_ 前缀;hasAuthority("order:read") 按原字符串比较。项目应统一角色和细粒度权限的命名,避免数据库存 ADMIN、代码又混用 ROLE_ADMIN 导致误判。
四、无状态 API¶
无状态 JWT API 如何配置?¶
频次 ★★★★★ · 难度 🟡 · 高频:字节/阿里
资源服务器接收 Authorization: Bearer <token>,验证签名、有效期、发行方和受众,再把声明转换为 GrantedAuthority。无状态表示服务端不保存登录 Session,不表示 Token 不需要撤销、轮换和审计。
http
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.oauth2ResourceServer(oauth -> oauth.jwt(Customizer.withDefaults()));
生产配置至少要限制允许的算法、校验 iss/aud、支持 kid 密钥轮换,并控制时钟偏差。Access Token 应短期有效;强制下线可结合短有效期、Refresh Token 轮换和高风险场景黑名单,详见安全认证。
前后端分离项目能直接关闭 CSRF 吗?¶
频次 ★★★★ · 难度 🟡
不能只凭“前后端分离”判断,应看浏览器是否会自动携带认证凭证:
- Bearer Token 只放在自定义
Authorization头,由前端代码主动添加,通常可关闭 CSRF 防护。 - Session Cookie 或把 JWT 放进 Cookie,浏览器会自动携带,仍需 CSRF Token、SameSite 等防护。
- CORS 解决的是跨域读取权限,不是 CSRF;必须限制可信 Origin,不能在携带凭证时使用通配来源。
常见追问:预检请求为什么被 401?→ OPTIONS 在认证过滤器前未放行,或 CORS Filter 顺序错误;应先处理 CORS,再进入认证授权链。