主要コンポーネント
SecurityContext:認証情報(Authentication)を保持するコンテキストオブジェクトSecurityContextHolder:コンテキストオブジェクトを取得するための静的ユーティリティクラスAuthentication:認証オブジェクトのデータ形式を定義するインターフェースAuthenticationManager:Authenticationを検証し、認証後のオブジェクトを返すインターフェース
SecurityContextオブジェクトは、認証されたオブジェクトのデータを格納する責任を担います。
public interface SecurityContext extends Serializable {
// 認証情報を取得
Authentication get認証情報();
// 認証情報を設定
void set認証情報(Authentication 認証情報);
}
主な役割は、getとset操作です。
SecurityContextHolderは、コンテキストオブジェクトを取得する静的ユーティリティクラスです。現在のユーザー情報、認証状態、ロールや権限などがここに保存されます。
public class SecurityContextHolder {
// コンテキストデータをクリア
public static void clearContext() {
strategy.clearContext();
}
// コンテキストデータを取得
public static SecurityContext getContext() {
return strategy.getContext();
}
// コンテキストデータを設定
public static void setContext(SecurityContext context) {
strategy.setContext(context);
}
}
主な役割は、SecurityContextの取得と設定です。
Authenticationインターフェースは、認証オブジェクトのデータ形式を定義します。
public interface Authentication extends Principal, Serializable {
// ユーザーの権限を取得
Collection extends GrantedAuthority> get権限();
// ユーザーが提出した証明情報(パスワードなど)
Object get認証情報();
// ユーザーの追加情報(ユーザーテーブルの情報など)
Object get詳細();
// ユーザーID情報を取得。未認証時はユーザー名、認証後はUserDetails
Object get主体();
// 現在の認証状態を取得
boolean is認証済み();
// 認証状態を設定
void set認証済み(boolean is認証済み) throws IllegalArgumentException;
}
Authenticationは、Spring Securityにおける認証データの形式を定義するものです。
AuthenticationManagerは認証の核心インターフェースで、未認証のAuthenticationを受け取り、認証後のAuthenticationを返します。デフォルトの実装クラスはProviderManagerです。
public interface AuthenticationManager {
Authentication authenticate(Authentication 認証情報)
throws AuthenticationException;
}
ProviderManagerはAuthenticationManagerのデフォルト実装クラスです。多くのユーティリティクラスはこのProviderManagerを中心に構築されており、AuthenticationProviderインターフェースが派生しています。
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
// 複数の認証方式を保持するAuthenticationProviderリスト。デリゲートパターンの応用。
// 認証のエントリーポイントは常に一つ:AuthenticationManager
// 例:ユーザー名+パスワード、メール+パスワード、電話番号+パスワードなど
// デフォルトでは、一つのProviderによる認証成功でログイン成功とみなされます。
private List<AuthenticationProvider> プロバイダリスト = Collections.emptyList();
public Authentication authenticate(Authentication 認証情報)
throws AuthenticationException {
Class extends Authentication> チェック対象 = 認証情報.getClass();
AuthenticationException 最終例外 = null;
Authentication 結果 = null;
// プロバイダリストを順番に認証し、成功したら即座に返す。
// 失敗した場合はnullを返し、次のProviderが試行する。
// 全てのProviderが失敗した場合は、ProviderNotFoundExceptionをスロー。
for (AuthenticationProvider プロバイダ : getProviders()) {
// このsupportsメソッドで、Providerが現在の認証オブジェクトをサポートするか判断。
if (!プロバイダ.supports(チェック対象)) {
continue;
}
try {
結果 = プロバイダ.authenticate(認証情報);
if (結果 != null) {
copyDetails(認証情報, 結果);
break;
}
}
...
catch (AuthenticationException e) {
最終例外 = e;
}
}
// 認証情報があれば直接返す
if (結果 != null) {
if (eraseCredentialsAfterAuthentication
&& (結果 instanceof CredentialsContainer)) {
// パスワードを消去
((CredentialsContainer) 結果).eraseCredentials();
}
// ログイン成功イベントを発行
eventPublisher.publishAuthenticationSuccess(結果);
return 結果;
}
...
// ここまで到達した場合は認証失敗。例外情報をラップしてスロー。
if (最終例外 == null) {
最終例外 = new ProviderNotFoundException(messages.getMessage(
"ProviderManager.providerNotFound",
new Object[] { チェック対象.getName() },
"No AuthenticationProvider found for {0}"));
}
prepareException(最終例外, 認証情報);
throw 最終例外;
}
}
AuthenticationProviderインターフェースには二つのメソッドがあります。
public interface AuthenticationProvider {
// 認証処理
Authentication authenticate(Authentication 認証情報)
throws AuthenticationException;
// このProviderが現在の認証オブジェクトをサポートするか判断。
boolean supports(Class> 認証クラス);
}
入力(証明情報)がAuthenticationProvider.authenticate()メソッドに渡され、出力(主体)が返されます。両方ともAuthenticationにカプセル化されています。
入力(証明情報) ---->
AuthenticationProvider.authenticate()
出力(主体)<-------
また、複数の認証方式をサポートできます。
これらのAuthenticationProviderは何をしますか?まず、ユーザー名でデータベースからユーザーデータを取得します。そのため、UserDetailService.loadUserByUserName()が使用されます。このUserDetailServiceはユーザーデータをAuthenticationに埋め込みます(Authentication.getCredentials())。
最後に、他のフィルタもこの認証済みのAuthenticationにアクセスできるように、Spring Securityはそれをコンテキストオブジェクトに保存します。
DaoAuthenticationProviderはAuthenticationProviderの実装クラスです。
Daoはデータアクセス層の略称であり、この認証の実装アプローチを示唆しています。ユーザーがユーザー名とパスワードを提出し、データベースに保存されたユーザー名とパスワードを比較して、同じユーザー名に対して提出されたパスワードと保存されたパスワードが一致するかどうかを確認するのが認証の役割です。
Spring Securityでは、提出されたユーザー名とパスワードはUsernamePasswordAuthenticationTokenにカプセル化され、ユーザー名に基づいてユーザーを検索するのはUserDetailsServiceに任されています。
ユーザーを取得する方法
DaoAuthenticationProviderでは、ユーザー名に基づいてユーザーをロードするメソッドはretrieveUserです。
// 二つの引数がありますが、retrieveUserは主に最初の引数(username)を使用し、UserDetailsを返します。
protected final UserDetails retrieveUser(String ユーザー名, UsernamePasswordAuthenticationToken 認証情報)
throws AuthenticationException {
prepareTimingAttackProtection();
try {
UserDetails ロードされたユーザー = this.getUserDetailsService().loadUserByUsername(ユーザー名);
if (ロードされたユーザー == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return ロードされたユーザー;
}
catch (UsernameNotFoundException ex) {
mitigateAgainstTimingAttack(認証情報);
throw ex;
}
catch (InternalAuthenticationServiceException ex) {
throw ex;
}
catch (Exception ex) {
throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
}
}
パスワードを比較する
また、UsernamePasswordAuthenticationTokenとUserDetailsのパスワードを比較する必要があります。これはadditionalAuthenticationChecksメソッドが担当します。
このメソッドは親クラスAbstractUserDetailsAuthenticationProviderのauthenticateメソッドから呼び出され、例外がスローされなければ、パスワードの一致は成功とみなされます。
protected void additionalAuthenticationChecks(UserDetails ユーザー詳細,
UsernamePasswordAuthenticationToken 認証情報)
throws AuthenticationException {
if (認証情報.getCredentials() == null) {
logger.debug("Authentication failed: no credentials provided");
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
String 提出されたパスワード = 認証情報.getCredentials().toString();
// パスワードの比較プロセス。PasswordEncoderとSaltSourceが使用されます。
if (!passwordEncoder.matches(提出されたパスワード, ユーザー詳細.getPassword())) {
logger.debug("Authentication failed: password does not match stored value");
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
}
要するに、DaoAuthenticationProviderはユーザーが提出したユーザー名とパスワードを取得し、その正確性を検証します。正しければ、データベースに保存されたユーザー情報を返します。
UserDetailsは最も詳細なユーザー情報を表し、このインターフェースはいくつかの必須のユーザー情報フィールドを含んでいます。具体的な実装クラスはこれを拡張します。
public interface UserDetails extends Serializable {
// アクセス権限情報
Collection extends GrantedAuthority> get権限();
// パスワードを取得
String getPassword();
// ユーザー名を取得
String getUsername();
// アカウントが有効期限切れかどうか
boolean isAccountNonExpired();
// アカウントがロックされていないかどうか
boolean isAccountNonLocked();
// 認証情報が有効期限切れでないかどうか
boolean isCredentialsNonExpired();
// ユーザーが有効かどうか
boolean isEnabled();
}
AuthenticationのgetCredentials()とUserDetailsのgetPassword()は区別する必要があります。前者はユーザーが提出したパスワード証明情報であり、後者はユーザーの正しいパスワードです。**認証プロバイダはこの二つの比較を行います**。
AuthenticationのgetAuthorities()は、実際にはUserDetailsのgetAuthorities()から渡されたものです。
UserDetailsService
public interface UserDetailsService {
UserDetails loadUserByUsername(String ユーザー名) throws UsernameNotFoundException;
}
UserDetailsServiceは、ユーザーデータのDAO(データアクセスオブジェクト)としてのみ機能し、フレームワーク内の他のコンポーネントにデータを提供する以外の機能はありません。一般的なタイプには、データベースからロードするものや、メモリからロードするものがあります。
全体的な認証フロー:
- まず、リクエストが認証情報を含んで到着し、ユーザー名とパスワードがフィルタによって取得され、デフォルトでは
UsernamePasswordAuthenticationTokenという実装クラスにカプセル化されます。 - この
AuthenticationがAuthenticationManagerの認証に成功すると、AuthenticationManagerは情報が埋め込まれたAuthenticationインスタンスを返します。 SecurityContextHolderは、上記の情報が詰まったAuthenticationをSecurityContextHolder.getContext().setAuthentication(...)メソッドでSecurityContextコンテキストオブジェクトに設定します。AuthenticationオブジェクトからUserDetailsオブジェクトを取得します。以前説明したように、認証後のAuthenticationオブジェクトのgetPrincipal()メソッドを呼び出すと、データベースからクエリした後に組み立てられたUserDetailsオブジェクトが取得できます。その後、トークンを作成します。UserDetailsオブジェクトをキャッシュに保存し、後続のフィルタで使用できるようにします。
これで完了です。主要な認証操作はすべてAuthenticationManager.authenticate()が処理してくれます。
authenticateメソッド
このauthenticateメソッドは認証の核心メソッドで、AbstractUserDetailsAuthenticationProviderという抽象クラスにあります。ほとんどの具体的なAuthenticationProviderは、この抽象クラスを継承しています。
// AbstractUserDetailsAuthenticationProvider
// このAbstractUserDetailsAuthenticationProviderクラスは、上記のDaoAuthenticationProviderの親クラスであり、
// 認証の大部分はこの親クラスで行われます。
public Authentication authenticate(Authentication 認証情報) {
// 未認証のAuthenticationオブジェクトにユーザー名があるか検証
String ユーザー名 = (認証情報.getPrincipal() == null) ? "NONE_PROVIDED"
: 認証情報.getName();
boolean キャッシュ使用 = true;
// キャッシュからユーザー名XXXのオブジェクトを検索
UserDetails ユーザー = this.userCache.getUserFromCache(ユーザー名);
// もしなければ、このメソッドに入ります
if (ユーザー == null) {
キャッシュ使用 = false;
try {
// 我々がオーバーライドしたUserDetailsServiceのloadUserByUsernameメソッドを呼び出し
// 自分で組み立てたUserDetailsオブジェクトを取得
ユーザー = retrieveUser(ユーザー名,
(UsernamePasswordAuthenticationToken) 認証情報);
} catch (UsernameNotFoundException notFound) {
logger.debug("User '" + ユーザー名 + "' not found");
if (hideUserNotFoundExceptions) {
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
} else {
throw notFound;
}
}
Assert.notNull(ユーザー,
"retrieveUser returned null - a violation of the interface contract");
}
try {
// アカウントが無効かどうか検証
preAuthenticationChecks.check(ユーザー);
// データベースから取得したパスワードと、入力されたパスワードが一致するか検証
additionalAuthenticationChecks(ユーザー,
(UsernamePasswordAuthenticationToken) 認証情報);
}
}
ロジックモデル
public static void main(String[] args) throws Exception {
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
AuthenticationManager am = new カスタム認証マネージャ();
while(true) {
System.out.println("ユーザー名を入力してください:");
String ユーザー名 = in.readLine();
System.out.println("パスワードを入力してください:");
String パスワード = in.readLine();
try {
// 1、UsernamePasswordAuthenticationTokenオブジェクトをカプセル化
Authentication 認証リクエスト = new UsernamePasswordAuthenticationToken(ユーザー名, パスワード);
// 2、AuthenticationManagerによる認証。失敗した場合はAuthenticationExceptionがスローされます。
Authentication 認証結果 = am.authenticate(認証リクエスト);
// 3、この認証済みのAuthenticationをSecurityContextに設定
SecurityContextHolder.getContext().set認証情報(認証結果);
break;
} catch(AuthenticationException e) {
System.out.println("認証失敗: " + e.getMessage());
}
}
System.out.println("認証成功: Security context contains:
" +
SecurityContextHolder.getContext().get認証情報());
}
// キーアクション:認証部分
@Override
public Authentication authenticate(Authentication 認証情報) throws AuthenticationException {
// getCredentialsはパスワードを返します。ここでは簡単に、ユーザー名とパスワードが一致すればログイン成功とします。
if (認証情報.getName().equals(認証情報.getCredentials())) {
// 認証成功後、認証済みのUsernamePasswordAuthenticationTokenオブジェクトを返し、ユーザーの権限を埋め込みます。
return new UsernamePasswordAuthenticationToken(認証情報.getName(),
認証情報.getCredentials(), 権限リスト);
}
throw new BadCredentialsException("Bad Credentials");
}
}