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. 01JWT 認証フィルターの設定
  2. 02ロールによるコントローラーメソッドの制限
  3. 03401、403、CORS の問題の調査

MCP 対応

README では、より詳しい Spring Security ドキュメントの取得先として `mcp__documentation__fetch_docs` を挙げています。

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/claude-dev-suite/claude-dev-suite --skill spring-security

インストール後、Claude Code で/spring-securityを入力して読み込まれたか確認します。

詳細

Spring Security - クイックリファレンス

このスキルを使用する場合

  • JWT 認証を設定する
  • ロールベースのアクセス制御
  • メソッドレベルのセキュリティ

詳細な知識:包括的なドキュメントについては、technology: spring-security を指定して mcp__documentation__fetch_docs を使用してください。

基本的なパターン

セキュリティ設定
@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());
    }
}
監視メトリクス
メトリクス目標
認証失敗の試行回数急増を監視する
トークンの検証時間< 5ms
レート制限への到達回数< 1%
セキュリティ例外未処理の例外 0 件
チェックリスト
  • JWT シークレットを環境変数から取得する
  • トークンの有効期限を設定する
  • リフレッシュトークンの仕組みを用意する
  • CORS の許可リストを設定する(本番環境ではワイルドカードを使用しない)
  • レート制限を有効にする
  • セキュリティヘッダーを設定する
  • メソッドレベルのセキュリティを設定する(@PreAuthorize)
  • パスワードをエンコードする(BCrypt)
  • HTTPS を強制する
  • セキュリティ統合テストを実施する

このスキルを使用しない場合

  • Spring Boot アプリケーションのセットアップ → spring-boot スキルを使用
  • OAuth2 サーバーの実装 → OAuth2 専用スキルを使用
  • 一般的な暗号技術 → security-expert スキルを使用
  • データベースアクセス → spring-data-jpa スキルを使用
  • REST API のパターン → spring-web スキルを使用

アンチパターン

アンチパターン問題点正しい方法
JWT シークレットのハードコードセキュリティリスク環境変数を使用する
ステートレスでない場合に CSRF を無効にする攻撃に対して脆弱になるJWT(ステートレス)の場合のみ無効にする
本番環境ですべてのアクセスを許可するセキュリティが確保されない適切な認可を設定する
パスワードを平文で保存する重大なセキュリティ上の欠陥BCryptPasswordEncoder を使用する
トークンに有効期限を設定しないトークンが無効にならない適切な有効期限を設定する
すべての CORS オリジンを許可するCSRF の脆弱性特定のオリジンを許可リストに追加する

簡単なトラブルシューティング

問題考えられる原因解決策
401 Unauthorizedトークンがない、または無効Authorization ヘッダーの形式を確認する
403 Forbidden権限が不足しているユーザーのロールと @PreAuthorize を確認する
CORS エラーオリジンが許可されていないCORS 設定にオリジンを追加する
フィルターが実行されないフィルターの順序が誤っているaddFilterBefore の配置を確認する
JWT の解析に失敗するシークレットキーが誤っているJWT_SECRET が一致することを確認する
Authentication が nullフィルターが適用されていないSecurityContextHolder が設定されていることを確認する

参考資料

高度な設定については、mcp__documentation__fetch_docs を参照してください。

コメント