Spring Security基礎
Spring SecurityはJavaセキュリティの業界標準です。フィルタチェーン, 認証, 認可の3つの要素でAPIのセキュリティを守ります。
1. 学ぶ内容
- Spring Securityフィルタチェーンアーキテクチャと
SecurityFilterChainの設定 HttpSecurityの設定:パス認可, CSRF, CORS- インメモリユーザー認証
UserDetailsServiceとPasswordEncoder @PreAuthorize/@Securedメソッドレベルの認可- Aliceが注文管理APIへのアクセスをADMINロールのみに制限
2. プロダクトマネージャーの実話
(1) ペインポイント:保護されていないAPI
プロダクトレビュー会議で, CharlieはOrderFlowのAPIに一切認証がないことを発見しました。誰でもAPIを呼び出して注文を作成したり商品を削除したりできます。さらに悪いことに, BobがActuatorエンドポイントをインターネットに公開しており, スキャナーが本番データベース情報を取得できる状態でした。Charlieは直ちにセキュリティ制御の実装を要求し, Aliceは最も速い解決策を必要としていました。
(2) Spring Securityによる解決策
Spring Securityなら数行の設定でAPIを保護できます。
@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) フィルタチェーン
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) ▶ サンプル:基本的なセキュリティ設定
@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;
}
}
出力:
// 実行成功
| 設定オプション | 説明 | REST APIの推奨 |
|---|---|---|
| CSRF | クロスサイトリクエストフォージェリ保護 | 無効 (ステートレスAPIでは不要) |
| CORS | クロスオリジンリソース共有 | 許可ドメインを設定 |
| Session | セッション管理 | STATELESS (ステートレス) |
| httpBasic | HTTP Basic認証 | 開発/テスト用 |
| formLogin | フォームログイン | 従来のWebアプリケーション用 |
5. インメモリユーザー認証
(1) ▶ サンプル:InMemoryUserDetailsManager
@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();
}
}
出力:
// 実行成功
| PasswordEncoder | セキュリティ | 使用場面 |
|---|---|---|
BCryptPasswordEncoder |
高い (ソルト値を含む) | 本番環境で推奨 |
Argon2PasswordEncoder |
最高 (GPU耐性) | 高セキュリティ要件 |
NoOpPasswordEncoder |
なし (平文) | テスト目的のみ |
NoOpPasswordEncoderは絶対に使用しないでください。BCryptが最低標準, Argon2がより高い標準です。
(2) ▶ サンプル:認証テスト
# 資格情報なしでアクセス -> 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
出力:
{"status":"ok","data":{}}
6. メソッドレベルの認可
(1) ▶ サンプル:@PreAuthorizeメソッド認可
@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);
}
}
出力:
// 実行成功
| アノテーション | 機能 | 使用場面 |
|---|---|---|
@PreAuthorize |
SpEL式, メソッドアクセス前にチェック | 最も柔軟, 推奨 |
@PostAuthorize |
SpEL式, メソッド実行後にチェック | 戻り値に基づいて権限を決定する必要がある場合 |
@Secured |
ロールリスト, SpELをサポートしない | シンプルなロールチェック |
@RolesAllowed |
JSR-250標準アノテーション | フレームワーク間互換性 |
@EnableMethodSecurityを追加してください。
(2) ▶ サンプル:@EnableMethodSecurityの設定
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
// ... SecurityFilterChainとUserDetailsServiceのBean
}
出力:
// 実行成功
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アクセス制限
@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();
}
出力:
// 実行成功
8. 総合サンプル:OrderFlowのセキュリティ設定
// 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) { /* ... */ }
}
❓ よくある質問
hasRoleとhasAuthorityの違いは何ですか?hasRole("ADMIN")は自動的に"ROLE_"プレフィックスを追加してROLE_ADMIN権限をチェックします。hasAuthority("ADMIN")はプレフィックスを追加せずに直接ADMIN権限をチェックします。一貫してhasRoleを使用することを推奨します。SecurityContextHolder.getContext().getAuthentication();2)ControllerメソッドにPrincipal principalをインジェクション;3)@AuthenticationPrincipal UserDetails user。3番目の方法を推奨します。📖 まとめ
- Spring Securityコアアーキテクチャ:SecurityFilterChain → 認証 → 認可
SecurityFilterChainでパス認可ルールを設定:permitAll / authenticated / hasRole- REST APIのベストプラクティス:CSRF無効, STATELESSセッション, HTTP BasicまたはBearerトークン
@PreAuthorize+ SpEL:式を使ったメソッドレベルのきめ細かい認可の実装BCryptPasswordEncoderがパスワードエンコーディングの最低セキュリティ標準- InMemoryUserDetailsManagerは開発用のみ。本番環境ではカスタムUserDetailsServiceの実装が必須
📝 練習問題
-
基本問題 (難易度 ⭐):OrderFlowにSpring Securityを設定し, 商品照会は
permitAll, 注文操作はauthenticated, 管理APIはhasRole("ADMIN")を実装してください。HTTP Basic認証とインメモリユーザーでテストしてください。 -
応用問題 (難易度 ⭐⭐):
@PreAuthorizeメソッドレベル認可を実装してください。ユーザーは自分の注文のみ閲覧でき, ADMINはすべての注文を閲覧できます。ヒント:OrderSecurityヘルパーBeanを作成し, SpELで参照してください。 -
チャレンジ問題 (難易度 ⭐⭐⭐):カスタム
UserDetailsServiceを実装してデータベース (JPA)からユーザーとロール情報を読み込み,InMemoryUserDetailsManagerを置き換えてください。パスワード保存とユーザー登録プロセスのセキュリティ設計について考察してください。



