Spring Security技术文档:从入门到精通(Spring Security 6.x 版)
开篇
面试官:"你们项目的安全架构是怎么设计的?"
你:"用了 Spring Security!"
面试官追问:"那 Spring Security 6.x 相比 5.x 有什么本质区别?OAuth2 Resource Server 的 JwtAuthenticationConverter 职责边界在哪?JWT 令牌版本控制怎么实现全设备强制登出?Redis Bitmap 做用户封禁的内存开销是多少?"
你开始支支吾吾......
或者,凌晨 3 点你被电话吵醒------线上系统被攻击,用户数据泄露。老板问你:"不是用了 Spring Security 吗?怎么还会被攻击?"
先别急着背 API,我们先看一个真实的生产级架构。
这份文档会带你从 Spring Security 6.x 的最新架构开始,结合真实项目代码(来自生产环境),一层一层剥开现代安全设计的面纱。
本文假设: 你已经熟悉 Java 和 Spring Boot,使用的是 Spring Security 6.x + Spring Boot 3.x。
第一篇:Spring Security 6.x 核心架构------理解现代安全设计
过滤器链真的过时了吗?
技术社区里经常听到这样的声音:
- "Spring Security 6.x 还在用过滤器链吗?太老土了吧?"
- "不是应该用拦截器或者 AOP 吗?"
- "过滤器链性能很差吧?"
表面看是架构问题,底层其实是认知偏差。
答案是:过滤器链依然是底层基础,但上层 API 已经完全现代化了。
想象一下汽车的发展:
- 发动机(Filter Chain):依然是核心动力,没有它车跑不动
- 变速箱(SecurityFilterChain 配置):从手动挡升级到了自动挡
- 驾驶模式(OAuth2 Resource Server):从传统驾驶升级到了智能辅助驾驶
所以,过滤器链没有落伍,而是被更好地封装了。
架构演进:Spring Security 5.x → 6.x
Spring Security 6.x - 现代模式
SecurityFilterChain Bean
HttpSecurity DSL
OAuth2ResourceServer
JwtAuthenticationConverter
Spring Security 5.x - 传统模式
WebSecurityConfigurerAdapter
configure(HttpSecurity)
手动写 JwtTokenFilter
继承 OncePerRequestFilter
文字版架构演进说明:
Spring Security 5.x(已废弃):
- 使用
WebSecurityConfigurerAdapter(已被标记为 @Deprecated) - 需要手动继承
OncePerRequestFilter写 JWT 过滤器 - 配置方式较为繁琐
Spring Security 6.x(推荐):
- 使用
SecurityFilterChainBean +HttpSecurityDSL - 内置
OAuth2ResourceServer支持 JWT 自动解析 - 只需实现
JwtAuthenticationConverter做业务验证 - 代码量减少 50%+,更符合现代开发习惯
核心组件关系图(现代架构)
查询令牌版本和封禁状态
创建配置
配置资源服务器
注册转换器
创建认证对象
调用工具类
<<Configuration>>
SecurityConfig
+securityFilterChain() : SecurityFilterChain
HttpSecurity
+oauth2ResourceServer()
+exceptionHandling()
+sessionManagement()
OAuth2ResourceServer
+jwt() : JwtAuthenticationConverter
JwtAuthenticationConverter
+convert(Jwt) : JwtAuthenticationToken
JwtAuthenticationToken
-userInfo: JwtUserInfo
-rawToken: String
+getUserId() : Long
+getUserInfo() : JwtUserInfo
RestAuthenticationEntryPoint
+commence(request, response, authException)
JwtTokenService
+parseSubjectAsUserId(String) : Long
+assertTokenVersion(Jwt, long, String)
RedisTemplate
文字版组件关系说明:
- SecurityConfig :主配置类,创建
SecurityFilterChainBean - HttpSecurity:DSL 构建器,链式配置安全策略
- OAuth2ResourceServer:资源服务器配置,处理 JWT 解析
- JwtAuthenticationConverter :JWT 转换器,将 JWT 转为认证对象(核心业务逻辑)
- JwtAuthenticationToken:自定义认证对象,存储用户信息
- RestAuthenticationEntryPoint:认证失败处理器,返回统一 JSON 错误响应
- JwtTokenService:JWT 工具类,提供解析和验证功能
第二篇:认证机制------传统 vs 现代方案对比
两种认证模式的 Trade-off(数据说话)
同样实现 JWT 认证,代码量差距有多大?
| 方案 | 代码行数 | 维护成本 | 安全性 | 推荐度 |
|---|---|---|---|---|
| 传统 Filter 模式 | 200+ 行 | 高(手动处理异常) | 取决于开发者 | ⭐⭐ |
| OAuth2 Resource Server | 80-120 行 | 低(框架兜底) | 经过社区验证 | ⭐⭐⭐⭐⭐ |
你想想,如果让你选择:
- 方案 A:自己写一个 Filter,手动解析 JWT、验证签名、查询数据库......
- 方案 B:让框架自动解析 JWT,你只写业务逻辑(如查 Redis、检查权限)......
显然是方案 B!这就是 Spring Security 6.x 的核心思想:把通用的交给框架,把业务的留给你。
方案一:传统 Filter 模式(Spring Security 5.x)
适用场景: 需要完全控制认证流程的复杂场景
优点:
- 完全掌控认证流程
- 可以定制任何逻辑
缺点:
- 代码量大(通常 200+ 行)
- 需要自己处理 JWT 解析、签名验证等通用逻辑
- 容易出错(如时间戳校验、算法匹配等)
代码示例(已过时):
java
// Spring Security 5.x 的写法(不推荐)
public class JwtTokenFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
// 1. 从请求头提取 Token
String token = resolveToken(request);
if (token == null) {
filterChain.doFilter(request, response);
return;
}
// 2. 解析 JWT(需要自己处理各种异常)
try {
Claims claims = Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(token)
.getBody();
// 3. 验证 Token 是否过期
if (claims.getExpiration().before(new Date())) {
throw new JwtException("Token expired");
}
// 4. 查询用户信息
UserDetails userDetails = userDetailsService.loadUserByUsername(claims.getSubject());
// 5. 创建 Authentication 对象
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
// 6. 存入 SecurityContext
SecurityContextHolder.getContext().setAuthentication(authentication);
} catch (Exception e) {
// 处理异常...
}
filterChain.doFilter(request, response);
}
}
方案二:OAuth2 Resource Server 模式(Spring Security 6.x )
适用场景: 前后端分离、微服务、REST API(推荐)
优点:
- 框架自动处理 JWT 解析、签名验证、过期检查
- 代码量少(通常 100 行以内)
- 符合 OAuth2 标准,易于集成第三方
- 更安全(避免重复造轮子)
缺点:
- 需要学习 OAuth2 Resource Server 的配置方式
- 对自定义逻辑有一定限制(但通过 Converter 可以解决)
代码示例(生产级):
java
//Spring Security 6.x 的写法(推荐)
@Configuration
@EnableWebSecurity
public class SecurityConfig {
/**
* 安全过滤器链配置(核心配置方法)
*
* 这是 Spring Security 6.x 的标准配置方式。
* 相比 5.x 的 WebSecurityConfigurerAdapter,更加简洁和类型安全。
*/
@Bean
public SecurityFilterChain securityFilterChain(
HttpSecurity http,
JwtAuthenticationConverter jwtAuthenticationConverter,
RestAuthenticationEntryPoint restAuthenticationEntryPoint) throws Exception {
// 1. 配置 CSRF(REST API 通常禁用)
http.csrf(csrf -> csrf.disable());
// 2. 配置 Session 管理(无状态)
http.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
// 3. 配置 OAuth2 资源服务器(JWT 认证的核心配置)
http.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
// 注册自定义 JWT 转换器(关键!)
.jwtAuthenticationConverter(jwtAuthenticationConverter)
)
// 配置认证失败处理器
.authenticationEntryPoint(restAuthenticationEntryPoint)
);
// 4. 配置 URL 权限规则
http.authorizeHttpRequests(auth -> auth
// 公开接口(不需要认证)
.requestMatchers("/api/v1/auth/**", "/actuator/health").permitAll()
// 需要认证的接口
.requestMatchers("/api/v1/users/**").authenticated()
// 需要管理员权限的接口
.requestMatchers("/api/v1/admin/**").hasRole("ADMIN")
// 其他所有接口都需要认证
.anyRequest().authenticated()
);
return http.build();
}
}
面试追问:
Q:Spring Security 6.x 相比 5.x 有哪些重大变化?为什么要弃用 WebSecurityConfigurerAdapter?
A:
重大变化:
- 弃用 WebSecurityConfigurerAdapter :改用
SecurityFilterChainBean +HttpSecurityDSL- 引入 OAuth2 Resource Server:内置 JWT/Opaque Token 支持,无需手写 Filter
- 新增 authorizeHttpRequests() :替代
authorizeRequests(),基于 Servlet API 而非 Spring MVC弃用原因:
- WebSecurityConfigurerAdapter 是一个巨大的 God Class,职责过多
- 继承方式不够灵活,难以组合多个配置
- 新的方式更函数式、更类型安全、更易测试
迁移建议: 如果还在用 5.x,建议尽快升级到 6.x,官方提供了详细的迁移指南。
第三篇:OAuth2 Resource Server + JWT 认证(现代安全架构)
微服务架构下的安全困境
你有没有想过这样一个问题:
如果你要构建一个微服务架构的安全体系:
- 用户服务负责登录签发 JWT
- 订单服务需要验证 JWT
- 支付服务也需要验证 JWT
- 每个服务都要写一遍 JWT 解析逻辑?
这太痛苦了!而且一旦 JWT 解析逻辑有 Bug(比如时间戳校验遗漏),所有服务都要重新部署。
OAuth2 Resource Server 的价值在于:标准化 JWT 验证流程,让每个服务都能开箱即用地验证 Token。
如果把这个问题放到线上流量下(QPS 10万+),你会怎么选?
- A. 每个服务自己写 Filter,重复造轮子
- B. 使用 OAuth2 Resource Server,框架自动处理 + 你只写业务验证
聪明的你肯定选 B。
OAuth2 Resource Server 工作原理
Redis集群 资源服务(验证JWT) 认证服务(签发JWT) API网关 客户端(前端/移动端) Redis集群 资源服务(验证JWT) 认证服务(签发JWT) API网关 客户端(前端/移动端) 用户访问受保护接口 alt [验证通过] [验证失败] 1. 登录(用户名+密码) 2. 存储令牌版本号 3. 返回 accessToken + refreshToken 4. 携带 accessToken 5. 转发请求 6. JwtDecoder 自动解析JWT (验证签名、过期时间) 7. 调用 JwtAuthenticationConverter (执行三重安全检查) 8. 查询令牌版本号 返回当前版本号 9. 查询用户封禁状态 返回封禁标记 10. 创建 JwtAuthenticationToken 11. 存入 SecurityContext 12. 返回业务数据 13. RestAuthenticationEntryPoint 返回 401 JSON
文字版 OAuth2 Resource Server 工作流程:
- 用户在认证服务登录,获得 accessToken
- 用户携带 accessToken 访问资源服务的受保护接口
- 资源服务的 OAuth2 Resource Server 过滤器拦截请求
- JwtDecoder 自动完成:解析 JWT、验证签名、检查过期时间
- JwtAuthenticationConverter 执行业务逻辑:令牌类型检查、版本号验证、封禁状态检查
- 验证通过后,创建 JwtAuthenticationToken 并存入 SecurityContext
- 后续业务代码可通过 SecurityContextHolder 获取当前用户信息
核心:JwtAuthenticationConverter 实现(生产级)
这是整个认证流程的最关键组件,也是你的项目中写得最好的部分之一!
java
/**
* JWT认证转换器(请求身份验证网关)
*
* <p>这个类是Spring Security认证流程的核心组件。每当用户访问受保护接口时,
* Spring Security会自动调用此转换器,将HTTP请求头中的JWT令牌转换为
* 系统内部的认证对象(JwtAuthenticationToken)。
*
* <p>在Spring Security工作流程中的位置:
* <pre>
* 1. 用户发送HTTP请求,携带Authorization: Bearer {accessToken}
* 2. Spring Security的OAuth2ResourceServer过滤器拦截请求
* 3. 解析JWT令牌(验证签名、过期时间等)【框架自动完成】
* 4. 调用此转换器进行业务验证(令牌类型、版本号、封禁状态)【你需要写的】
* 5. 转换为JwtAuthenticationToken对象
* 6. 存入SecurityContext,后续业务代码可通过SecurityContextHolder获取
* </pre>
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class JwtAuthenticationConverter implements Converter<Jwt, JwtAuthenticationToken> {
private final JwtTokenService jwtTokenService;
private final RedisTemplate<String, String> redisTemplate;
/**
* 转换JWT令牌为Spring Security认证对象(核心转换逻辑)
*
* <p>三重安全检查(这是生产级系统的标配):
*
* 检查1:令牌类型验证
* - 确保令牌是access token(不能用refresh token访问接口)
* - 防止令牌混用导致的安全问题
*
* 检查2:令牌版本验证(高级特性)
* - 对比JWT中的版本号与Redis中的当前版本号
* - 如果版本号不匹配,说明用户已强制登出(如修改密码、全设备登出)
* - 这是实现"全设备强制登出"功能的关键检查点
*
* 检查3:用户封禁状态验证(高级特性)
* - 检查Redis中的封禁标记(使用Bitmap高效存储)
* - 如果用户被封禁,立即拒绝访问
* - 管理员封禁用户后,被封禁用户的所有令牌立即失效
*/
@Override
public JwtAuthenticationToken convert(Jwt jwt) {
try {
// ==================== 安全检查1:验证令牌类型 ====================
if (!JwtTokenService.TYPE_ACCESS.equals(jwt.getClaimAsString(JwtTokenService.CLAIM_TOKEN_TYPE))) {
log.debug("JWT令牌类型不匹配 - 期望: {}, 实际: {}",
JwtTokenService.TYPE_ACCESS, jwt.getClaimAsString(JwtTokenService.CLAIM_TOKEN_TYPE));
throw new BadCredentialsException("Invalid access token");
}
// ==================== 安全检查2:解析用户ID ====================
Long userId = jwtTokenService.parseSubjectAsUserId(jwt.getSubject());
// ==================== 安全检查3:验证令牌版本(全设备强制登出)====================
long currentVersion = getCurrentTokenVersion(userId);
jwtTokenService.assertTokenVersion(jwt, currentVersion, JwtTokenService.TYPE_ACCESS);
// ==================== 安全检查4:检查用户封禁状态(实时封禁)====================
Boolean banned = redisTemplate.opsForValue().getBit(USER_IS_BANNED_BITMAP_KEY, userId);
if (Boolean.TRUE.equals(banned)) {
throw new BadCredentialsException("User is banned");
}
// ==================== 步骤5:提取用户昵称 ====================
String nickname = jwt.getClaimAsString("nickName");
// ==================== 步骤6:构建认证对象 ====================
JwtUserInfo userInfo = new JwtUserInfo(userId, nickname);
log.debug("JWT认证转换成功 - 用户ID: {}, 昵称: {}", userId, nickname);
return new JwtAuthenticationToken(userInfo, jwt.getTokenValue());
} catch (BadCredentialsException e) {
throw e;
} catch (Exception e) {
log.debug("JWT认证转换失败 - 错误: {}", e.getMessage());
throw new BadCredentialsException("Invalid access token", e);
}
}
/**
* 获取用户当前令牌版本号(版本查询器)
*
* <p>Redis存储格式:
* Key: user:token:version:{userId}
* Value: {versionNumber}(字符串形式的数字)
* 例如:user:token:version:1001 -> "3"
*
* <p>版本号递增场景:
* 1. 用户修改密码 -> 版本号+1
* 2. 用户全设备登出 -> 版本号+1
* 3. 管理员封禁用户 -> 版本号+1(可选)
*/
private long getCurrentTokenVersion(Long userId) {
String raw = redisTemplate.opsForValue().get(USER_TOKEN_VERSION_KEY_PREFIX + userId);
if (!StringUtils.hasText(raw)) {
return 0L; // 初始版本
}
try {
return Long.parseLong(raw);
} catch (NumberFormatException e) {
return 0L; // 容错处理
}
}
}
自定义认证对象:JwtAuthenticationToken
java
/**
* JWT认证令牌类(Spring Security认证对象容器)
*
* <p>这个类是Spring Security框架中的"认证令牌",用于在安全上下文中
* 存储已认证用户的身份信息。你可以把它理解为"通过安检后的通行证"。
*
* <p>核心职责:
* - 持有不可变的用户身份信息(JwtUserInfo)
* - 持有原始JWT令牌字符串(用于审计日志等场景)
* - 提供用户权限信息(默认为ROLE_USER)
* - 作为Spring Security认证状态的载体
*/
public class JwtAuthenticationToken extends AbstractAuthenticationToken {
private final JwtUserInfo userInfo;
private final String rawToken;
public JwtAuthenticationToken(JwtUserInfo userInfo, String rawToken) {
super(extractAuthorities(userInfo));
Assert.notNull(userInfo, "JwtUserInfo cannot be null");
Assert.hasText(rawToken, "rawToken cannot be empty");
this.userInfo = userInfo;
this.rawToken = rawToken;
super.setAuthenticated(true); // 设置为已认证状态
}
/**
* 获取用户ID(快捷方法)- 最常用的方法
*/
public long getUserId() {
return userInfo.userId();
}
/**
* 获取完整用户信息
*/
public JwtUserInfo getUserInfo() {
return userInfo;
}
@Override
public Object getCredentials() {
return rawToken; // 凭证:原始JWT令牌
}
@Override
public Object getPrincipal() {
return userInfo; // 主体:用户信息
}
/**
* 提取用户权限列表(当前简化设计,所有用户都是ROLE_USER)
* 未来可扩展多角色支持
*/
private static Collection<? extends GrantedAuthority> extractAuthorities(JwtUserInfo userInfo) {
return List.of(new SimpleGrantedAuthority("ROLE_USER"));
}
}
认证失败处理器:RestAuthenticationEntryPoint
java
/**
* 认证失败统一处理器(401错误响应器)
*
* <p>当用户认证失败时(未登录、token过期、token无效等),
* Spring Security会自动调用此方法,返回统一的JSON格式错误响应。
*
* <p>触发场景:
* - 用户未登录访问需要认证的接口
* - JWT令牌过期(accessToken已失效)
* - JWT令牌签名无效(被篡改或伪造)
* - JWT令牌类型错误(用refreshToken访问接口)
* - JWT令牌版本不匹配(用户已强制登出)
* - 用户被封禁(管理员在Redis中设置封禁标记)
*/
@Component
@RequiredArgsConstructor
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
private final ObjectMapper objectMapper;
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException {
// 设置 HTTP 状态码和响应头
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
response.setContentType("application/json;charset=UTF-8");
// 构建统一的错误响应体
ApiResponse<Void> body = ApiResponse.fail(ErrorCode.UNAUTHORIZED, null);
// 序列化并写入响应流
response.getWriter().write(objectMapper.writeValueAsString(body));
}
}
面试追问:
Q:OAuth2 Resource Server 模式相比传统 Filter 模式有什么优势?JwtAuthenticationConverter 的职责边界在哪里?
A:
优势对比:
维度 传统 Filter OAuth2 Resource Server 代码量 200+ 行 80-120 行 JWT 解析 手动实现 框架自动 安全性 取决于开发者水平 经过社区验证 可维护性 较差(逻辑耦合) 较好(职责清晰) 扩展性 一般 优秀(支持多种 Token) JwtAuthenticationConverter 的职责边界:
- 应该做的:业务验证(令牌类型、版本号、封禁状态)、提取用户信息、构建认证对象
- 不应该做的:JWT 解析、签名验证、过期检查(这些由框架的 JwtDecoder 完成)
设计原则:让框架做通用的事,让业务代码只关注业务逻辑。
第四篇:高级安全特性------令牌版本控制与封禁管理
线上事故复盘:修改密码后旧 Token 为何依然有效?
这是一个真实的 P0 事故场景(来自某互联网大厂):
时间 :双十一大促期间凌晨 2 点
现象 :大量用户投诉"我已经修改了密码,为什么还能用旧密码登录?"
根因:用户在手机 App 修改密码后,电脑 Web 端的旧 Token 还有 7 天有效期,攻击者利用这个时间窗口窃取了用户数据
核心问题:如何实现"全设备强制登出"?
这是生产环境必须解决的问题,也是面试官最爱问的高频题:
- 用户修改密码 → 所有旧 Token 立即失效
- 管理员封禁用户 → 被封禁用户的所有请求立即被拒绝
- 用户主动全设备登出 → 其他设备的 Token 失效
解决方案:令牌版本控制机制 + Redis Bitmap 封禁管理
令牌版本控制机制原理
签发JWT并写入版本号到Redis
每次请求验证版本号
版本号不匹配
Redis Bitmap标记
抛出401
抛出401
登录成功
正常访问
令牌失效
用户被封禁
重新登录
触发条件:
-
用户修改密码
-
用户主动全设备登出
-
管理员强制下线
触发条件: -
管理员在后台封禁用户
-
系统检测到异常行为自动封禁
文字版令牌版本控制流程:
正常流程:
- 用户登录 → 签发 JWT(包含当前版本号 v=3)→ 存入 Redis(user:token:version:1001 = 3)
- 用户访问接口 → JWT 中的版本号(v=3)== Redis 中的版本号(3)→ ✅ 允许访问
强制登出流程:
- 用户修改密码 → Redis 版本号 +1(user:token:version:1001 = 4)
- 用户用旧 Token 访问 → JWT 中的版本号(v=3)≠ Redis 中的版本号(4)→ ❌ 抛出 BadCredentialsException
- RestAuthenticationEntryPoint 返回 401 → 前端跳转登录页
Redis 数据结构设计
1. 令牌版本号(String 类型)
Key格式: user:token:version:{userId}
Value格式: {versionNumber} (字符串形式的数字)
示例: user:token:version:1001 -> "3"
TTL: 无过期时间(永久有效,直到下次修改)
操作:
- 读取: GET user:token:version:1001
- 写入: SET user:token:version:1001 4
- 递增: INCR user:token:version:1001
Java 代码示例:
java
@Service
@RequiredArgsConstructor
public class TokenVersionService {
private final RedisTemplate<String, String> redisTemplate;
/**
* 递增令牌版本号(触发全设备强制登出)
*/
public long incrementVersion(Long userId) {
String key = USER_TOKEN_VERSION_KEY_PREFIX + userId;
Long version = redisTemplate.opsForValue().increment(key);
return version != null ? version : 1L;
}
/**
* 获取当前令牌版本号
*/
public long getCurrentVersion(Long userId) {
String key = USER_TOKEN_VERSION_KEY_PREFIX + userId;
String value = redisTemplate.opsForValue().get(key);
return StringUtils.hasText(value) ? Long.parseLong(value) : 0L;
}
}
2. 用户封禁状态(Bitmap 类型)
Key格式: user:banned:bitmap
Value格式: 二进制位图(每个 bit 代表一个用户是否被封禁)
示例: user:banned:bitmap -> [01000010...]
含义: 第 2 位和第 8 位为 1,表示 userId=2 和 userId=8 的用户被封禁
操作:
- 封禁用户: SETBIT user:banned:bitmap {userId} 1
- 解封用户: SETBIT user:banned:bitmap {userId} 0
- 查询封禁: GETBIT user:banned:bitmap {userId}
为什么用 Bitmap 而不是 SET?
| 方案 | 存储量(1亿用户) | 查询复杂度 | 适用场景 |
|---|---|---|---|
| SET(每个用户一个 Key) | ~6GB | O(1) | 需要记录封禁时间和原因 |
| Bitmap(所有用户共享一个 Key) | ~12.5MB | O(1) | 只需判断是否被封禁 |
| Hash(HSET) | ~4GB | O(1) | 需要存储额外元数据 |
Bitmap 的优势:
- 极省内存:1亿用户只需 12.5MB(1亿 bit = 12.5MB)
- O(1) 查询:直接定位到对应 bit
- 批量操作:可以一次查询/修改多个用户状态
Java 代码示例:
java
@Service
@RequiredArgsConstructor
public class UserBanService {
private final RedisTemplate<String, String> redisTemplate;
/**
* 封禁用户(立即生效,所有令牌失效)
*/
public void banUser(Long userId) {
// 设置封禁标记
redisTemplate.opsForValue().setBit(USER_IS_BANNED_BITMAP_KEY, userId, true);
// 同时递增令牌版本号(双重保险)
tokenVersionService.incrementVersion(userId);
log.info("用户 {} 已被封禁", userId);
}
/**
* 解封用户
*/
public void unbanUser(Long userId) {
redisTemplate.opsForValue().setBit(USER_IS_BANNED_BITMAP_KEY, userId, false);
log.info("用户 {} 已解封", userId);
}
/**
* 检查用户是否被封禁
*/
public boolean isBanned(Long userId) {
Boolean banned = redisTemplate.opsForValue().getBit(USER_IS_BANNED_BITMAP_KEY, userId);
return Boolean.TRUE.equals(banned);
}
}
三重安全检查的完整时序图
Redis集群 JwtAuthenticationConverter JwtDecoder 资源服务 客户端 Redis集群 JwtAuthenticationConverter JwtDecoder 资源服务 客户端 开始三重安全检查 token_type == "access"? 是access token subject = "1001" 格式合法 未被封禁 三重检查全部通过 GET /api/v1/users/profile Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9... 解析JWT(验证签名、过期时间) JWT有效,返回Jwt对象 convert(Jwt) 检查1:令牌类型验证 检查2:解析用户ID 检查3:获取令牌版本号 GET user:token:version:1001 当前版本号 = 3 对比版本号 JWT中(v=3) == Redis中(3)? 版本匹配 检查4:查询封禁状态 GETBIT user:banned:bitmap 1001 封禁标记 = 0 (未封禁) 返回 JwtAuthenticationToken(userId=1001) 存入 SecurityContext 200 OK {"code":200,"data":{...}}
文字版三重安全检查流程:
- JwtDecoder 自动验证:JWT 签名正确、未过期、格式合法
- 令牌类型检查:确保是 access token(防止用 refresh token 访问接口)
- 用户 ID 解析:从 subject 字段提取,验证格式合法性
- 令牌版本验证:对比 JWT 中版本号与 Redis 中当前版本号
- 封禁状态检查:查询 Redis Bitmap,判断用户是否被封禁
- 全部通过后,创建 JwtAuthenticationToken 并存入 SecurityContext
面试追问:
Q:令牌版本控制机制的优缺点是什么?有没有更好的替代方案?
A:
优点:
- 实现简单,只需要 Redis 的 INCR 操作
- 性能优秀,每次请求只需一次 Redis GET
- 即时生效,修改密码后所有旧 Token 立刻失效
缺点:
- 无法区分"哪个设备"的 Token 失效(只能全设备强制登出)
- 依赖 Redis,如果 Redis 不可用会导致所有请求失败
- 版本号会一直增长(但可以用 Long 类型,几乎不会溢出)
替代方案:
- Token 黑名单:将失效的 Token 加入 Redis Set,每次请求检查是否在黑名单中(适合少量 Token 失效的场景)
- 短生命周期 Token + Refresh Token:AccessToken 有效期设为 15 分钟,Refresh Token 用于刷新(减少强制登出的需求)
- JWK(JSON Web Key Set)轮换:定期更换签名密钥,使旧密钥签发的 Token 失效(适合高安全要求的场景)
推荐组合: 令牌版本控制 + 短生命周期 AccessToken(双保险)
第五篇:授权机制------验证"你能做什么"
权限系统的本质是"权衡的艺术"
你可能觉得这块很基础,但真正难的是------在"安全性"和"易用性"之间找到平衡点。
你想想,一个权限系统,需要考虑多少维度?
| 维度 | 问题 | 典型方案 |
|---|---|---|
| 粒度粗细 | URL 级别 vs 方法级? | 粗(拦截器)+ 细(注解) |
| 数据范围 | 能看自己的数据?还是能看部门数据? | 数据权限切面 |
| 动态性 | 权限写死在代码里?还是从数据库加载? | 动态权限 + 缓存 |
| 性能开销 | 每次请求都查 10 张表? | 本地缓存 + Redis |
| 可维护性 | 改个权限要改代码重启? | 配置化 + 热更新 |
同样一个结论,在不同约束下答案会完全不同:
- 小项目(<10 个接口)→ 硬编码就够了
- 中型系统(100+ 接口)→ RBAC + 注解
- 大型企业(多租户 SaaS)→ ABAC(基于属性的访问控制)
接下来这一步,决定了你的权限系统是否真正落地。
RBAC 权限模型(经典且实用)
多对多
多对多
多对多
1 1 * * * 1 User
+userId: Long
+username: String
+roles: Set<Role>
Role
+roleId: Long
+roleName: String
+permissions: Set<Permission>
Permission
+permissionId: Long
+permissionName: String
+resource: String
+action: String
Resource
+resourceId: String
+resourceName: String
+type: String
文字版 RBAC 模型说明:
五大数据实体:
- User(用户):系统的使用者
- Role(角色):权限的集合(如 ADMIN、USER、MANAGER)
- Permission(权限):对资源的操作许可(如 user:read、order:write)
- Resource(资源):系统中的功能或数据(如用户管理、订单管理)
- Action(操作):动作类型(CRUD:create、read、update、delete)
关系:
- 用户-角色:多对多(一个用户可以有多个角色,一个角色可以分配给多个用户)
- 角色-权限:多对多(一个角色包含多个权限,一个权限可以属于多个角色)
- 权限-资源-操作:多对多(一个权限可以对多个资源进行操作)
URL 级别权限配置(Spring Security 6.x DSL)
java
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(auth -> auth
// ========== 公开接口(不需要认证)==========
.requestMatchers(
"/api/v1/auth/login",
"/api/v1/auth/register",
"/api/v1/auth/refresh-token",
"/actuator/health",
"/swagger-ui/**",
"/v3/api-docs/**"
).permitAll()
// ========== 需要认证的接口(任何已登录用户)==========
.requestMatchers(
"/api/v1/users/profile",
"/api/v1/users/update-password"
).authenticated()
// ========== 需要特定角色的接口 ==========
.requestMatchers("/api/v1/admin/**").hasRole("ADMIN")
.requestMatchers("/api/v1/manager/**").hasAnyRole("MANAGER", "ADMIN")
// ========== 需要特定权限的接口 ==========
.requestMatchers(HttpMethod.POST, "/api/v1/orders/**").hasAuthority("order:create")
.requestMatchers(HttpMethod.DELETE, "/api/v1/users/{id}").hasAuthority("user:delete")
// ========== 默认规则 ==========
.anyRequest().authenticated()
);
return http.build();
}
方法级权限控制(@PreAuthorize 注解)
java
// 1. 开启方法级权限注解
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig {
}
// 2. 在 Service 层使用注解
@Service
public class UserService {
@PreAuthorize("hasRole('ADMIN')")
public List<User> findAllUsers() {
return userRepository.findAll();
}
@PreAuthorize("hasPermission(#id, 'user', 'read') or hasRole('ADMIN')")
public User findById(@PathVariable Long id) {
return userRepository.findById(id).orElse(null);
}
@PreAuthorize("#username == authentication.name or hasRole('ADMIN')")
public User findByUsername(@PathVariable String username) {
return userRepository.findByUsername(username);
}
@DataScope(deptAlias = "d", userAlias = "u")
public List<User> selectUserList(User query) {
return userRepository.selectUserList(query);
}
}
常用权限表达式速查表:
| 表达式 | 说明 | 示例 |
|---|---|---|
hasRole('xxx') |
拥有指定角色(自动加 ROLE_ 前缀) | hasRole('ADMIN') |
hasAnyRole('a','b') |
拥有任一角色 | hasAnyRole('ADMIN','MANAGER') |
hasAuthority('xxx') |
拥有指定权限(不加前缀) | hasAuthority('user:read') |
isAuthenticated() |
已认证(已登录) | - |
isAnonymous() |
匿名用户(未登录) | - |
permitAll() |
允许所有人 | - |
denyAll() |
拒绝所有人 | - |
#param == xxx |
参数判断 | #userId == authentication.principal.id |
面试追问:
Q:URL 级别权限和方法级权限有什么区别?应该优先使用哪种?
A:
URL 级别权限(粗粒度):
- 适用于:API 接口级别的权限控制
- 优点:集中管理,一目了然
- 缺点:无法根据参数动态判断
方法级权限(细粒度):
- 适用于:Service 方法内部的业务逻辑权限控制
- 优点:可以根据参数动态判断(如 #userId == authentication.name)
- 缺点:分散在各处,不易全局查看
推荐做法:
- 第一道防线:URL 级别(拦截非法请求,减轻后端压力)
- 第二道防线:方法级(精确控制业务逻辑)
- 两者结合使用,形成完整的权限防护体系
第六篇:核心组件详解------理解 Spring Security 的"零件"
调试权限问题时的"三连问"
你有没有遇到过这种调试经历?
现象 :接口返回 403,但代码里明明写了
@PreAuthorize("hasRole('ADMIN')")你的排查思路:
- 用户登录了吗?(SecurityContext 里有没有 Authentication?)
- Authentication 对象类型对吗?(是 JwtAuthenticationToken 还是别的?)
- authorities 里有没有 ROLE_ADMIN?(还是只有 ROLE_USER?)
如果你能快速回答这三个问题,说明你已经理解了 Spring Security 的核心组件。
不理解组件,就像开车不懂发动机------出了问题只能干瞪眼。这一篇会帮你彻底搞懂:
SecurityContext在哪存储?怎么传播?Authentication的 principal 和 credentials 分别是什么?- 为什么异步任务里拿不到当前用户信息?
SecurityContext 与 SecurityContextHolder
一句话定义: 安全上下文,存储当前请求的认证信息。
关键点:
- 底层使用
ThreadLocal存储(线程隔离) - 请求结束时自动清理(避免内存泄漏)
- 异步线程需要特殊处理才能传播
实战用法:
java
// 1. 在 Controller 中获取当前用户
@GetMapping("/profile")
public ResponseEntity<ApiResponse<UserProfileDTO>> getProfile() {
// 从 SecurityContext 获取认证对象
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
// 强制转换为自定义的 JwtAuthenticationToken
JwtAuthenticationToken authToken = (JwtAuthenticationToken) authentication;
// 获取用户 ID
Long currentUserId = authToken.getUserId();
// 获取完整用户信息
JwtUserInfo userInfo = authToken.getUserInfo();
// 业务逻辑...
UserProfileDTO profile = userService.getUserProfile(currentUserId);
return ResponseEntity.ok(ApiResponse.success(profile));
}
// 2. 在 Service 层中使用
@Service
@Transactional
public class OrderService {
public Order createOrder(OrderCreateRequest request) {
// 获取当前用户 ID(无侵入式,不需要传参)
Long userId = SecurityUtils.getCurrentUserId();
Order order = new Order();
order.setUserId(userId);
order.setAmount(request.getAmount());
// ...
return orderRepository.save(order);
}
}
// 3. 工具类封装(推荐)
@Component
public class SecurityUtils {
public static Long getCurrentUserId() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication instanceof JwtAuthenticationToken) {
return ((JwtAuthenticationToken) authentication).getUserId();
}
throw new IllegalStateException("未登录或认证对象类型错误");
}
public static JwtUserInfo getCurrentUser() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication instanceof JwtAuthenticationToken) {
return ((JwtAuthenticationToken) authentication).getUserInfo();
}
throw new IllegalStateException("未登录或认证对象类型错误");
}
}
Authentication 接口详解
Authentication 的四大属性:
| 属性 | 类型 | 说明 | 你的代码中的值 |
|---|---|---|---|
principal |
Object | 认证主体("谁通过了认证") | JwtUserInfo(用户信息) |
credentials |
Object | 凭证(用于验证身份的信息) | rawToken(原始 JWT 字符串) |
authorities |
Collection | 权限集合 | [ROLE_USER] |
authenticated |
boolean | 是否已认证 | true(已认证) |
面试追问:
Q:SecurityContext 是怎么存储的?在多线程环境下如何传播?异步任务中如何获取当前用户?
A:
存储方式:
- 默认使用
ThreadLocalSecurityContextHolderStrategy- 将 SecurityContext 存储在 ThreadLocal 中
- 每个线程有自己的独立副本,天然线程安全
多线程传播:
java// 方式1:手动传递(简单粗暴) Runnable task = () -> { SecurityContextHolder.setContext(originalContext); // 业务逻辑... }; // 方式2:使用 DelegatingSecurityContextRunnable(推荐) SecurityContext context = SecurityContextHolder.getContext(); Runnable wrappedTask = new DelegatingSecurityContextRunnable(task, context); executor.execute(wrappedTask); // 方式3:@Async + 配置 DelegatingSecurityContextAsyncTaskExecutor @Async public void asyncMethod() { // 可以直接使用 SecurityContextHolder Long userId = SecurityUtils.getCurrentUserId(); }注意: 如果你发现异步任务中拿不到当前用户信息,大概率是因为 SecurityContext 没有传播过去!
第七篇:实战场景------真实项目中的最佳实践
从代码到生产:三个核心场景的完整实现
理论都懂了,但真正写代码时你会遇到这些问题:
场景一:用户登录
- Token 怎么签发?版本号什么时候递增?
- AccessToken 和 RefreshToken 的有效期分别设多长?
- 登录成功后返回哪些信息给前端?
场景二:修改密码(触发全设备强制登出)
- 密码修改成功后,旧 Token 为什么还能用?
- 令牌版本号在哪里递增?Redis Key 怎么设计?
- 前端收到 401 后怎么处理?是静默刷新还是跳转登录页?
场景三:管理员封禁用户
- 封禁用户后,正在进行的请求怎么处理?
- Redis Bitmap 的内存开销是多少?1 亿用户需要多少空间?
- 解封操作是立即生效还是有延迟?
接下来这一步,我们直接看生产级代码实现。 每个场景都会给出完整的时序图 + 代码示例 + 效果说明。
数据库 Redis集群 认证服务 客户端 数据库 Redis集群 认证服务 客户端 POST /api/v1/auth/login {username, password} 查询用户信息 返回用户信息 校验密码(BCrypt) 生成/更新令牌版本号 INCR user:token:version:{userId} 返回新版本号(如 v=4) 签发 AccessToken(包含版本号 v=4) 签发 RefreshToken(长期有效) 200 OK {accessToken, refreshToken, expiresIn, tokenType:"Bearer"}
代码示例:
java
@Service
@RequiredArgsConstructor
public class AuthService {
private final AuthenticationManager authenticationManager;
private final JwtTokenService jwtTokenService;
private final TokenVersionService tokenVersionService;
/**
* 用户登录
*/
public LoginResponseDTO login(LoginRequestDTO request) {
// 1. 认证(Spring Security 自动校验密码)
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword())
);
// 2. 获取用户信息
UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal();
// 3. 递增令牌版本号(使旧 Token 失效)
long newVersion = tokenVersionService.incrementVersion(userDetails.getId());
// 4. 签发 Token
String accessToken = jwtTokenService.generateAccessToken(userDetails, newVersion);
String refreshToken = jwtTokenService.generateRefreshToken(userDetails);
// 5. 返回
return LoginResponseDTO.builder()
.accessToken(accessToken)
.refreshToken(refreshToken)
.expiresIn(jwtTokenService.getAccessTokenExpirationSeconds())
.tokenType("Bearer")
.build();
}
}
场景二:修改密码(触发全设备强制登出)
java
@RestController
@RequestMapping("/api/v1/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
private final TokenVersionService tokenVersionService;
/**
* 修改密码(会触发全设备强制登出)
*/
@PutMapping("/password")
public ApiResponse<Void> updatePassword(@RequestBody UpdatePasswordRequest request) {
// 1. 获取当前用户 ID
Long currentUserId = SecurityUtils.getCurrentUserId();
// 2. 验证旧密码
userService.validateOldPassword(currentUserId, request.getOldPassword());
// 3. 更新新密码(BCrypt 加密)
userService.updatePassword(currentUserId, request.getNewPassword());
//# 4. 关键步骤:递增令牌版本号(使所有旧 Token 失效)
long newVersion = tokenVersionService.incrementVersion(currentUserId);
log.info("用户 {} 修改密码成功,令牌版本号已更新至 {}", currentUserId, newVersion);
return ApiResponse.success();
}
}
效果:
- 用户在手机上修改密码后
- 电脑上的旧 Token 下次请求时会因版本号不匹配而失败
- 所有设备都需要重新登录
- 安全性极高!
场景三:管理员封禁用户
java
@RestController
@RequestMapping("/api/v1/admin")
@PreAuthorize("hasRole('ADMIN')")
@RequiredArgsConstructor
public class AdminController {
private final UserBanService userBanService;
/**
* 封禁用户(立即生效)
*/
@PostMapping("/users/{userId}/ban")
public ApiResponse<Void> banUser(@PathVariable Long userId, @RequestBody BanReasonDTO reason) {
// 1. 封禁用户(设置 Redis Bitmap 标记)
userBanService.banUser(userId);
// 2. 记录封禁日志(审计追踪)
auditLogService.recordBan(SecurityUtils.getCurrentUserId(), userId, reason.getReason());
// 3. 可选:通知用户(邮件/短信)
notificationService.sendBanNotification(userId);
log.warn("管理员 {} 已封禁用户 {},原因:{}", SecurityUtils.getCurrentUserId(), userId, reason.getReason());
return ApiResponse.success();
}
/**
* 解封用户
*/
@PostMapping("/users/{userId}/unban")
public ApiResponse<Void> unbanUser(@PathVariable Long userId) {
userBanService.unbanUser(userId);
log.info("管理员 {} 已解封用户 {}", SecurityUtils.getCurrentUserId(), userId);
return ApiResponse.success();
}
}
效果:
- 管理员点击"封禁"按钮后
- 该用户的所有请求都会因"User is banned"而返回 401
- 即时生效,无需等待 Token 过期!
第八篇:性能优化与安全最佳实践
性能数据会说话:你的安全系统是否拖慢了接口?
先看一组真实的生产环境监控数据(某日活 100 万用户的系统):
| 指标 | 当前值 | 告警阈值 | 状态 |
|---|---|---|---|
| JWT 解析 P99 | 8ms | < 10ms | ✅ 正常 |
| Redis 查询 P99 | 15ms | < 10ms | ⚠️ 需优化 |
| 认证接口成功率 | 99.2% | > 99.9% | ⚠️ 需关注 |
| 安全相关 CPU 占比 | 35% | < 20% | ❌ 异常 |
你发现问题了吗? Redis 查询耗时偏高,CPU 占比过高------这很可能是因为:
- 每次请求都查了 3 次 Redis(应该只需要 2 次)
- 权限检查没有本地缓存,每次都穿透到数据库
- JWT 没有启用 JWK Set 缓存,频繁远程拉取密钥
安全不能以牺牲性能为代价,但也不能为了性能牺牲安全。如何在两者之间找到平衡点?这一篇会给你完整的优化清单和监控指标体系。
性能优化清单
1. Redis 查询优化(你的代码已经很优秀)
java
// 你的代码:只查询 2 次 Redis(最优)
@Override
public JwtAuthenticationToken convert(Jwt jwt) {
// 查询 1:令牌版本号(GET 操作,O(1) 时间复杂度)
long currentVersion = getCurrentTokenVersion(userId);
// 查询 2:封禁状态(GETBIT 操作,O(1) 时间复杂度)
Boolean banned = redisTemplate.opsForValue().getBit(USER_IS_BANNED_BITMAP_KEY, userId);
// ...
}
优化建议:
- 使用 Pipeline 批量查询(如果 Redis 距离较远)
- 本地缓存热点用户信息(Caffeine/Guava Cache)
- 监控 Redis 慢查询(设置阈值 > 10ms 告警)
2. JWT 解析优化
yaml
# application.yml(Spring Security OAuth2 配置)
spring:
security:
oauth2:
resourceserver:
jwt:
# 使用 JWK Set Endpoint(适合多实例部署)
jwk-set-uri: https://auth-service/.well-known/jwks.json
# 或者使用对称密钥(适合单实例)
# secret-key: ${JWT_SECRET_KEY}
# 缓存 JWK Set(避免频繁远程拉取)
cache-jwk-set: true
优化效果:
- JWK Set 会缓存在本地(默认 5 分钟刷新)
- JWT 签名验证在内存中完成(无需网络 I/O)
- 单次 JWT 解析耗时 < 1ms
3. 权限检查优化
java
// ❌ 低效:每次请求都查数据库
@PreAuthorize("@permissionService.hasPermission(#id, 'read')")
public User findById(Long id) {
return userRepository.findById(id).orElse(null);
}
// 高效:使用本地缓存
@Service
public class CachedPermissionService {
@Cacheable(value = "user:permissions", key = "#userId", unless = "#result == false")
public boolean hasPermission(Long userId, String permission) {
// 先查本地缓存,命中则直接返回
// 未命中才查数据库
return permissionRepository.existsByUserIdAndPermission(userId, permission);
}
}
安全最佳实践清单
必须做的
- HTTPS 强制加密(防止中间人攻击窃取 Token)
- Token 短生命周期(AccessToken 建议 15-30 分钟)
- 密码 BCrypt 加密(强度 10-12)
- 敏感操作二次验证(修改密码、支付等需要输入原密码/短信验证码)
- 审计日志(记录谁在什么时候做了什么操作)
- 速率限制(防止暴力破解,如登录接口限制 5 次/分钟)
推荐做的
- IP 黑名单(检测到异常行为自动封禁 IP)
- 设备指纹(识别同一设备的多账号行为)
- Token 刷新机制(Refresh Token 定期轮换)
- 安全响应头(X-Frame-Options、Content-Security-Policy 等)
绝对不要做的
- 在前端存储敏感信息(Token 应放在 HttpOnly Cookie 或内存中)
- 在日志中打印 Token(即使脱敏也不推荐)
- 使用弱密钥(JWT 密钥至少 256 位随机数)
- 忽略异常(所有认证异常都应该记录日志)
- 硬编码密钥(必须从配置中心或环境变量读取)
监控指标
yaml
# 关键监控指标(Prometheus + Grafana)
metrics:
- name: security_auth_success_total
type: counter
description: 认证成功次数
- name: security_auth_failure_total
type: counter
labels: [reason] # invalid_token, expired, banned, etc.
description: 认证失败次数(按原因分类)
- name: security_jwt_validation_duration_seconds
type: histogram
buckets: [0.001, 0.005, 0.01, 0.05, 0.1]
description: JWT 验证耗时分布
- name: security_redis_query_duration_seconds
type: histogram
buckets: [0.001, 0.005, 0.01, 0.05]
description: Redis 查询耗时分布
- name: security_active_tokens_total
type: gauge
description: 当前活跃 Token 数量
告警规则:
- 认证失败率 > 5%(持续 5 分钟)→ 可能遭受攻击
- JWT 验证 P99 > 50ms → Redis 或 CPU 瓶颈
- Redis 查询 P99 > 10ms → 网络延迟或 Redis 负载过高
总结:Spring Security 6.x 的核心思想
回顾一下我们学到的内容:
架构演进
- Spring Security 5.x → 6.x:从手动 Filter 到 OAuth2 Resource Server
- 核心理念:把通用的交给框架,把业务的留给你
- 代码量减少 50%+,安全性反而更高
认证机制(三重安全检查)
- 令牌类型验证:防止 access/refresh 混用
- 令牌版本控制:实现全设备强制登出
- 封禁状态检查:使用 Redis Bitmap 实现实时封禁
授权机制
- URL 粗粒度 + 方法细粒度 双层防护
- RBAC 模型:用户 → 角色 → 权限 → 资源
- 数据权限:通过切面 + SQL 拼接实现
性能优化
- Redis Bitmap:1 亿用户仅需 12.5MB 内存
- JWT 本地缓存:单次解析 < 1ms
- 监控告警:及时发现异常流量
最佳实践
- Token 短生命周期(15-30 分钟)
- BCrypt 密码加密(强度 10-12)
- 全链路审计日志
- HTTPS 强制加密
附录:快速参考卡片
Spring Security 6.x 配置模板
java
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(
HttpSecurity http,
JwtAuthenticationConverter jwtAuthenticationConverter,
RestAuthenticationEntryPoint restAuthenticationEntryPoint) throws Exception {
http.csrf(csrf -> csrf.disable())
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthenticationConverter))
.authenticationEntryPoint(restAuthenticationEntryPoint)
)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/v1/auth/**").permitAll()
.anyRequest().authenticated()
);
return http.build();
}
}
常见问题排查
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 401 Unauthorized | Token 过期 | 刷新 Token 或重新登录 |
| 401 Unauthorized | 令牌版本不匹配 | 用户已修改密码,需重新登录 |
| 401 Unauthorized | 用户被封禁 | 联系管理员解封 |
| 性能突然下降 | Redis 慢查询 | 检查 Redis 连接池和网络延迟 |
| 无法获取当前用户 | 异步线程未传播 SecurityContext | 使用 DelegatingSecurityContextRunnable |