« 2016年8月 | トップページ | 2017年12月 »

2017年9月17日 (日)

ログイン失敗時のメッセージを増やしたい on Spring Boot

0. ことのはじまり

先日、ログイン/ログアウト前後でクエリパラメータを保つ on Spring Bootと題して、ログイン/ログアウト時にクエリパラメータを保持する方法について書いたんだけど、こいつの参照実装を後輩君に教えてやったところ、「ログイン失敗時のメッセージが出なくなったんすけど?」とかいうリプライが。

どうやら、槙 俊明さんのはじめての Spring Boot[改訂版]をもとに(というかほぼそのまんまの模様)Spring Securityの設定をしていたようだが、「ログインに失敗したときに、画面上どうふるまうか」を理解せぬまま、僕が示した参照実装のコードを丸写ししていたようで、あんなリプライを出してきた模様。

このあたり、「自分で考えろ」と突き放してもよかったんだが、後学のために「どうすればいいか」を少々考えてみることにする。

1. 実際どうなっているのか

氏の書籍には、だいたいこんな感じでエラーメッセージ周りの処理が紹介されている。

Viewのhtml(抜粋)

<div th:if="${param.error}>ユーザー名、またはパスワードが違います。</div>

SecurityConfig(抜粋)

    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()
            .loginProcessingUrl("/login")
            .loginPage("/loginForm")
            .failureUrl("/loginForm?error")
            .defaultSuccessUrl("/", true)
                .usernameParameter("username")
                .passwordParameter("password")
            .permitAll();
    }

勘のいい方はお分かりと思うが、

  • 認証に成功したら、「/」にリダイレクト
  • 認証に失敗したら、「/loginForm?error」に移動
と、 URLだけで制御している ことがわかると思う。

それが証拠に、認証に失敗しなくても、「/loginForm?error」をブラウザのアドレスバーに直接入力すると、エラーメッセージが出るはずである。

これ自体は、話を簡単にするための処置なのだろう、と考えているのだが、今回の一件では、認証の成功、失敗で適切にクエリパラメータを加工して流す考慮が必要になるので、アプローチとしては以下の2つがあると考える。

  1. 認証ハンドラで、クエリパラメータを適切に追加、削除する
  2. 失敗時の認証ハンドラで、エラーメッセージをセッションに置く

2. URLだけで制御する場合

Viewの実装をそのままにしたい場合、いままでどおりURLでコントロールするよう、クエリパラメータを適切に追加、削除する処理を書けばよい、ということになる。
たとえば、こんな感じ。

認証成功時(抜粋)

        // add whole query parameters to url 
        String queryParams = request.getQueryString() == null ? "" : "?" + request.getQueryString();
        
        // remove error parameter if present.
        if (queryParams.contains("error&")) {
            queryParams.replaceAll("error&", "");
        } else if (queryParams.contains("&error")) {
            queryParams.replaceAll("&error", "");
        }

認証失敗時(抜粋)

        // add whole query parameters to url 
        String queryParams = request.getQueryString() == null ? "" : "?" + request.getQueryString();
        
        // add error parameter if not present
        if (!queryParams.contains("error")) {
            if (queryParams.length() == 0) {
                queryParams = queryParams + "?error";
            } else {
                queryParams = queryParams + "&error";
            }
        }

汚い実装で恐縮だが、雰囲気は伝わっただろうか。

3. セッションを使う場合

認証失敗時に呼ばれる AuthenticationFailurehandler#onAuthenticationFailure(HttpServletRequest, HttpServletResponse, AuthenticationException) では、認証失敗に至った理由が AuthenticationException のオブジェクトとして受け取ることができるようになっている。(参考

「パスワードが違う」「アカウントが有効期限切れ」「アカウント凍結中」など、詳細に調べることができるので、これを使って流すエラーメッセージを選別すればいい。
たとえば、こんな感じ。

認証失敗時(抜粋)

        // Analyze the cause of the error
        String errReason = null;
        if (exception instanceof BadCredentialsException) {
            errReason = "Invalid user name or password.";
        } else if (exception instanceof AccountExpiredException) {
            errReason = "This account is expired. Please contact administrator.";
        } else if (exception instanceof CredentialsExpiredException) {
            errReason = "Your password is expired. Please contact administrator.";
        } else if (exception instanceof DisabledException) {
            errReason = "Your password is disabled. Please contact administrator.";
        } else if (exception instanceof LockedException) {
            errReason = "Your accouunt is locked. Please contact administrator.";
        } else {
            errReason = "Unknown problem occured. Please contact administrator.";
        }
        
        if (errReason != null && errReason.length() > 0) {
            HttpSession session = request.getSession();
            session.setAttribute("errReason", errReason);
        }

一方Thymeleafには、セッションオブジェクトを扱うための「session」というそのものずばりな予約変数がある。

利用方法は「Spring MVC and Thymeleaf: how to access data from templates - Thymeleaf」という公式ドキュメントに詳しく書かれているので、ひとまずそちらを参照していただくとして、セッションにエラーメッセージを置くなら、セッションから値を取り出して張る処理をViewテンプレートに書く。
たとえば、こんな感じ。

Viewのテンプレート(抜粋)

<div th:unless="${session == null}" th:text="${session.errReason}">Hoge</div>

この方法のメリットは、クエリパラメータをこねくり回す必要がなくなる点で、おそらくこういう方法のほうがフレームワークの設計思想に沿ったものなんじゃないだろうか、と個人的に考えている。

4. とりあえず動く参照実装

URLだけでコントロールしたい場合こちらを、セッションを使う場合こちらを、それぞれご参照ください。

| | コメント (0) | トラックバック (0)

2017年9月10日 (日)

ログイン/ログアウト前後でクエリパラメータを保つ on Spring Boot

0. ことのはじまり

僕は現在、都合により外に出されているんだけど、先日22:00くらいに自宅で自社のグループウェアを開いてみると、弊社内にいる後輩君から「助けてくださ~い(´;ω;`)」とかいうメールが。

いわく、「Spring Bootを使っているアプリがあり、認証にSpring Securityを使っているが、ログイン/ログアウトの際にクエリパラメータ(※ページのアドレスの後ろについている「?」以降のアレ)を保つ必要があり、どうやったらいいのか自分ではわからない」とのことらしい。

まぁ、自分で調べる際の検索キーワードの選定がまずいだけ、のような気もしないでもないのだが、とりあえず参照実装作成までを目標にして、どうやったらいいかを調べてみた。

ということなので、どういう想定でいくかの整理をしてみる。

  • Spring Bootの最新安定版(現時点では1.5.6.RELEASE)を使用
  • 依存ライブラリは、Thymeleaf、Spring Securityと、JS処理用にjQuery 2.1.4を追加
  • 認証には、データアクセスではなくインメモリ認証を使う
  • クエリパラメータはpだけを認識する

というわけなので、やってみる。

1. やること

いろいろググってみたところ、やらなければならないことは以下の3つだということが判明。

  1. 認証ハンドラを実装したクラスを用意し、その中でクエリパラメータを右から左にリレーする処理を書く。
  2. WebSecurityConfigurerAdapter の継承クラスで、1.で作成した認証ハンドラを使う設定を書く。
  3. Viewからのアクセスは、単純なsubmitやaタグのhrefで処理するのではなく、JSでaction属性やhref属性を直接操作する。

2. 実際には

2-1. カスタム認証ハンドラの実装

必要なカスタム認証ハンドラは、以下のとおり。

処理されるタイミング 実装するinterface
ログイン成功時 org.springframework.security.web.authentication.AuthenticationSuccessHandler
ログイン失敗時 org.springframework.security.web.authentication.AuthenticationFailureHandler
ログアウト時 org.springframework.security.web.authentication.logout.LogoutHandler

とりあえずログイン成功時だけ乗せるけど、HttpSevletRequestからクエリパラメータの文字列をとって、リダイレクトURLにぶら下げなおす、というのが基本。

public class AuthSuccess implements AuthenticationSuccessHandler {

  @Override
  public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
      Authentication authentication) throws IOException, ServletException {
    
    // add whole query parameters to url 
    String queryParams = request.getQueryString() == null ? "" : "?" + request.getQueryString();

    RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
    redirectStrategy.sendRedirect(request, response, "/menu" + queryParams );
  }

}

2-2. WebSecurityConfigurerAdapterに認証ハンドラをセット

Spring Securityを入れていると、その構成を行うJavaConfigがあるはずだが、そこをこんな感じに書き換える。

http.formLogin()
    .loginProcessingUrl("/login")
    .loginPage("/login")
    .successHandler(new AuthSuccess())  // ログイン成功時のカスタムハンドラ
    .failureHandler(new AuthFailure())  // ログイン失敗時のカスタムハンドラ
    .permitAll();

http.logout()
    .logoutRequestMatcher(new AntPathRequestMatcher("/logout**"))
    .addLogoutHandler(new LogoutPostProcess())  // ログアウト時のカスタムハンドラ
    .deleteCookies("JSESSIONID")
    .invalidateHttpSession(true);

それぞれカスタム認証ハンドラを設定するためのメソッドが用意されているので、そこにオブジェクトを放り込めばOK。

2-3. JSでURLをたたく

フロント系中心にやってる人にはどうということはないんだけど、通常 <input type="submit"> とか <a href="hogehoge"> とかで書いている処理を、JavaScriptでクエリパラメータを取得してURLをたたくようにすればOK。

/**
 * URLについているクエリパラメータを取得して、Formのaction属性を書き換えてsubmitする処理。
 */
function sendReq() {
  var arg = getQueryParam();
  
  var url = $('#loginForm').attr('action');
  if (arg.p != null) {
    url += '?p=' + arg.p;
  }

  $('#loginForm').attr('action', url);
  $('#loginForm').submit();
}

/**
 * URLについているクエリパラメータを取得する処理。
 */
function getQueryParam() {
  var arg = new Object;
  var pair = location.search.substring(1).split('&');
  for (var i = 0; pair[i]; i++) {
    var kv = pair[i].split('=');
    arg[kv[0]] = kv[1];
  }
  
  return arg;
}

/**
 * URLについているクエリパラメータを取得して特定ページへ遷移する処理。
 */
function doLogout() {
  var proto = location.protocol;
  var host = location.host;

  var url = proto + '//' + host + '/logout';
  
  var arg = getQueryParam();
  if (arg.p != null) {
    url += '?p=' + arg.p;
  }
  
  location.href = url;
}

3. とりあえず動かせる参照実装

GitHubに上げました。ご参照あれ。
f97one/AddingQueryParamsDemo

| | コメント (0) | トラックバック (0)

« 2016年8月 | トップページ | 2017年12月 »