組み込みログイン制御 (Login Control)

LastaFluteの特徴の一つです。

概要

組み込みのログイン実装

ログインの実装が組み込まれています。TypicalLoginAssist を継承して、アプリごとにフィットさせた Assist クラスを作成します。必須ではないですが、カスタマイズも可能なのでオススメです。

ログインチェックも組み込み

Actionのスーパークラスである TypicalAction の中でログインチェックが組み込まれています。 組み込みのログインロジックを使って自動的にログインチェックがかかるようになっています。

ログインコントロールの概念マップ

LastaFlute LoginControl

ログイン画面のAction

ログイン画面の Action は、以下のような実装となります。

e.g. ログインのActionの実装、MaihamaプロジェクトのDocksideアプリにて @Java
@Resource
private DocksideLoginAssist docksideLoginAssist;

@Execute
public HtmlResponse index() {
    if (getUserBean().isPresent()) { // すでにログイン済みなら
        return redirect(MypageAction.class); // マイページへ
    }
    return asHtml(path_Signin_SigninHtml).useForm(SigninForm.class);
}

@Execute
public HtmlResponse signin(SigninForm form) { // ログインボタン押した
    validate(form, messages -> moreValidate(form, messages), () -> {
        form.clearSecurityInfo(); // ダメならパスワード消して...
        return asHtml(path_Signin_SigninHtml); // ログイン画面もう一回
    });

    // DB検索して、セッションにユーザー情報を入れて、ログイン履歴残して...
    // ログイン必須画面で拒否されて飛ばされてきてたら、そっちへログインリダイレクト
    return docksideLoginAssist.loginRedirect(createCredential(form)
            , op -> op.rememberMe(form.rememberMe) // 第三引数: 必要ならクッキー
            , () -> { // 第三引数: OKのときのリダイレクト先
        return redirect(MypageAction.class); // 通常はマイページへ
    });
}

// アノテーションによる必須チェックの後、実際にログインできるかどうかバリデーション
private void moreValidate(SigninForm form, DocksideMessages messages) {
    if (isNotEmpty(form.email) && isNotEmpty(form.password)) {
        if (!docksideLoginAssist.checkUserLoginable(form.email, form.password)) {
            messages.addErrorsLoginFailure("email");
        }
    }
}

private UserPasswordCredential createCredential(SigninForm form) {
    return new UserPasswordCredential(form.account, form.password);
}

ログインの処理は、[App]LoginAssist に集約されています。

ログイン必須チェック

ActionHookでチェックされる

ログイン必須画面にログインしていない状態でのアクセスを防ぐ仕組みは、ActionHook の godHandPrologue() で行われています。その中から、TypicalGodHandPrologue#performPrologue() が呼ばれています。

e.g. ActionHookでのログインチェックの実装 @Java
public ActionResponse performPrologue(ActionRuntime runtime) { // fixed process
    arrangeThreadCacheContextBasicItem(runtime);
    arrangePreparedAccessContext(runtime);
    arrangeCallbackContext(runtime); // should be after access-context (using access context's info)
    checkLoginRequired(runtime); // should be after access-context (may have update)
    arrangeThreadCacheContextLoginItem(runtime);
    return ActionResponse.undefined();
}

...

/**
 * Check the login required for the requested action.
 * @param runtime The runtime meta of action execute to determine required action. (NotNull)
 * @throws LoginRequiredException When it fails to access the action for non-login.
 */
protected void checkLoginRequired(ActionRuntime runtime) throws LoginRequiredException {
    loginManager.ifPresent(nager -> {
        nager.checkLoginRequired(createLogingHandlingResource(runtime));
    });
}

すべての Action にて、loginManager の checkLoginRequired() が呼ばれます。

ここでの loginManager の実体は [App]LoginAssist のことです。LoginAssist は LoginManager インターフェースを implements しているので、フレームワークの中では、LoginManagerとして取り扱っています。

ログイン必須例外がthrowされる

ログインしていない状態で、ログイン必須のActionにアクセスした場合は、checkLoginRequired() が LoginRequiredException を throw します。

LastaFlute組み込みの例外クラスで、LaApplicationException を継承しているため業務例外として扱われます。 どのように例外ハンドリングされるのか?細かく追いたい場合は ApplicationExceptionResolver や ApiFailureHook を読むと良いでしょう。

HtmlResponseのActionであれば、自動でログイン画面に遷移します。その後、正常にログインされたら、もともとアクセスしようとした画面に遷移します。 (ログインリダイレクトと呼んでいます)

とにかく LoginAssist

どうやら、この [App]LoginAssist がとにかくログインの処理を一手に引き受けているようです。

ログインのAction や ActionHook でのログイン必須チェックから呼び出されます。 こちらに、認証処理やセッションの設定、RememberMeのクッキーの設定、ログイン履歴など、ログインに必要な処理が詰め込まれています。

LoginAssistの階層構造

それぞれのWebプロジェクトに LoginAssist があり、例えば Maihama プロジェクトの Dockside Webアプリであれば、DocksideLoginAssist となります。

e.g. LoginLogic の階層構造、Maihamaプロジェクトなら @Structure
LoginManager  // ログインの操作が定義されているインターフェース
 |   |
 |   +------------------------------------------------------------+
 |                                                                |
 |                                                                |
 |-TypicalLoginAssist // フレームワーク提供の典型的なベースクラス          |
    |                                                             |
    |-MaihamaLoginAssist // プロジェクト共通のベースクラス               |
       |                                                          |
       |      +---------------------------------------------------+
       |      |
       |      |
       |     PrimaryLoginManager // 主要のLoginManager (複数あったら一つだけに)
       |      |
       |-DocksideLoginAssist // Webアプリのクラス (スマートデプロイ対象)

LastaFluteが提供する TypicalLoginAssist に、おおよそのロジックが組み込まれていて、それぞれのプロジェクトにフィットさせる部分を、 サブクラスで実装していくような形です。

なので、DocksideLoginAssist にはあまり実装はたくさんありません。 どのテーブルにアクセスするのか?どこに履歴を残すのか?など、ピンポイントで必要な情報をスーパークラスに渡してあげるだけになります。 もちろん、とある処理だけをオーバーライドして独自の挙動にすることもできます(権限チェックなどはそのように)。

PrimaryLoginManager は、ひとつのWEBアプリ内で LoginAssist が複数あったときに、主要な Assist をユニークに識別するためのインターフェースです。 そういう状況にならない限りはあまり重要なものではありませんが、しっかりと付けておきましょう。

LoginAssistの現場フィット

アプリにフィットさせるための [App]LoginAssist のポイントはこちら。

PKの型
Genericで指定、メソッドの引数に影響 (Exampleテンプレートでは Integer)
ユーザー検索
ログインユーザの検索処理、doFindLoginUser()など
UserBeanの調整
[App]UserBeanの実装、LoginAssistでnewしたり
ログイン履歴の処理
saveLoginHistory() でいい感じに
アプリ固有のクラス指定
getUserBeanType() や getLoginActionType() など

LoginAssistのpublicメソッド

大きくは以下の四つ:

checkUserLoginable()
ログインできるか判定する (ログイン画面のActionのバリデーション)
login()
単純にログイン処理だけ (APIスタイルのログイン画面のAction)
loginRedirect()
ログイン処理とログインリダイレクト (HTMLスタイルのログイン画面のAction)
checkLoginRequired()
ログイン必須チェックを行う (ActionHookから呼ばれる想定)

さらには、引数が違うだけのオーバーロードのメソッドや、多種多様なログイン処理を行うメソッド、 ログイン処理やチェック処理の一部だけを行う部品的なメソッドなどがあります。

identityLogin()
ユーザーIDを指定してログイン処理 (事前にユーザーが特定できてる場合)
givenLogin()
Entityを指定してにログイン処理 (事前にユーザー情報を持っている場合)
logout()
ログアウト処理 (クッキーも削除、セッションはまるごとinvalidate)
findLoginUser()
ログインできるユーザーを探す (ログインできなければempty)

メソッドの一覧が見たければ、LoginManager のソースを読むとよいでしょう。 詳しい処理に関しては、TypicalLoginAssist を読んでみましょう。

Typicalは提案!?

厳密には、TypicalLoginAssist を使わなければならないわけではありません。基本的に Typical と付いているものは、もし現場にフィットするなら使ってみてください、というニュアンスのものです。 ですが、多くの処理をオーバーライドなどで拡張できるようにしているので、よほど特殊でなければフィットさせることができるのではないかと想定しています。

権限チェックの実装

拡張ポイントが用意されている

権限チェックの仕組み自体は、かなり業務に特化する部分なので特に組み込みでの用意はありませんが、 ログインロジックのプロセスの一環として組み込める拡張ポイントが用意されています。

checkPermission()

ログインチェックの処理の中で、ログイン状態のときに必ず呼ばれるメソッドがあります。 そのメソッドの中で権限チェックをすることで、ログインしていてもその画面で必要な権限がないアクセスを弾くことができます。

[App]LoginAssist が継承している TypicalLoginAssist にて、checkPermission() というメソッドが定義されていて、デフォルトでは中身は空っぽです。

e.g. checkPermission()のそのままのコード @Java
/**
 * Check the permission of the login user. (called in login status) <br>
 * If the request cannot access the action for permission denied, throw LoginRequiredException.
 * @param resource The resource of login handling to determine. (NotNull)
 * @throws LoginRequiredException When it fails to access the action for permission denied.
 */
protected void checkPermission(LoginHandlingResource resource) {
    // no check as default, you can override
}

checkPermission() は、既にログイン済み、もしくは、いま RememberMe したばかり のときに呼び出されます。

オーバーライドして権限チェック

それぞれの [App]LoginAssist にて、 この checkPermission() をオーバーライドして、権限チェックをするとよいでしょう。 権限エラーの場合は、LoginRequiredException を throw します。

e.g. checkPermission()で権限チェックして、ダメなら例外 @Java
@Override
protected void checkPermission(LoginHandlingResource resource) {
    ... = resource.getExecuteMethod(); // (いい感じに使って判定)
    if (...) { // 権限なければ
        throw new LoginRequiredException("デバッグ用メッセージ、何かいい感じに");
    }
}

どの画面がどんな権限?

ここは様々な方法がありますが、一例として、アノテーションに付けるやり方があります。

e.g. Executeメソッドにアノテーションでロールを紐付ける @Java
@Execute()
@Permission({CDef.Role.営業, CDef.Role.企画}) // 営業と企画の役割の人が実行できる
public HtmlResponse index() {
    ...
}
e.g. Executeメソッドに付ける権限のアノテーション @Java
// (アノテーションに付けるアノテーションはいい感じに)
public @interface Permission {

    CDef.Role[] value();
}

ロールはテーブル区分値として定義しておきます。自動生成された CDef をアノテーションで指定できるようにして、checkPermission() の中でそのアノテーションの値を取得して、ログインユーザーのロールと照らし合わせてアクセス可能かどうかを判定します。

もちろん、必ずこのやり方である必要はありません。例えば、データベースの中で、画面(Executeメソッド)とロールを紐づけてもよいでしょう。 データベースの中で権限設定を閉じこめることができて、動的にも修正できるのがメリットです。 ただ、画面が増えたときにそのテーブルのデータもメンテナンスしてあげないといけないので、インクリメンタル開発の現場ではちょっと運用が煩雑になります。 アノテーション方式であればプログラム作成時に自然と権限が付与されるので、定義し忘れのトラブルは少ないでしょう。

アクセス権限まとめブログ

ユーザーの方が、アクセス権限に関してブログでまとめて頂いています。 そちらもぜひ参考にすると良いでしょう。

RDB以外のもので認証するなら?

LoginAssistクラスにて、RDBに検索している箇所を別の認証サービスを使ったプログラムに差し替えます。 Assistクラスを使う側(Actionなど)では、RDBなのか別の認証サービスなのかは意識しないようにするのがオススメです。

具体的には、LoginAssistクラスの checkCredential() や resolveCredential() や doFindLoginUser() で、RDBにアクセスしてユーザーを認証している処理を、別のものに差し替えます。

例えば、RemoteAPi を使って認証することも想定されます。

ユーザーの方のブログをご紹介

実際に業務で LastaFlute を利用されていた方が素敵なブログを書いてくださっているのでご紹介します。 ありがとうございます!(リンクの承諾も頂いています)

アクセス権限以外の話題も書いてくださっています。