Spring Security 認証処理フロー解剖 — PreAuth / UserDetails / PreAuthorize の呼ばれ方
リクエストがどのフィルタを通り、どこでユーザー情報が解決され、どこで @PreAuthorize が効くのか。Spring Security の認証フローを図解しつつ、2024 年版 SecurityFilterChain への移行まで触れる。

なぜ Spring Security の 「フロー」 を理解すべきか
@PreAuthorize("hasAuthority('ADMIN')") と一行書くだけで認可が効く魔法のような仕組みですが、
裏で何が起きているか を知らないと以下でハマります:
- 独自ヘッダー認証を足したい → どの Filter を差し込めば良いのか分からない
@PreAuthorizeが効かない →@EnableGlobalMethodSecurityを書き忘れていたgetPrincipal()が String を返したり User を返したりでテストが辛い- CSRF / CORS / セッション固定化 — 何がデフォルトで有効なのか分からない
本稿では PreAuthenticated 方式 (HTTP ヘッダー等で既にユーザー情報が付いてくる前提) を題材に、 Spring Security の処理フローを 5 ステップで解剖します。
5 ステップで追う認証フロー
1. リクエストが FilterChain に突入
DispatcherServlet より前に Spring Security Filter Chain が噛みます。
SecurityFilterChain には認証系の Filter が順序付きで並んでいて、代表的なもの:
SecurityContextPersistenceFilter(セッションから既存 Context を取り出す)HeaderWriterFilter(X-Frame-Options等のセキュリティヘッダ付与)- 各種認証 Filter (本稿の
AbstractPreAuthenticatedProcessingFilterなど)-AuthorizationFilter(@PreAuthorize判定) ExceptionTranslationFilter(認証例外を 401/403 レスポンスに変換)
2. AbstractPreAuthenticatedProcessingFilter でクレデンシャル取り出し

PreAuthenticated 方式では「認証自体はこのアプリの前で既に済んでいる」前提で、 HTTP ヘッダー等から ユーザー識別子 (principal) と クレデンシャル (credential) を取り出します。
public class HeaderPreAuthFilter extends AbstractPreAuthenticatedProcessingFilter {
@Override
protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) {
return request.getHeader("X-User-Id");
}
@Override
protected Object getPreAuthenticatedCredentials(HttpServletRequest request) {
return request.getHeader("X-User-Token");
}
}
この時点では認証は済んでいません。取り出した情報を Authentication オブジェクトに詰めて、
次ステップの AuthenticationManager に投げます。
3. AuthenticationUserDetailsService でユーザー情報の実体化

PreAuthenticatedAuthenticationProvider が AuthenticationManager から呼ばれ、
最終的に AuthenticationUserDetailsService#loadUserDetails(...) でユーザー情報を解決します。
@Component
public class CustomUserDetailsService
implements AuthenticationUserDetailsService<PreAuthenticatedAuthenticationToken> {
private final UserRepository userRepository;
@Override
public UserDetails loadUserDetails(PreAuthenticatedAuthenticationToken token)
throws UsernameNotFoundException {
String userId = (String) token.getPrincipal();
String credential = (String) token.getCredentials();
var user = userRepository.findActive(userId)
.orElseThrow(() -> new UsernameNotFoundException("not found: " + userId));
if (!credential.equals(user.getToken())) {
throw new BadCredentialsException("token mismatch");
}
var authorities = user.getRoles().stream()
.map(r -> new SimpleGrantedAuthority("ROLE_" + r))
.toList();
return new User(user.getId(), "", authorities);
}
}
認証に成功すると Authentication#isAuthenticated() が true になり、
SecurityContextHolder に保存されます。
4. SecurityFilterChain 設定で各エンドポイントを保護

2017 当時 は WebSecurityConfigurerAdapter を継承していましたが、
Spring Security 5.7+ (Boot 3.x) では廃止され、SecurityFilterChain Bean を返す形式に変わりました:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
SecurityFilterChain filterChain(HttpSecurity http,
HeaderPreAuthFilter preAuthFilter) throws Exception {
return http
.csrf(csrf -> csrf.disable()) // API サーバなら無効化が多い
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/health", "/public/**").permitAll()
.anyRequest().authenticated()
)
.addFilter(preAuthFilter)
.exceptionHandling(e -> e
.authenticationEntryPoint((req, res, ex) -> res.sendError(401))
.accessDeniedHandler((req, res, ex) -> res.sendError(403))
)
.build();
}
}
5. メソッドレベル認可: @PreAuthorize / @PostAuthorize

Controller / Service のメソッドに付けると、呼び出し時に権限評価が走ります。
@RestController
@RequestMapping("/v1/admin")
public class AdminController {
@PreAuthorize("hasRole('ADMIN')")
@GetMapping("/users")
public List<UserDto> list() { ... }
@PreAuthorize("#id == authentication.name or hasRole('ADMIN')")
@DeleteMapping("/users/{id}")
public void delete(@PathVariable String id) { ... }
}
有効化を忘れると静かに失敗します:
@EnableMethodSecurity // Spring Security 6+ (旧: @EnableGlobalMethodSecurity)
@PostAuthorize は処理後に評価する (戻り値を使った認可判定):
@PostAuthorize("returnObject.ownerId == authentication.name")
public Order getOrder(String id) { ... }
落とし穴
1. @PreAuthorize の SpEL は実行時評価 → タイポしてもコンパイル通る
@PreAuthorize("hasRole('ADMIM')") // タイポ! でもビルドは通る
必ず認可の単体テストを書く。@WithMockUser で簡単に:
@Test
@WithMockUser(roles = "ADMIN")
void admin_can_delete() { ... }
2. CSRF が有効なまま API を叩いて 403
ブラウザから叩く REST API は CSRF 有効が原則。
純粋な API サーバ (Bearer Token) なら csrf.disable() を検討。
無効化する場合はそれで何が起きるか理解してから 。
3. SecurityContextHolder の情報が非同期スレッドに引き継がれない
@Async や Reactor スレッド切り替え時に認証情報が失われます。
DelegatingSecurityContextExecutor で包むか、Reactor 版 Spring Security を使う。
4. パスワードを {noop} のまま本番稼働
Spring Security 5+ は PasswordEncoder のプレフィックス指定が必要。
BCryptPasswordEncoder または DelegatingPasswordEncoder を使い、
{noop} プレフィックスは絶対に本番で使わない。
5. フィルタの順序ミスで想定ルートが保護されない
.anyRequest().authenticated() の 「前」 に .requestMatchers(...).permitAll() を置く順序は重要。
Spring Security の設定は 宣言順に評価 されます。
近代化アップデート (2017 → 2024+)
| 当時 | 現代 |
|---|---|
WebSecurityConfigurerAdapter を継承 | SecurityFilterChain Bean |
antMatchers("/path") | requestMatchers("/path") |
authorizeRequests() | authorizeHttpRequests() |
@EnableGlobalMethodSecurity(prePostEnabled = true) | @EnableMethodSecurity |
BCryptPasswordEncoder 直接 | PasswordEncoderFactories.createDelegatingPasswordEncoder() |
| XML / Java Config の一部混在 | 純 Java Config + Lambda DSL が標準 |
まとめ: Spring Security は 「Filter Chain + AuthenticationManager + UserDetails + メソッド認可」 の 4 要素で動いています。
一度フローを追えれば、独自認証方式 (JWT / OAuth2 / mTLS / API Key) も同じ型に収まります。
「なんとなく動いている」 を卒業するために、一度は FilterChain のログを DEBUG にして眺めてみるのを強くおすすめします。
関連記事
- セキュリティ·
CI で脆弱性・ライセンスを自動検出する — OSV / コンテナ Scan / License Scan
ソフトウェアサプライチェーンセキュリティを CI に組み込む実務指針。OSV 脆弱性データベース、コンテナイメージスキャン (Trivy / Grype)、ライセンススキャン、SBOM 生成までを GitHub Actions の例で整理。
- アルゴリズム·
二分木 (Binary Tree) を Java で自作する — BST / 走査 / バランス崩れの実例
二分木、特に二分探索木 (BST) を Generics で実装。挿入・探索・削除の計算量、3 種類の走査、偏った木がなぜ遅くなるかとその対策 (AVL / Red-Black Tree) まで。
- アルゴリズム·
Singleton パターン — 2020 年代の Java では `enum` 一択の理由
伝統的な Singleton の 4 実装と、各々のスレッド安全性・シリアライズ耐性・リフレクション耐性。結論として現代は enum Singleton で終わり。