关于 spring-security
供 Spring Security 开发者配置身份验证与授权的速查资料,涵盖 JWT、角色和方法级安全模式,并提供生产检查与故障排查指引。
主要功能
- 01JWT 身份验证模式
- 02基于角色的访问控制
- 03方法级授权
- 04CORS 与安全标头
- 05安全测试指引
适用场景
- 01配置 JWT 身份验证过滤器
- 02按角色限制控制器方法
- 03排查 401、403 与 CORS 问题
MCP 相关
README 提及可通过 `mcp__documentation__fetch_docs` 获取更全面的 Spring Security 文档。
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ claude-dev-suite/ claude-dev-suite --skill spring-security安装后在 Claude Code 中输入/spring-security确认已加载。
详细介绍
Spring Security - 快速参考
何时使用此技能
- 配置 JWT 身份验证
- 基于角色的访问控制
- 方法级安全
深入了解:使用
mcp__documentation__fetch_docs,并将 technology 设为:spring-security,获取完整文档。
基本模式
安全配置
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthFilter;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.csrf(AbstractHttpConfigurer::disable)
.cors(Customizer.withDefaults())
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/v1/auth/**").permitAll()
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
.requestMatchers("/api/v1/admin/**").hasRole("ADMIN")
.anyRequest().authenticated())
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
JWT 过滤器
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtService jwtService;
private final UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain chain) throws Exception {
String authHeader = request.getHeader("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
chain.doFilter(request, response);
return;
}
String jwt = authHeader.substring(7);
String email = jwtService.extractUsername(jwt);
if (email != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails user = userDetailsService.loadUserByUsername(email);
if (jwtService.isTokenValid(jwt, user)) {
var auth = new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(auth);
}
}
chain.doFilter(request, response);
}
}
方法安全
@PreAuthorize("hasRole('ADMIN')")
@DeleteMapping("/{id}")
public void delete(@PathVariable Long id) { ... }
@PreAuthorize("#id == authentication.principal.id or hasRole('ADMIN')")
@GetMapping("/{id}/profile")
public UserProfile getProfile(@PathVariable Long id) { ... }
UserDetails
public class UserPrincipal implements UserDetails {
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of(new SimpleGrantedAuthority("ROLE_" + role.name()));
}
}
常见注解
| 注解 | 用途 |
|---|---|
@PreAuthorize | 在方法执行前检查 |
hasRole('X') | 需要指定角色 |
hasAnyRole | 需要任一指定角色 |
应避免的反模式
- 无状态模式下才禁用 CSRF
- 不要忘记为前端配置 CORS
- 不要硬编码 JWT 密钥
生产就绪
JWT 服务
@Service
@RequiredArgsConstructor
public class JwtService {
@Value("${security.jwt.secret-key}")
private String secretKey;
@Value("${security.jwt.expiration}")
private long jwtExpiration;
@Value("${security.jwt.refresh-expiration}")
private long refreshExpiration;
public String generateToken(UserDetails userDetails) {
return buildToken(userDetails, jwtExpiration);
}
public String generateRefreshToken(UserDetails userDetails) {
return buildToken(userDetails, refreshExpiration);
}
private String buildToken(UserDetails userDetails, long expiration) {
return Jwts.builder()
.setSubject(userDetails.getUsername())
.claim("roles", userDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.toList())
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + expiration))
.signWith(getSigningKey(), SignatureAlgorithm.HS256)
.compact();
}
public boolean isTokenValid(String token, UserDetails userDetails) {
try {
final String username = extractUsername(token);
return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
} catch (ExpiredJwtException | MalformedJwtException e) {
return false;
}
}
private SecretKey getSigningKey() {
byte[] keyBytes = Decoders.BASE64.decode(secretKey);
return Keys.hmacShaKeyFor(keyBytes);
}
}
CORS 配置
@Configuration
public class CorsConfig {
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.setAllowedOrigins(List.of(
"https://yourdomain.com",
"https://app.yourdomain.com"
));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
config.setAllowedHeaders(List.of("*"));
config.setExposedHeaders(List.of("Authorization"));
config.setMaxAge(3600L);
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
}
速率限制
@Component
public class RateLimitingFilter extends OncePerRequestFilter {
private final Cache<String, AtomicInteger> requestCounts = Caffeine.newBuilder()
.expireAfterWrite(1, TimeUnit.MINUTES)
.build();
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain chain) throws Exception {
String clientIP = getClientIP(request);
AtomicInteger count = requestCounts.get(clientIP, k -> new AtomicInteger(0));
if (count.incrementAndGet() > 100) {
response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
response.getWriter().write("{\"error\": \"Rate limit exceeded\"}");
return;
}
chain.doFilter(request, response);
}
private String getClientIP(HttpServletRequest request) {
String xForwardedFor = request.getHeader("X-Forwarded-For");
if (xForwardedFor != null && !xForwardedFor.isEmpty()) {
return xForwardedFor.split(",")[0].trim();
}
return request.getRemoteAddr();
}
}
安全标头
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
// ... existing config
.headers(headers -> headers
.contentSecurityPolicy(csp -> csp
.policyDirectives("default-src 'self'; frame-ancestors 'none'"))
.frameOptions(frame -> frame.deny())
.xssProtection(xss -> xss.block(true))
.contentTypeOptions(Customizer.withDefaults())
.referrerPolicy(referrer -> referrer
.policy(ReferrerPolicyHeaderWriter.ReferrerPolicy.STRICT_ORIGIN_WHEN_CROSS_ORIGIN)))
.build();
}
安全测试
@WebMvcTest(UserController.class)
@Import(SecurityConfig.class)
class UserControllerSecurityTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserService userService;
@Test
void unauthenticated_shouldReturn401() throws Exception {
mockMvc.perform(get("/api/v1/users"))
.andExpect(status().isUnauthorized());
}
@Test
@WithMockUser(roles = "ADMIN")
void adminEndpoint_withAdminRole_shouldSucceed() throws Exception {
mockMvc.perform(delete("/api/v1/admin/users/1"))
.andExpect(status().isOk());
}
@Test
@WithMockUser(roles = "USER")
void adminEndpoint_withUserRole_shouldReturn403() throws Exception {
mockMvc.perform(delete("/api/v1/admin/users/1"))
.andExpect(status().isForbidden());
}
}
监控指标
| 指标 | 目标 |
|---|---|
| 身份验证失败次数 | 监控突增情况 |
| Token 验证时间 | < 5ms |
| 触发速率限制的次数 | < 1% |
| 安全异常 | 0 个未处理 |
检查清单
- JWT 密钥来自环境变量
- 已配置 Token 过期时间
- 刷新 Token 机制
- CORS 白名单(生产环境不使用通配符)
- 已启用速率限制
- 已配置安全标头
- 方法级安全(@PreAuthorize)
- 密码编码(BCrypt)
- 已强制使用 HTTPS
- 安全集成测试
何时不使用此技能
- Spring Boot 应用设置 → 使用
spring-boot技能 - OAuth2 服务端实现 → 使用 OAuth2 专用技能
- 通用密码学 → 使用
security-expert技能 - 数据库访问 → 使用
spring-data-jpa技能 - REST API 模式 → 使用
spring-web技能
反模式
| 反模式 | 问题所在 | 正确做法 |
|---|---|---|
| 硬编码 JWT 密钥 | 安全风险 | 使用环境变量 |
| 无状态模式下禁用 CSRF | 容易受到攻击 | 仅在使用 JWT(无状态)时禁用 |
| 在生产环境允许所有访问 | 没有安全性 | 配置适当的授权 |
| 以纯文本存储密码 | 严重的安全缺陷 | 使用 BCryptPasswordEncoder |
| Token 永不过期 | Token 永远不会失效 | 设置适当的过期时间 |
| 允许所有 CORS 来源 | CSRF 漏洞 | 将特定来源列入白名单 |
快速故障排除
| 问题 | 可能原因 | 解决方案 |
|---|---|---|
| 401 Unauthorized | Token 缺失或无效 | 检查 Authorization 标头格式 |
| 403 Forbidden | 权限不足 | 检查用户角色和 @PreAuthorize |
| CORS 错误 | 不允许该来源 | 将来源添加到 CORS 配置中 |
| 过滤器未执行 | 过滤器顺序错误 | 检查 addFilterBefore 的位置 |
| JWT 解析失败 | 密钥错误 | 确认 JWT_SECRET 匹配 |
| Authentication 为 null | 未应用过滤器 | 确保已设置 SecurityContextHolder |
延伸阅读
有关高级配置:
mcp__documentation__fetch_docs
- 技术:
spring-security,主题:basics- Spring Security 文档
评论
支持 Markdown