YesodのID・パスワードの認証

一般的なログインIDとパスワードによる認証の実装 yesod-auth-hashdb というYesodのpluginを利用する、モデルに認証テーブルを作成し ログインIDとパスワードのフィールドを利用して認証を実装する。

実装する画面は以下の2つ。

ログインフォーム

認証前のログイン画面

ログイン後の画面

認証完了後のログイン画面

yesod-auth-hashdbプラグインの追加

package.yamlのdependenciesへyesod-auth-hashdbを追加する。

...
dependencies:
...
- yesod-auth-hashdb
...

認証のアカウント

データベースへ作成する認証用のアカウント情報を保存するモデル。

AdminUsr
    adminId Int sqltype=bigint default=nextval('admin_usr_admin_id_seq')
    loginId Text sqltype=varchar(256)
    password Text Maybe sqltype=varchar(512)
    validFlag Bool sqltype=boolean
    createTime UTCTime sqltype=timestamptz
    updateTime UTCTime sqltype=timestamptz default=now()
    version Int sqltype=integer
    UniAdminUsrLoginId loginId
    Primary adminId
    deriving Typeable Show

ログインフォームのための設定

カスタムフォームを利用するための、 authHashDBWithForm と認証元のモデルを決めるための HashDBUser をインポートする。

import Yesod.Auth.HashDB (authHashDBWithForm, HashDBUser(..))

フォームバリデーション

mreq関数と合わせて利用し、 check関数 を利用して合否を判定する、check関数Right a を返せば成功、 Left b を返したら失敗となる。 check関数 に モナド版の checkM関数 があり、チェックの中でIOなどを発行したい場合に利用できる。

adminLoginIdField :: Int -> Field Handler Text
adminLoginIdField loginIdLen = check (validateLoginId loginIdLen) textField

validateLoginId :: Int -> Text -> Either Text Text
validateLoginId loginIdLen loginId
    | T.length(loginId) > loginIdLen = Left errorMessage
    | otherwise = Right loginId

認証バックエンドの設定

認証にログインIDとパスワードを利用すためのプラグイン設定を行う、 Yesod Auth のインスタンス宣言の authPlugins関数 で指定する。 authHashDBWithForm loginPage でカスタムログインページが出力されるように指定し、ユーザーIDとして利用するモデルのカラムを指定する、注意点としてそのカラムにユニーク属性が必要となる。

instance YesodAuth App where
...
    authPlugins :: App -> [AuthPlugin App]
    authPlugins app = [authHashDBWithForm loginPage (Just . UniAdminUsrLoginId)]

アプリケーションの認証IDをモデルで定義された認証IDへシノニムとして定義する。

instance YesodAuth App where
    type AuthId App = AdminUsrId
..

認証処理

yesod-auth-hashdb を利用した認証の場合、実際のログインIDとパスワードの認証は yesod-auth-hashdb 内の関数で実行されるため、 この authenticate関数 は認証が成功した後に呼び出される。なので、 Nothing のコードは通らない。実際にここで行っていることは認証成功ごの追加処理となっている。

instance YesodAuth App where
...
    authenticate :: (MonadHandler m, HandlerSite m ~ App)
                 => Creds App -> m (AuthenticationResult App)
    authenticate creds = liftHandler $ runDB $ do
        x <- getBy $ UniAdminUsrLoginId $ credsIdent creds
        case x of
            Just (Entity uid usr) ->
              case adminUsrValidFlag usr of
                False -> do
                  -- アカウント停止中
                  return $ UserError InvalidLogin
                True -> do
                  -- 正常なログイン
                  updateAdminLoginTime uid
                  return $ Authenticated uid
            -- yesod-auth-hashdb内部で処理してしまうのでここは通らない
            Nothing -> return $ UserError $ IdentifierNotFound "Admin user not found: "

認証エラー時のメッセージは、「UserError InvalidLogin」 を経由して出力される、 yesod-auth-hashdb を利用した場合のエラーメッセージはプラグイン側が出力するので不要となっている(メッセージを変更するのが少し面倒)

認証の必要なURL

認証が必要なURLに対するアクセスチェックを行う、EntR のように isAuthenticated を利用し、認証されていない場合、AuthResult 型の AuthenticationRequired を返すことによって、ログインフォームへ転送する。

    isAuthorized EntR _ = isAuthenticated
...
-- 認証されているかのチェック
isAuthenticated :: Handler AuthResult
isAuthenticated = do
    muid <- maybeAuthId
    return $ case muid of
        Nothing -> AuthenticationRequired
        Just _ -> Authorized
..

認証が必要でないURLへのアクセスは TopRAuthR のように Authorizedを返す。

    isAuthorized TopR _ = return Authorized
    isAuthorized (AuthR _) _ = return Authorized

出力メッセージのロケール変更

AuthMessage で定義されるディフォルトのメッセージのロケールが en になっているため日本語に切り替えたい場合は以下のように renderAuthMessage を定義する。

instance YesodAuth App where
    -- AuthMessageのロケールを日本語に設定する
    renderAuthMessage :: master -> [Text] -> AuthMessage -> Text
    renderAuthMessage _ _ = japaneseMessage

認証情報

認証したユーザー情報は maybeAuthPair関数 などで取得する、その他、 maybeAuth間数requireAuthPair関数 などがある。

    defaultLayout :: Widget -> Handler Html
    defaultLayout widget = do
        master <- getYesod
...
        muser <- maybeAuthPair

ログイン・ログアウト後のURLの設定

認証完了後のリダイレクト先URLの設定を行う場合は loginDest関数 を設定しログイン後の Route を指定する。

instance YesodAuth App where
...
    loginDest :: App -> Route App
    loginDest _ = EntR
...

ログアウト後のリダイレクト先URLの設定を行う場合は logoutDest関数 を設定しログアウト後の Route を指定する。

instance YesodAuth App where
...
    logoutDest :: App -> Route App
    logoutDest _ = TopR
...

認証後に、認証前にアクセスしようとしていたURLへ自動的にリダイレクトさせたい場合、 redirectToReferer関数 を設定する。

instance YesodAuth App where
...
    -- ログイン成功時、遷移先URLを保存したい場合Trueを設定する
    redirectToReferer :: App -> Bool
    redirectToReferer _ = True
...

ビルド時の警告

ビルド実行時に Foundation.hswarning: [-Worphans] が出力される、 instance宣言AdminUsr の型定義時に行えないことが原因。

$ stack build
..
/home/cuomo/Code/yesod/hashauth/src/Foundation.hs:209:1: warning: [-Worphans]
    Orphan instance: instance HashDBUser AdminUsr
    To avoid this
        move the instance declaration to the module of the class or of the type, or
        wrap the type with a newtype and declare the instance on the new type.
    |
209 | instance HashDBUser AdminUsr where
    | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^...
...

得にに必要な対応ではないが気になる人は、 Foundation.hs ファイルの先頭の拡張宣言へ {-# OPTIONS_GHC -fno-warn-orphans #-} を追加することで出力が止まる。

{-# OPTIONS_GHC -fno-warn-orphans #-}
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}
...

サンプルコード

サンプルコードはguthubへ上げてあるのでどうぞ。

Posted on 2020-12-06 08:06:47

はじめまして

お茶の国静岡で、焼酎のお茶割なんか罰当たりで飲んだことはありません、常に一番搾りを嗜む静岡極東のBBQerです、最近まわりのエンジニアの方々がお料理を上手にやっている姿を恨めしそうに横目に見ながら、軟骨ピリ辛チクワを食べています、みなさんよろしく。

Posted

Amazon

tags