セッションCookie認証のログイン処理の構成
@PostMapping("/signin")
public ResponseEntity<?> signin(
@Valid @RequestBody SignInRequest request,
HttpServletRequest httpRequest
) {
// 入力されたメールアドレスとパスワードでユーザー認証を実行する
Authentication authentication =
authService.authenticate(request.email(), request.password());
// 認証結果を現在のリクエストに紐づける
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(authentication);
SecurityContextHolder.setContext(context);
// 認証状態をセッションに保持し、ログイン状態を維持する
httpRequest.getSession(true).setAttribute(
HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY,
context
);
return ResponseEntity.ok().build();
}
// メールアドレスとパスワードでユーザー認証を実行する
public Authentication authenticate(String email, String password) {
return authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(email, password)
);
}
@Configuration
@EnableWebSecurity
public class SecurityConfig {
// Spring Security のフィルタチェーンを設定する
// CSRF設定やアクセス制御など、HTTPセキュリティのルールを定義する
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler())
)
.securityContext(context -> context
.securityContextRepository(new HttpSessionSecurityContextRepository())
)
.authorizeHttpRequests(auth -> auth
.requestMatchers(AuthPath.SIGNUP, AuthPath.SIGNIN).permitAll()
.anyRequest().authenticated()
);
return http.build();
}
// パスワードのハッシュ化と検証に使用する PasswordEncoder を定義
// BCrypt を使用してパスワードを安全に保存・検証する
@Bean
public PasswordEncoder encoder() {
return new BCryptPasswordEncoder(11);
}
// Spring Security が使用する AuthenticationManager を取得する
// ログイン時の認証処理(AuthenticationProvider など)を管理するコンポーネント
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration configuration
) throws Exception {
return configuration.getAuthenticationManager();
}
}
public class SecurityUserDetailsService implements UserDetailsService {
// ...
// Spring Security の認証処理で呼び出されるメソッド。
// メールアドレスをもとにユーザーをDBから取得し、UserDetailsとして返す。
@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
Account user = accountRepository.findByEmail(email)
.orElseThrow(() ->
new UsernameNotFoundException("User not found"));
return org.springframework.security.core.userdetails.User
.builder()
.username(user.getEmail())
.password(user.getPasswordHash())
.roles(ROLE_USER)
.build();
}
}
ログインAPIで実際に起きている処理
- POST /signin
- AuthenticationManager
- AuthenticationProvider
├ UserDetailsService(DB検索)
└ PasswordEncoder(パスワード検証) - Authentication生成
- SecurityContext(認証情報を現在のリクエストに保持)
- Session(認証情報をセッションに保存)
- Cookie(セッションIDをクライアントに送信)
- 認証維持
2. AuthenticationManager が認証を開始する
ログインAPIでは、次のコードで認証処理を開始しています。
public Authentication authenticate(String email, String password) {
return authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(email, password)
);
}
AuthenticationManager は Spring Security における認証処理の入口となるコンポーネントです。
ここにユーザーの認証情報(メールアドレスとパスワード)を渡すと、Spring Security が内部でユーザーの検証を行います。
ただし、AuthenticationManager 自身が認証処理を行うわけではありません。
実際の認証処理は AuthenticationProvider に委譲される仕組みになっています。
実際のソースコードでも、AuthenticationManager の実装である ProviderManager では次のような処理が行われています。
for (AuthenticationProvider provider : getProviders()) {
result = provider.authenticate(authentication);
}
このコードから分かるように、AuthenticationManager は登録されている AuthenticationProvider を順番に呼び出し、認証処理を委譲しています。
つまり、認証処理の実体は AuthenticationProvider の中で行われます。
3. AuthenticationProvider がユーザー認証を行う
AuthenticationManager から処理を委譲された AuthenticationProvider は、実際のユーザー認証を行います。
Spring Security の標準実装である DaoAuthenticationProvider では、認証処理の中で次のような流れでユーザー認証が行われます。
UserDetails user = retrieveUser(username, authentication);
additionalAuthenticationChecks(user, authentication);
ここでは
-
retrieveUser()でユーザー情報の取得 -
additionalAuthenticationChecks()でパスワード検証
が行われます。
この処理の中で UserDetailsService と PasswordEncoder が利用されます。
UserDetailsService がユーザーを取得する
AuthenticationManager.authenticate() が呼び出されると、Spring Security は認証処理の中でユーザー情報を取得します。この処理を担当するのが UserDetailsService です。
UserDetailsService は、ユーザー識別子(通常はユーザー名やメールアドレス)からユーザー情報を取得するためのインターフェースです。ログイン処理では、入力されたメールアドレスをもとにユーザーを検索し、認証に必要な情報を取得します。
@Override
public UserDetails loadUserByUsername(String email)
このメソッドでは、データベースからユーザーを取得し、Spring Security が扱う UserDetails オブジェクトを生成します。この情報は次のパスワード検証処理で利用されます。
PasswordEncoder がパスワードを検証する
ユーザー情報が取得されると、次に入力されたパスワードが正しいかどうかの検証が行われます。
この処理を担当するのが PasswordEncoder です。
Spring Security では、パスワードは平文ではなく ハッシュ化された状態でデータベースに保存されます。そのためログイン時には、入力されたパスワードと保存されているハッシュを比較して認証を行います。
認証処理では内部で次のような比較が行われます。
passwordEncoder.get().matches(presentedPassword, userDetails.getPassword())
ここでは、入力されたパスワード (presentedPassword) とデータベースに保存されているパスワードハッシュ (userDetails.getPassword()) が一致するかを確認しています。
一致すれば認証は成功と判断され、次のステップとして Authentication オブジェクトが生成されます。
4. Authentication が生成される
ユーザーの取得とパスワード検証が成功すると、Spring Security は認証結果として Authentication オブジェクトを生成します。
Spring Security の標準実装である DaoAuthenticationProvider では、認証成功時に createSuccessAuthentication() が呼ばれ、認証済みの Authentication が生成されます。
return UsernamePasswordAuthenticationToken.authenticated(
principal,
authentication.getCredentials(),
authorities
);
このオブジェクトには、認証済みユーザーの情報や権限情報が含まれており、ログイン処理の結果として AuthenticationManager.authenticate() の戻り値として返されます。
Authentication
├ principal(ユーザー情報)
├ authorities(権限)
└ authenticated=true
この Authentication は、次のステップで SecurityContext に保存され、アプリケーションがユーザーを 認証済みユーザーとして扱えるようになります。
5. SecurityContext にログイン情報を保存する
ユーザー認証が成功すると、AuthenticationManager から 認証済みの Authentication オブジェクトが返されます。
このオブジェクトには、ログインしたユーザーの情報や権限情報が含まれています。
ログインAPIでは、この Authentication を次のコードで SecurityContext に保存しています。
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(authentication);
SecurityContextHolder.setContext(context);
SecurityContext は、現在のリクエストにおける認証情報を保持するコンテキストです。
ここに Authentication を設定することで、アプリケーションはこのユーザーを「認証済み」として扱います。
構造としては次のようになります。
SecurityContext
└ Authentication
├ principal(ユーザー情報)
├ authorities(権限)
└ authenticated=true
ただし、この状態は まだ現在の処理スレッド内に保持されているだけです。
次のリクエストでもログイン状態を維持するためには、これをセッションに保存する必要があります。
6. SecurityContext がセッションに保存される
設定した SecurityContext は、HTTPセッションに保存することで次のリクエストでも利用できるようになります。
今回の実装では、SecurityContext を明示的にセッションへ保存しています。
httpRequest.getSession(true).setAttribute(
HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY,
context
);
このコードにより、セッションには次のような構造で認証情報が保存されます。
HttpSession
└ SPRING_SECURITY_CONTEXT
└ SecurityContext
└ Authentication
Spring Security の formLogin では、認証成功時にフィルタによって自動でセッションへ保存されますが、REST API でログイン処理を自前実装した場合は、この保存処理は自動では行われません。
そのため本実装では、SecurityContext を明示的にセッションへ保存しています。
これにより、以降のリクエストではセッションから SecurityContext が読み出され、ユーザーの認証状態が復元されます。
7. セッションIDがCookieとして送信される
SecurityContext がセッションに保存されると、サーバーはそのセッションを識別するための セッションID をクライアントに送信します。
このセッションIDは Cookie としてレスポンスに含まれます。
例えば、レスポンスヘッダーには次のような Set-Cookie が付与されます。
Set-Cookie: JSESSIONID=abc123...; Path=/; HttpOnly
ブラウザはこのCookieを保存し、次のリクエスト以降で自動的に送信します。
Cookie: JSESSIONID=abc123...
サーバーはこのセッションIDをもとにセッションを取得し、そこに保存されている SecurityContext を読み出すことでユーザーの認証状態を復元します。
8. 次のリクエストで認証が復元される
ブラウザは、保存しているセッションCookieを次のリクエストで自動的に送信します。
Cookie: JSESSIONID=abc123...
サーバーはこの JSESSIONID を使って対応する HTTPセッション を取得し、そこに保存されている SecurityContext を読み出します。
HttpSession
└ SPRING_SECURITY_CONTEXT
└ SecurityContext
└ Authentication
Spring Security は、この SecurityContext に含まれる Authentication を現在のリクエストに設定することで、ユーザーを 認証済みユーザーとして扱います。
これにより、ログイン処理を再度行わなくても認証状態が維持されます。
9. ログアウトとセッション失効
ログアウト処理では、現在の認証情報を削除してログイン状態を終了させます。
Spring Security では、ログアウト時に SecurityContext の削除とセッションの無効化が行われます。
ログアウトAPIでは、次のコードでログアウト処理を実行します。
new SecurityContextLogoutHandler().logout(request, response, authentication);
この処理によってセッションが無効化されると、セッションに保存されていた SecurityContext も削除されます。そのため次のリクエストで JSESSIONID が送信されても、対応するセッションが存在しないため認証情報を復元できません。
また、セッションはログアウトだけでなく一定時間操作がなかった場合にも自動的に失効します。セッションが失効した場合も同様に、次のリクエストではユーザーは未認証として扱われます。
まとめ
Spring Security のセッション認証では、ログイン時に AuthenticationManager によってユーザー認証が行われ、生成された Authentication が SecurityContext に保存されます。
その SecurityContext をセッションに保存することで、ブラウザに送信される JSESSIONID を通じて次のリクエストでもセッションが特定され、ユーザーの認証状態が維持されます。
formLogin とは異なり、REST API でログイン処理を自前実装した場合は、
認証情報は自動でセッションに保存されないため、明示的に保存する必要があります。