mcpField
S
spring-security

spring-security

github.com/claude-dev-suite安全4638 次安装2 天前更新
代码仓库

关于 spring-security

供 Spring Security 开发者配置身份验证与授权的速查资料,涵盖 JWT、角色和方法级安全模式,并提供生产检查与故障排查指引。

主要功能

  1. 01JWT 身份验证模式
  2. 02基于角色的访问控制
  3. 03方法级授权
  4. 04CORS 与安全标头
  5. 05安全测试指引

适用场景

  1. 01配置 JWT 身份验证过滤器
  2. 02按角色限制控制器方法
  3. 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 UnauthorizedToken 缺失或无效检查 Authorization 标头格式
403 Forbidden权限不足检查用户角色和 @PreAuthorize
CORS 错误不允许该来源将来源添加到 CORS 配置中
过滤器未执行过滤器顺序错误检查 addFilterBefore 的位置
JWT 解析失败密钥错误确认 JWT_SECRET 匹配
Authentication 为 null未应用过滤器确保已设置 SecurityContextHolder

延伸阅读

有关高级配置:mcp__documentation__fetch_docs

评论

支持 Markdown