Spring Security認証の仕組みとフロー

主要コンポーネント

  • 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 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;
}

ProviderManagerAuthenticationManagerのデフォルト実装クラスです。多くのユーティリティクラスはこのProviderManagerを中心に構築されており、AuthenticationProviderインターフェースが派生しています。

public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {

    // 複数の認証方式を保持するAuthenticationProviderリスト。デリゲートパターンの応用。
    // 認証のエントリーポイントは常に一つ:AuthenticationManager
    // 例:ユーザー名+パスワード、メール+パスワード、電話番号+パスワードなど
    // デフォルトでは、一つのProviderによる認証成功でログイン成功とみなされます。
    private List<AuthenticationProvider> プロバイダリスト = Collections.emptyList();


    public Authentication authenticate(Authentication 認証情報)
            throws AuthenticationException {
        Class チェック対象 = 認証情報.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はそれをコンテキストオブジェクトに保存します。

DaoAuthenticationProviderAuthenticationProviderの実装クラスです。

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);
    }
}

パスワードを比較する

また、UsernamePasswordAuthenticationTokenUserDetailsのパスワードを比較する必要があります。これは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 get権限();
    // パスワードを取得
    String getPassword();
    // ユーザー名を取得
    String getUsername();
    // アカウントが有効期限切れかどうか
    boolean isAccountNonExpired();
    // アカウントがロックされていないかどうか
    boolean isAccountNonLocked();
    // 認証情報が有効期限切れでないかどうか
    boolean isCredentialsNonExpired();
    // ユーザーが有効かどうか
    boolean isEnabled();
}

AuthenticationgetCredentials()UserDetailsgetPassword()は区別する必要があります。前者はユーザーが提出したパスワード証明情報であり、後者はユーザーの正しいパスワードです。**認証プロバイダはこの二つの比較を行います**。

AuthenticationgetAuthorities()は、実際にはUserDetailsgetAuthorities()から渡されたものです。

UserDetailsService

public interface UserDetailsService {
   UserDetails loadUserByUsername(String ユーザー名) throws UsernameNotFoundException;
}

UserDetailsServiceは、ユーザーデータのDAO(データアクセスオブジェクト)としてのみ機能し、フレームワーク内の他のコンポーネントにデータを提供する以外の機能はありません。一般的なタイプには、データベースからロードするものや、メモリからロードするものがあります。

全体的な認証フロー:

  1. まず、リクエストが認証情報を含んで到着し、ユーザー名とパスワードがフィルタによって取得され、デフォルトではUsernamePasswordAuthenticationTokenという実装クラスにカプセル化されます。
  2. このAuthenticationAuthenticationManagerの認証に成功すると、AuthenticationManagerは情報が埋め込まれたAuthenticationインスタンスを返します。
  3. SecurityContextHolderは、上記の情報が詰まったAuthenticationSecurityContextHolder.getContext().setAuthentication(...)メソッドでSecurityContextコンテキストオブジェクトに設定します。
  4. AuthenticationオブジェクトからUserDetailsオブジェクトを取得します。以前説明したように、認証後のAuthenticationオブジェクトのgetPrincipal()メソッドを呼び出すと、データベースからクエリした後に組み立てられたUserDetailsオブジェクトが取得できます。その後、トークンを作成します。
  5. 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");
    }
}

タグ: Spring Security Java 認証 Spring Framework

7月5日 16:12 投稿