Chuyển đến nội dung chính

レッスン 12: メソッド セキュリティ、CORS、CSRF、セキュリティのベスト プラクティス

@PreAuthorize、@PostAuthorize、@Secured SpEL 式。 CORS 構成、SPA の CSRF 保護。レート制限、セキュリティ ヘッダー、OWASP Top 10 防止。

💻 プログラミング — レッスン 11 レッスン 12: メソッド セキュリティ、CORS、CSRF、および セキュリティのベストプラクティス

Spring Boot 4: 基本から上級まで

パート 3: アプリケーションのセキュリティ

xdev.asia

はじめに

この記事では、基本的な認証と認可を理解した後、運用環境でアプリケーションを保護するためのメソッド レベルのセキュリティ、SPA の CORS 構成、OWASP トップ 10 に基づくセキュリティのベスト プラクティスについて詳しく説明します。


1. メソッドレベルのセキュリティ

1.1 メソッドセキュリティを有効にする

@Configuration
@EnableMethodSecurity // Spring Boot 4.x
public class MethodSecurityConfig { }

1.2 @PreAuthorize — 実行前にチェックする

@Service
public class ArticleService {

    // Chỉ ADMIN mới gọi được
    @PreAuthorize("hasRole('ADMIN')")
    public void deleteArticle(Long id) { }

    // ADMIN hoặc EDITOR
    @PreAuthorize("hasAnyRole('ADMIN', 'EDITOR')")
    public ArticleResponse updateArticle(Long id, UpdateArticleRequest request) { }

    // Kiểm tra authority cụ thể
    @PreAuthorize("hasAuthority('WRITE_ARTICLES')")
    public ArticleResponse createArticle(CreateArticleRequest request) { }

    // SpEL: Kiểm tra user hiện tại là owner
    @PreAuthorize("#username == authentication.name or hasRole('ADMIN')")
    public UserResponse getUserProfile(String username) { }

    // SpEL: Truy cập tham số method
    @PreAuthorize("@articleSecurity.isOwner(#articleId, authentication)")
    public void editArticle(Long articleId, UpdateArticleRequest request) { }
}

1.3 @PostAuthorize — 実行後のチェック

// Kiểm tra return value
@PostAuthorize("returnObject.owner == authentication.name or hasRole('ADMIN')")
public ArticleResponse getArticle(Long id) {
    return articleRepository.findById(id)
        .map(ArticleResponse::from)
        .orElseThrow(() -> new ResourceNotFoundException("Article", "id", id));
}

1.4 カスタムセキュリティ式

@Component("articleSecurity")
public class ArticleSecurity {

    private final ArticleRepository articleRepository;

    public ArticleSecurity(ArticleRepository articleRepository) {
        this.articleRepository = articleRepository;
    }

    public boolean isOwner(Long articleId, Authentication authentication) {
        return articleRepository.findById(articleId)
            .map(article -> article.getAuthor().getUsername()
                .equals(authentication.getName()))
            .orElse(false);
    }
}

2. CORS 構成

2.1 なぜ CORS が必要なのでしょうか?

フロントエンド (React、Next.js) が実行されるとき localhost:3000 上記のバックエンド localhost:8080、ブラウザは同一オリジン ポリシーに従ってクロスオリジン リクエストをブロックします。 CORS を使用すると、サーバーはどのドメインにアクセスするかを指定できます。

2.2 CORS 構成

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http)
        throws Exception {
    http
        .cors(cors -> cors.configurationSource(corsConfigurationSource()))
        // ...other config
    ;
    return http.build();
}

@Bean
public CorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration config = new CorsConfiguration();

    config.setAllowedOrigins(List.of(
        "http://localhost:3000",          // React dev server
        "https://myapp.example.com"       // Production frontend
    ));

    config.setAllowedMethods(List.of(
        "GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"
    ));

    config.setAllowedHeaders(List.of(
        "Authorization", "Content-Type", "X-Requested-With"
    ));

    config.setExposedHeaders(List.of(
        "X-Total-Count", "X-Page-Number"
    ));

    config.setAllowCredentials(true);
    config.setMaxAge(3600L); // Cache preflight 1 hour

    UrlBasedCorsConfigurationSource source =
        new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/api/**", config);
    return source;
}

3. CSRF 保護

3.1 CSRF はどのような場合に必要ですか?

  • 純粋な REST API (JWT/トークン認証): CSRF (トークンベースの自己保護) を無効にする
  • サーバーでレンダリングされたページ (Thymeleaf + フォーム ログイン): CSRF を有効にする
  • SPA + Cookie ベースの認証: Cookie リポジトリを使用して CSRF を有効にします
// REST API: Disable CSRF
http.csrf(csrf -> csrf.disable());

// SPA với cookie auth: CSRF via cookie
http.csrf(csrf -> csrf
    .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
    .csrfTokenRequestHandler(new SpaCsrfTokenRequestHandler())
);

4. セキュリティヘッダー

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http)
        throws Exception {
    http
        .headers(headers -> headers
            .contentTypeOptions(Customizer.withDefaults())  // X-Content-Type-Options: nosniff
            .frameOptions(frame -> frame.deny())             // X-Frame-Options: DENY
            .httpStrictTransportSecurity(hsts -> hsts        // HSTS
                .maxAgeInSeconds(31536000)
                .includeSubDomains(true))
            .contentSecurityPolicy(csp -> csp
                .policyDirectives("default-src 'self'; script-src 'self'"))
        );
    return http.build();
}

5. レート制限

// Simple in-memory rate limiter (production: dùng Redis)
@Component
public class RateLimitFilter extends OncePerRequestFilter {

    private final Map<String, List<Long>> requestCounts =
        new ConcurrentHashMap<>();
    private static final int MAX_REQUESTS = 100;
    private static final long TIME_WINDOW_MS = 60_000; // 1 phút

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                     HttpServletResponse response,
                                     FilterChain filterChain)
            throws ServletException, IOException {

        String clientIp = request.getRemoteAddr();
        long now = System.currentTimeMillis();

        requestCounts.computeIfAbsent(clientIp, k -> new ArrayList<>());
        List<Long> timestamps = requestCounts.get(clientIp);

        // Remove expired entries
        timestamps.removeIf(t -> now - t > TIME_WINDOW_MS);

        if (timestamps.size() >= MAX_REQUESTS) {
            response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
            response.getWriter().write("Rate limit exceeded");
            return;
        }

        timestamps.add(now);
        filterChain.doFilter(request, response);
    }
}

6. OWASP トップ 10 — Spring Boot のチェックリスト

#脅威Spring Boot ソリューション
A01壊れたアクセス制御@PreAuthorize、URL ベースの認証
A02暗号化の失敗BCrypt、AES暗号化、TLS
A03注射パラメータ化されたクエリ (JPA)、入力検証
A04安全でない設計脅威モデリング、最小権限
A05セキュリティの設定ミスセキュリティヘッダー、詳細エラーを無効にする
A06脆弱なコンポーネントdependabot、スプリングブート依存関係管理
A07認証の失敗強力なパスワード、JWT 有効期限、ブルート フォース保護
A08データ整合性の失敗CSRF 保護、署名付き JWT
A09ログの失敗構造化されたログ、監査証跡
A10SSRFURL 検証、外部呼び出しの許可リスト

概要

  • メソッド セキュリティ (@PreAuthorize、@PostAuthorize) により、SpEL 式を使用したサービス層での承認が可能になります
  • 正しく構成された CORS により、フロントエンドのクロスオリジン アクセスのオリジン、メソッド、ヘッダーが可能になります
  • セキュリティ ヘッダー (HSTS、CSP、X-Frame-Options) は、XSS、クリックジャッキング、およびインジェクション攻撃から保護します

演習

  1. セキュリティ メソッドの実装: @PreAuthorize は、編集/削除を許可する前に、ユーザーがリソースの所有者であることを確認します。
  2. CORS 構成により、React フロントエンド (localhost:3000 および運用ドメイン) が API にアクセスできるようになります
  3. レート制限ミドルウェアの実装: IP ごとに最大 100 リクエスト/分、429 個の多すぎるリクエストを返します