404 Not Found

404 Not Found


nginx

Spring Security基礎

Spring SecurityはJavaセキュリティの業界標準です。フィルタチェーン, 認証, 認可の3つの要素でAPIのセキュリティを守ります。

1. 学ぶ内容


2. プロダクトマネージャーの実話

(1) ペインポイント:保護されていないAPI

プロダクトレビュー会議で, CharlieはOrderFlowのAPIに一切認証がないことを発見しました。誰でもAPIを呼び出して注文を作成したり商品を削除したりできます。さらに悪いことに, BobがActuatorエンドポイントをインターネットに公開しており, スキャナーが本番データベース情報を取得できる状態でした。Charlieは直ちにセキュリティ制御の実装を要求し, Aliceは最も速い解決策を必要としていました。

(2) Spring Securityによる解決策

Spring Securityなら数行の設定でAPIを保護できます。

JAVA
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.csrf(csrf -> csrf.disable())
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/v1/admin/**").hasRole("ADMIN")
                .requestMatchers("/api/v1/orders/**").authenticated()
                .anyRequest().permitAll()
            )
            .httpBasic(Customizer.withDefaults());
        return http.build();
    }
}

(3) 成果

AliceがSpring SecurityでOrderFlowに認証と認可を実装した後, 管理APIはADMINロールを持つユーザーのみアクセス可能, 通常APIはログインが必要, Actuatorエンドポイントは内部ネットワークからのみアクセス可能になりました。Charlieはセキュリティコンプライアンスに満足しました。


3. Spring Securityアーキテクチャ

(1) フィルタチェーン

100%
sequenceDiagram
    participant Client
    participant Chain as SecurityFilterChain
    participant Auth as 認証フィルタ
    participant Authz as 認可フィルタ
    participant Controller

    Client->>Chain: HTTPリクエスト
    Chain->>Auth: 1. 認証
    alt 資格情報が無効
        Auth-->>Client: 401 Unauthorized
    end
    Auth->>Authz: 2. 認可
    alt アクセス拒否
        Authz-->>Client: 403 Forbidden
    end
    Authz->>Controller: 3. Controllerに転送
    Controller-->>Client: 200 OK
主要概念 説明
SecurityFilterChain 順序付けられたフィルタチェーン。各リクエストが順番に通過
Authentication 認証:「あなたは誰か」を確認
Authorization 認可:「何ができるか」を確認
SecurityContext 現在のユーザーの情報を含むセキュリティコンテキスト
GrantedAuthority 権限/ロール情報

(2) コアコンポーネントの関係

コンポーネント 責務 インターフェース
AuthenticationManager 認証マネージャ authenticate()
ProviderManager AuthenticationManagerのデフォルト実装 AuthenticationProviderに委譲
UserDetailsService ユーザー情報の読み込み loadUserByUsername()
PasswordEncoder パスワードエンコーディング encode() / matches()

4. SecurityFilterChainの設定

(1) ▶ サンプル:基本的なセキュリティ設定

JAVA
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable())
            .cors(cors -> cors.configurationSource(corsConfigurationSource()))
            .sessionManagement(session ->
                session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/v1/products/**").permitAll()
                .requestMatchers("/api/v1/orders/**").authenticated()
                .requestMatchers("/api/v1/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .httpBasic(Customizer.withDefaults());
        return http.build();
    }

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowedOrigins(List.of("https://orderflow.example.com"));
        config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
        config.setAllowedHeaders(List.of("*"));
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/api/**", config);
        return source;
    }
}

出力:

TEXT
// 実行成功
設定オプション 説明 REST APIの推奨
CSRF クロスサイトリクエストフォージェリ保護 無効 (ステートレスAPIでは不要)
CORS クロスオリジンリソース共有 許可ドメインを設定
Session セッション管理 STATELESS (ステートレス)
httpBasic HTTP Basic認証 開発/テスト用
formLogin フォームログイン 従来のWebアプリケーション用

5. インメモリユーザー認証

(1) ▶ サンプル:InMemoryUserDetailsManager

JAVA
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public UserDetailsService userDetailsService(PasswordEncoder encoder) {
        UserDetails admin = User.builder()
            .username("alice")
            .password(encoder.encode("admin123"))
            .roles("ADMIN", "CUSTOMER")
            .build();

        UserDetails customer = User.builder()
            .username("bob")
            .password(encoder.encode("customer123"))
            .roles("CUSTOMER")
            .build();

        return new InMemoryUserDetailsManager(admin, customer);
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

出力:

TEXT
// 実行成功
PasswordEncoder セキュリティ 使用場面
BCryptPasswordEncoder 高い (ソルト値を含む) 本番環境で推奨
Argon2PasswordEncoder 最高 (GPU耐性) 高セキュリティ要件
NoOpPasswordEncoder なし (平文) テスト目的のみ
🔒 セキュリティ: 本番環境でNoOpPasswordEncoderは絶対に使用しないでください。BCryptが最低標準, Argon2がより高い標準です。

(2) ▶ サンプル:認証テスト

BASH
# 資格情報なしでアクセス -> 401
curl http://localhost:8080/api/v1/orders

# 顧客の資格情報でアクセス -> 200
curl -u bob:customer123 http://localhost:8080/api/v1/orders

# 顧客で管理エンドポイントにアクセス -> 403
curl -u bob:customer123 http://localhost:8080/api/v1/admin/dashboard

# 管理者で管理エンドポイントにアクセス -> 200
curl -u alice:admin123 http://localhost:8080/api/v1/admin/dashboard

出力:

TEXT
{"status":"ok","data":{}}

6. メソッドレベルの認可

(1) ▶ サンプル:@PreAuthorizeメソッド認可

JAVA
@Service
public class OrderService {

    @Transactional(readOnly = true)
    @PreAuthorize("hasAnyRole('ADMIN', 'CUSTOMER')")
    public Order getOrder(Long orderId) {
        return orderRepository.findById(orderId).orElseThrow();
    }

    @Transactional
    @PreAuthorize("hasRole('ADMIN')")
    public void deleteOrder(Long orderId) {
        orderRepository.deleteById(orderId);
    }

    @Transactional
    @PreAuthorize("hasRole('ADMIN') or #customerId == authentication.principal.id")
    public List<Order> getCustomerOrders(Long customerId) {
        return orderRepository.findByCustomerId(customerId);
    }
}

出力:

TEXT
// 実行成功
アノテーション 機能 使用場面
@PreAuthorize SpEL式, メソッドアクセス前にチェック 最も柔軟, 推奨
@PostAuthorize SpEL式, メソッド実行後にチェック 戻り値に基づいて権限を決定する必要がある場合
@Secured ロールリスト, SpELをサポートしない シンプルなロールチェック
@RolesAllowed JSR-250標準アノテーション フレームワーク間互換性
📌 重要ポイント: メソッドレベルの認可を使用するには, 設定クラスに@EnableMethodSecurityを追加してください。

(2) ▶ サンプル:@EnableMethodSecurityの設定

JAVA
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
    // ... SecurityFilterChainとUserDetailsServiceのBean
}

出力:

TEXT
// 実行成功

7. パス認可ルールのクイックリファレンス

マッチング方法 説明
requestMatchers(String) Antスタイルのパスマッチング /api/v1/orders/**
requestMatchers(HttpMethod, String) HTTPメソッドの制限 POST /api/v1/orders
anyRequest() すべてのリクエストにマッチ 最後に配置してキャッチオール
認可方法 説明
permitAll() 全員に公開
authenticated() 認証が必要
hasRole("ADMIN") ADMINロールが必要
hasAnyRole("A", "B") いずれかのロールが必要
denyAll() すべてのアクセスを拒否

(1) ▶ サンプル:ロール別のAPIアクセス制限

JAVA
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http.csrf(csrf -> csrf.disable())
        .authorizeHttpRequests(auth -> auth
            .requestMatchers(HttpMethod.GET, "/api/v1/products/**").permitAll()
            .requestMatchers(HttpMethod.POST, "/api/v1/products").hasRole("ADMIN")
            .requestMatchers(HttpMethod.PUT, "/api/v1/products/**").hasRole("ADMIN")
            .requestMatchers(HttpMethod.DELETE, "/api/v1/products/**").hasRole("ADMIN")
            .requestMatchers("/api/v1/orders/**").hasAnyRole("ADMIN", "CUSTOMER")
            .requestMatchers("/actuator/**").hasRole("ADMIN")
            .anyRequest().authenticated()
        )
        .httpBasic(Customizer.withDefaults());
    return http.build();
}

出力:

TEXT
// 実行成功

8. 総合サンプル:OrderFlowのセキュリティ設定

JAVA
// SecurityConfig.java
package com.orderflow.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable())
            .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers(HttpMethod.GET, "/api/v1/products/**").permitAll()
                .requestMatchers("/api/v1/orders/**").hasAnyRole("CUSTOMER", "ADMIN")
                .requestMatchers("/api/v1/admin/**").hasRole("ADMIN")
                .requestMatchers("/actuator/health").permitAll()
                .requestMatchers("/actuator/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .httpBasic(Customizer.withDefaults());
        return http.build();
    }

    @Bean
    public UserDetailsService userDetailsService(PasswordEncoder encoder) {
        var admin = User.builder()
            .username("alice").password(encoder.encode("admin123"))
            .roles("ADMIN", "CUSTOMER").build();
        var customer = User.builder()
            .username("bob").password(encoder.encode("pass123"))
            .roles("CUSTOMER").build();
        return new InMemoryUserDetailsManager(admin, customer);
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

// メソッドレベルセキュリティ付きOrderController
@RestController
@RequestMapping("/api/v1/orders")
public class OrderController {

    @GetMapping
    @PreAuthorize("hasAnyRole('CUSTOMER', 'ADMIN')")
    public List<Order> listOrders() { /* ... */ }

    @GetMapping("/{id}")
    @PreAuthorize("hasRole('ADMIN') or @orderSecurity.isOwner(#id, authentication)")
    public Order getOrder(@PathVariable Long id) { /* ... */ }

    @DeleteMapping("/{id}")
    @PreAuthorize("hasRole('ADMIN')")
    public void deleteOrder(@PathVariable Long id) { /* ... */ }
}

❓ よくある質問

Q REST APIでなぜCSRFを無効にするべきですか?
A CSRF保護はCookieとセッションに依存します。REST APIは通常ステートレス (トークンベース認証を使用)でCookieを使用しないため, CSRF攻撃は成功しません。したがってCSRF保護を無効にできます。
Q hasRolehasAuthorityの違いは何ですか?
A hasRole("ADMIN")は自動的に"ROLE_"プレフィックスを追加してROLE_ADMIN権限をチェックします。hasAuthority("ADMIN")はプレフィックスを追加せずに直接ADMIN権限をチェックします。一貫してhasRoleを使用することを推奨します。
Q InMemoryUserDetailsManagerは本番で使えますか?
A いいえ。インメモリユーザー管理は開発とテスト専用です。本番環境ではカスタムUserDetailsServiceを使ってデータベースからユーザー情報を読み込む必要があります。
Q 現在ログインしているユーザーの情報を取得するにはどうすればよいですか?
A 3つの方法があります:1)SecurityContextHolder.getContext().getAuthentication();2)ControllerメソッドにPrincipal principalをインジェクション;3)@AuthenticationPrincipal UserDetails user。3番目の方法を推奨します。
Q Spring Securityフィルタの順序はカスタマイズできますか?
A はい。@OrderアノテーションでSecurityFilterChainの順序を制御できます。複数のSecurityFilterChainを設定して異なるリクエストパスにマッチさせることもできます。
Q httpBasicとformLoginのどちらを選ぶべきですか?
A REST APIにはhttpBasicまたはBearer Tokenを使用してください。従来のWebアプリケーションにはformLoginを使用してください。このレッスンではhttpBasicを入門として使用し, 次のレッスンでJWTに切り替えます。

📖 まとめ


📝 練習問題

  1. 基本問題 (難易度 ⭐):OrderFlowにSpring Securityを設定し, 商品照会はpermitAll, 注文操作はauthenticated, 管理APIはhasRole("ADMIN")を実装してください。HTTP Basic認証とインメモリユーザーでテストしてください。

  2. 応用問題 (難易度 ⭐⭐):@PreAuthorizeメソッドレベル認可を実装してください。ユーザーは自分の注文のみ閲覧でき, ADMINはすべての注文を閲覧できます。ヒント:OrderSecurityヘルパーBeanを作成し, SpELで参照してください。

  3. チャレンジ問題 (難易度 ⭐⭐⭐):カスタムUserDetailsServiceを実装してデータベース (JPA)からユーザーとロール情報を読み込み, InMemoryUserDetailsManagerを置き換えてください。パスワード保存とユーザー登録プロセスのセキュリティ設計について考察してください。

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%