カジュアルな技術ノート

小難しい技術のお話をできるだけわかりやすく...

Spring Security をわかりやすく(認証全体像編・その2)

何回かに渡って Spring Security の認証を扱っていきたいと思います。
Spring Security を触ったことのある人にまず聞いてみたいのですが、認証の実装に対してどんな印象を持っていますか?

いままで何人かの人にこの質問をぶつけてみましたが、 とても直感的にはわかりにくい という声を聞いております(かくいう僕自身、何度か挫折しました。。)。

よくわからないクラスがたくさん登場したり、必要な実装自体が非常に部分的だったりするので、全体像がよく見えなくなりがちです。

少しずつ全体像が見えるように。。

今回は Spring Security 認証の中でも、

  • 共通的にログイン認証を構成するクラス群とその役割

を説明していきます。

*今回は前回からの続編なので、先にこちらを読むことをお勧めします。

casual-tech-note.hatenablog.com

こちらの記事ではログイン認証の中でも

  • 結局ログインって何をやってるのか
  • 結局ログイン済みユーザーかどう判断しているのか

を Authentication オブジェクトを通してざっくりとみていきました。

今回はその中でも、

  • ユーザー名とパスワードをチェックするところ
  • Authentication オブジェクトを作成するところ

によりフォーカスしています。
f:id:shin-kinoshita:20181124163741p:plain

Filter による認証と AuthenticationManager

今回みていきます、ユーザー名とパスワードのチェックや Authentication オブジェクトの作成は Filter 処理で行われています。

実行されている Filter は UsernamePasswordAuthenticationFilter です(パスワード認証の場合)。
この Filter 処理では次のような処理が実行されます。

  1. Authentication オブジェクトを生成
    この時作成される Authentication オブジェクトはユーザー名とパスワードを保持します。
  2. AuthenticationManager の authenticate メソッドを実行
    Authentication オブジェクトが保持しているユーザー名やパスワードに問題がないかチェックします。

AuthenticationManager は次のように実装されています。

public interface AuthenticationManager {
    Authentication authenticate(Authentication authentication)
            throws AuthenticationException;
}

authenticate メソッドは引数として Authentication オブジェクトを受け取り、オブジェクトの情報が適切でなければ例外をスローする、という設計になっています。
f:id:shin-kinoshita:20181124163822p:plain

AuthenticationProvider と様々なパスワード認証

次に AuthenticationManager の authenticate メソッドが行なっている処理をより深ぼっていきます。

具体的に以下が実行されています。

  1. AuthenticationProvider の supports メソッドを実行
  2. AuthenticationProvider の authenticate メソッドを実行

ここで新しく AuthenticationProvider という別のインターフェースが登場します。
Authentication オブジェクトのチェックは AuthenticationManager によって行われるとのことでしたが、内部的には AuthenticationProvider が行います。

public interface AuthenticationProvider {
    Authentication authenticate(Authentication authentication)
            throws AuthenticationException;

    boolean supports(Class<?> authentication);
}

それぞれのメソッドは、

  • supports メソッド
    引数で与えられる型情報をサポートしていれば true を返す(Authentication オブジェクトの型が引数として与えられる)。
  • authenticate メソッド
    Authentication オブジェクトが保持する情報が適切かチェックする。

このような設計になっていることを理解するに当たって、パスワード認証そのものについて押さえておくべきことがあります。
それは、ここまで一口にパスワード認証ということで扱ってきましたが 実際にはパスワード認証にもいくつか種類が考えられる ということです。
例えば

  • 通常のパスワード認証
    セッションが切れると、再度ユーザー名とパスワードを入力してログインする認証方法。
  • Remember 認証
    初回だけしかユーザー名とパスワードを入力しないパスワード認証方法。
  • 代理ログイン認証
    別のアカウントとして代理でログインする認証方法。

などなど、他にもたくさん存在します。

そして認証方法によってログイン済みのユーザーかを判断するために必要な情報も異なります。
これに合わせて 今まで扱ってきた Authentication も認証方式ごとに実装クラスが存在しています。

また拡張性ということを考えてみましょう。
もしかしたら、この認証方式は今後も増える可能性もあります。
そうなると認証方式が増えるたびに AuthenticationManager の authenticate メソッドも変更する必要が出てきてしまい扱いが悪いです。

そのため 各認証方式ごとに AuthenticationProvider の実装クラスが用意され、認証処理が実装されています。

つまり 認証方式ごとに Authentication と AuthenticationProvider の実装クラスが対になって用意されているということです。

f:id:shin-kinoshita:20181124165932p:plain
認証方式ごとに存在する AuthenticationProvider と Authentication

さて、ここまでを踏まえて結局 AuthenticationManager の authenticate メソッドで行なっていることは、

  • 認証方式に合わせて AuthenticationProvider を選択
  • 選択された AuthenticationProvider で Authentication オブジェクトをチェック

以下に認証方式が Remember Me 認証の時の例を示しておきます。

f:id:shin-kinoshita:20181124182454p:plain

まとめ

最後に今まで扱ってきたクラス群の全体像を示しておきます。
f:id:shin-kinoshita:20181124184559p:plain

ざっというと、Filter 処理で Authentication オブジェクトを作成し AuthenticationManager -> AuthenticationProvider と渡って認証を行います。
次回は、各認証方式ごとに解説していきます。

Spring Security をわかりやすく(認証全体像編・その1)

何回かに渡って Spring Security の認証を扱っていきたいと思います。
Spring Security を触ったことのある人にまず聞いてみたいのですが、認証の実装に対してどんな印象を持っていますか?

いままで何人かの人にこの質問をぶつけてみましたが、 とても直感的にはわかりにくい という声を聞いております(かくいう僕自身、何度か挫折しました。。)。

よくわからないクラスがたくさん登場したり、必要な実装自体が非常に部分的だったりするので、全体像がよく見えなくなりがちです。

少しずつ全体像が見えるように。。

今回は Spring Security 認証の中でも、

  • 結局ログインって何をやってるのか
  • 結局ログイン済みユーザーかどう判断しているのか

と行ったところをざっくりと説明していきます。

一般的なログイン認証の話

最初に一般的なログイン認証の話をします。

ログイン認証では、ユーザーがユーザー名とパスワードを入力してその組み合わせが適切であれば、ログインが成功。
ログイン成功ページに進むことができます。
f:id:shin-kinoshita:20181120233023p:plain

この時に セッション ID が発行されて、ブラウザのクッキーに保存します。
ログイン後にリクエストを送信するたびに、ユーザー名とパスワードを入力してもらい認証をするのは面倒なので、代わりにこのセッション ID でログイン済みかどうかを判断します。
f:id:shin-kinoshita:20181120232855p:plain

これが一般的なログイン認証の話ですが Spring Security でも同じ方法を取っています。
Spring Security を有効化したアプリケーションにアクセスして、何かしらの Web ページを開き、そこでのクッキーを確認してみましょう(ブラウザの拡張機能で簡単にみられます)。

すると JSESSIONID という名前でセッション ID が保存されているのが確認できるはずです。

セッションに保存されている Authentication

ここから Spring Security 独自の話にシフトしていきます。

一般的な話としてログイン時にセッション ID が発行されて、この情報を元にユーザーがログイン済みかどうかを判断するという話でした。
これを実現するためには、サーバーサイドでセッション ID に紐づいたログイン済みユーザーの情報を保持しておく必要があります。

このログイン済みユーザー情報を保持するのが Authentication インターフェースです。
実装を見ていきましょう。

public interface Authentication extends Principal, Serializable {
  // 1. 認可情報の getter
  Collection<? extends GrantedAuthority> getAuthorities();

  // 2. パスワードの getter
  Object getCredentials();

  Object getDetails();

  // 3. ユーザー名の getter
  Object getPrincipal();

  boolean isAuthenticated();

  void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}

特に認証に大きく関わるメソッドにコメントをつけてみましたが、それぞれ

  1. 認可情報
  2. パスワード
  3. ユーザー名

を保持していると考えてください。

結局ログインで何を行なっているのか

結局ログインで行なっていることは、入力したユーザー名とパスワードの組み合わせが正しければ、それを元に Authentication オブジェクトを作成することです。
この Authentication は、セッション領域にログイン成功したユーザーの各 JSESSIONID ごとに作成されます。
f:id:shin-kinoshita:20181122212457p:plain

もちろんユーザー名とパスワードの組み合わせが間違っていると、Authentication はセッションに保存されず、ログインページにリダイレクトされます。

結局どうログイン済みか確認してるのか

ログイン成功時には Authentication オブジェクトが作成されると言うことでした。 その後、認証が必要なページにアクセスした時は JSESSIONID を元に作成された Authentication の取得を試みます。
この Authentication が見つかれば問題なくリクエストしたページにアクセスできるというわけです。
f:id:shin-kinoshita:20181121005317p:plain

*正確には認可情報を検証し、権限がない時はページに遷移できませんが、今回は本筋から外れるので割愛します。

実際に Authentication を取得してみよう!

ここまでセッション領域に保持する Authentication オブジェクトの話を進めてきましたが、少し概念的な話になってしまったので、実際に取得するコードを書きます。

HttpSession session = request.getSession();
SecurityContext securityContext = (SecurityContext)session.getAttribute("SPRING_SECURITY_CONTEXT");
Authentication authentication = securityContext.getAuthentication();

request はリクエスト情報を持った HttpServeltRequest 型のオブジェクトです(DI をするなどして受け取ってください)。
そして最終的に取得した authentication が Authentication オブジェクトです。
Authentication オブジェクトはセッションの SPRING_SECURITY_CONTEXT 属性値である SecurityContext 型のオブジェクトの中に保存されています。

SecurityContext オブジェクトは、ログイン済みかどうかに関わらず取得できますが、 Authentication オブジェクトは、ログイン済みでないと null が格納されます。

まとめ

今回は Spring Security の認証をざっくりと説明しました。
ログインは、Authentication オブジェクトの作成。
ログイン済みかどうかは、Authentication オブジェクトがセッションに保持されているかをチェックすることで確認していると覚えておいてください。

次回はもう少し正確に、たくさん登場するクラスたちが認証をどのような役割を担っているかをみていきます。

Spring・コントローラーのメソッドで引数を受け取る仕組み

HTTP リクエストのパスに応じて、どの view を返すのか。。
ここの制御は Spring では、コントローラーを実装することで実現されています。

Spring を使用するなら必要不可欠の実装です。
でもコントローラーのメソッド実装において、いろんなものを引数で受け取ることができるってご存知ですか?(ここで思いっきり「はい!!」と答えられると、これから先辛いんだけれども。。)

今回は、コントローラーの実装メソッドによって何を受け取ることができるのか、それはどのように実現しているかを説明していきます。

どうやって受け取っているの?

コントローラーの実装メソッドで受け取る引数は HandlerMethodArgumentResolver というインターフェースを実装して定義されています。 実装クラスを探してみるとデフォルトで多数存在していることがわかります。

例えば、こんなコントローラーメソッドを実装したとします。

@PostMapping
public String paramTest(@RequestParam("curryType") String curryType, Model model) {
    model.addAttribute("curryType", curryType);
    return "pages/curry_love";
}

ここでは、@RequestParam を使用して curryType というリクエストパラメータと、model を引数として受け取っています。
よくありそうな実装ですが、ここでは次の HandlerMethodArgumentResolver たちにお世話になっています。

HandlerMethodArgumentResolver 提供機能
RequestParamMapMethodArgumentResolver @RequestParam をもった引数をサポートし、割り当て
ModelMethodProcessor Model クラスの引数をサポートし、割り当て

具体的には、どう実装していくのでしょうか。
このインターフェースが持っている2つの抽象メソッドを実装することで実現します。
順に見ていきましょう。

public interface HandlerMethodArgumentResolver {

    // 実装メソッドが持つ引数をサポートする条件をここで定義
    boolean supportsParameter(MethodParameter parameter);

    // メソッドの引数に、どう割り当てるのかを定義
    @Nullable
    Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
            NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;

}

イメージとしては、まず実装メソッドが持つ引数それぞれに対して、どの HandlerMethodArgumentResolver を割り当てるのかを supportsParameter メソッドにより決定します。
その後、選択された HandlerMethodArgumentResolverresolveArgument メソッドによって実際に引数に値を割り当てます。 f:id:shin-kinoshita:20181108094528p:plain

かんたんな実装例

ここまで来てイメージ湧いて来たでしょうか?
流れはわかったとしても、しっかりとイメージするには実際に実装してみるのが一番です。

まず OriginalObject というクラスを独自に定義します。

public class OriginalObject {
    private String name;
    private String favoriteCurry;

    public OriginalObject(String name, String favoriteCurry) {
        this.name = name;
        this.favoriteCurry = favoriteCurry;
    }

    public String getName() {
        return name;
    }

    public String getFavoriteCurry() {
        return favoriteCurry;
    }
}

名前と好きなカレーを格納する OriginalObject。 このクラスのインスタンスをコントローラーから受け取ることを考えます。

// OriginalObject のインスタンスを引数で受け取るコントローラーメソッド
@GetMapping
public String show(Model model, OriginalObject originalObject) {
    model.addAttribute("originalObject", originalObject);
    return "argument/original/show";
}

ここで指定した view では、 originalObject の格納している値を表示するものにして見ましょう。

<!-- originalObject を表示する view -->
<h1>Show Page</h1>
<p>I often know you, <b th:text="${originalObject.name}" style="font-size: 20px"></b></p>
<p>Your favorite curry is <b th:text="${originalObject.favoriteCurry}" style="font-size: 20px"></b>, isn't it??</p>

f:id:shin-kinoshita:20181107224542p:plain これは、originalObject の各フィールドが

  • name: "subaru"
  • favoriteCurry: "goa curry"

と設定されていた時の表示例です。

さあ、これを実現するための HandlerMethodArgumentResolver の実装サンプルを見ていきましょう。

// HandlerMethodArgumentResolver の実装サンプル
public class OriginalObjectArgumentResolver implements HandlerMethodArgumentResolver {
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return OriginalObject.class.isAssignableFrom(parameter.getParameterType());
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        return new OriginalObject("subaru", "goa curry");
    }
}

超ミニマムの実装です。

supportsParameter メソッドの parameter という引数は、コントローラーのメソッド引数の情報を持っています。
これの型情報が OriginalObject に適応可能であれば true を返し、OriginalObjectArgumentResolver の選択が決定されるというわけです。

resolveArgument メソッドは name に "subaru" 、favoriteCurry に "goa curry" を指定した OriginalObject のインスタンスを返すようにしています。

最後に定義した OriginalObjectArgumentResolver クラスを使用するさせるために WebMvcConfigurer の実装クラスから登録してあげる必要があります。

@Configuration
public class OriginalMvcConfigurer implements WebMvcConfigurer {

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(new OriginalObjectArgumentResolver());
    }
}

ちょっとだけ工夫した実装例

ここまで超ミニマム実装を見て来ましたが、結局どんなリクエストが来ても subaru が goa curry 大好きなことしかわからない実装になってしまっているので、あまりメソッドの引数で OriginalObject のインスタンスを受け取る必要性がないですね。。

なので、リクエスト情報から誰がどんなカレーが好きなのか決めるような仕様に変更してみましょう。

変更としては、name=yogesh&favoriteCurry=veg curry というパラメータを追可したリクエストを送ったとします。
ここで表示される画面は指定された名前とカレーにして見ましょう。
f:id:shin-kinoshita:20181108100445p:plain これを実装するには、resolveArgument メソッドで OriginalObject を初期化する際に、リクエストパラメータを取得できれば良いです。
以下実装例です。

// OriginalObjectArgumentResolver の resolveArgument を修正!
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
    HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
    String name = request.getParameter("name");
    String favoriteCurry = request.getParameter("favoriteCurry");

    if (name != null || favoriteCurry != null) {
        return new OriginalObject(name, favoriteCurry);
    }

    return new OriginalObject("subaru", "goa curry");
}

resolveArgument メソッドで受け取るたくさんの引数の中から webRequest を利用することでリクエストパラメータを取得します。

他にもいろんな情報を取得できますが、リクエストの情報取得は一番よく使用するところでしょう。

まとめ

今回は HandlerMethodArgumentResolver を使用して簡単な実装をすることで、コントローラーのメソッドで引数を受け取る仕組みを解説してみました。
ミニマム実装なので大したことはしてないですが、イメージ湧いたでしょうか?
より複雑な実装も、またの機会に掘り下げていきたいですね〜。
ではまた。。

th:field と th:object によるフォームバインディング機能(inputタグ・radio編)

th:fieldth:object が提供する機能説明の続きです。
今回は inputタグの type="radio" をみていきましょう。

th:fieldth:object の提供機能はタグや属性値によって異なります。
サポートされているタグの一覧はこちら

casual-tech-note.hatenablog.com

inputタグ・type="radio" における提供機能

input タグの type="radio" における th:fieldth:object の提供機能を示していきたいと思います。実は inputタグ・type="radio" のカテゴリー型における提供機能と、とても似ています。
→ 参考: inputタグ・type="checkbox" における提供機能

f:id:shin-kinoshita:20181027180045p:plain

  1. id 属性th:field に指定した変数名を 連番付き で代入
    → 詳細は 「連番付きの id 属性」を参考

  2. name 属性th:field に指定した変数名を代入

  3. checked 属性 の設定
    value に指定された値とフィールドに指定した値が同じであるなら checked 属性 が付与される。
    → 詳細は 「checked 属性の設定」を参考

  4. value 属性 の設定
    別途 value 属性で指定することが必須となる。
    → 詳細は 「カテゴリー型における value 属性値の設定」を参考

サンプルコード

毎回のことながら、簡単なサンプル画面とコードをみていきましょう!
f:id:shin-kinoshita:20181021165714p:plain

名前と性別を尋ねます。
性別によって色と敬称を変えてみました!というページになります。
Input Page と Output Page の実際のコードは次の通りです。

<!-- Input Page -->
<h1>Input Page</h1>
<form action="/binding/input/radio" method="post" th:object="${radioForm}">
  <p>name</p>
  <input type="text" th:field="*{name}"/>
  <p>gender</p>
  <input type="radio" th:field="*{gender}" value="male"/>
  <label th:for="${#ids.prev('gender')}" th:text="male"></label>
  <input type="radio" th:field="*{gender}" value="female"/>
  <label th:for="${#ids.prev('gender')}" th:text="female"></label>
  <br/>
  <input type="submit" value="submit"/>
</form>
<!-- Output Page -->
<h1>Output Page</h1>
<p>Welcome for visiting this page,
  <span th:text="${radioForm.gender == 'male' ? 'Mr.' : 'Ms.'} + ${radioForm.name}"
        th:style="${radioForm.gender == 'male' ? 'color: blue;' : 'color: red;'} + 'font-size: 20px'"></span>
</p>

結果 Input Page のレンダリング結果は次のようになります。
f:id:shin-kinoshita:20181021163511p:plain

デフォルトで checked 属性が male のラジオボタンに付与されていますね。
これは、フォームオブジェクトの gender フィールドに "male" を初期値として与えることで付与されます。

public class RadioForm {
    private String name;
    // これで male のラジオボタンはチェックされた状態でレンダリング
    private String gender = "male";  

    public void setName(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setGender(String gender) {
        this.gender = gender;
    }

    public String getGender() {
        return gender;
    }
}

今回も念のため、完全版フロントエンドバックエンドコード。

カテゴリー型チェックボックスとの違い

今回紹介している inputタグ・type="radio" における提供機能は本当にカテゴリー型のチェックボックスと似ています。なのでここでは異なる点を取り上げてみます。
ぜひカテゴリー型のチェックボックスと比較してみてください!

対応するフィールドの型

すでに少しバックエンドのコードも見ましたが、性別を受け取るためのフィールド gender は String 型を採用しています。カテゴリ型チェックボックスであれば、ここは List などの Collection を採用していました。

// ラジオボタンでのフィールド例
private String gender = "male"; 
// チェックボックスでのフィールド例
private List<String> favoriteFruits = Collections.singletonList("apple");

なぜ型が異なるかという理由は単純で、ラジオボタンは必ず一つの値しか格納しないためです。

今回の例では、性別は男と女しかなく、必ず片方が選択されることを想定しています。

デフォルト値の設定

「対応するフィールドの型」での話とも関連しますが、ラジオボタンでは必ず一つの値が格納されていることを想定しています。複数の値を格納することもないですが、同時に空の状態も想定していません。

そのため 必ずデフォルト値を格納しておくことが望ましいです。

初期値を特に設定していなければ gender の値には null が格納されます。 つまり gender の値が "male" でも "female" でもないため、どちらのラジオボタンもチェックされていない状態でレンダリングされることになります。バリデーションの実装をしていない状態ではこのまま選択せずに送信することも可能です。そのままではヌルポなど、危険な状態になりえるかもしれません。

また、カテゴリー型チェックボックスでは提供されていた type="hidden" の input タグもラジオボタンでは提供されていません。
参考: type="hidden" の input タグ

この input タグは特定のフィールドの値が送信されないことを想定して付与されるというものでした。このタグが付与されないということは、必ずフィールドの値が送信されることを想定しているからだと思われます。

まとめ

チェックボックスラジオボタンは、実用面でも似ていますが、使い分けはしっかり存在します。
複数項目を選択するとか、しないとか。。

実装も似ているんだけど、微妙に違っていて、それはちょうど使い分けの違いと対応するようになっていると、この記事を書いてて思いました。

ラジオボタンにおける提供機能の紹介でしたが、ラジオボタンチェックボックスの違いがより明確になればなお嬉しいです。

th:field と th:object によるフォームバインディング機能(inputタグ・checkbox編)

th:fieldth:object が提供する機能説明の続きです。
今回は inputタグの type="checkbox" をみていきましょう。

th:fieldth:object の提供機能はタグや属性値によって異なります。
サポートされているタグの一覧はこちら

casual-tech-note.hatenablog.com

inputタグ・type="checkbox" における提供機能

input タグの type="checkbox" における th:fieldth:object の提供機能を次に示します。チェックボックスにおける提供機能は、他と比べても多機能で複雑な部類に入るでしょう。

各機能の詳細をさっと見たいなら、各々リンクをたどるのがおすすめです!

  • id 属性th:field に指定した変数名を 連番付き で代入
    → 詳細は 「連番付きの id 属性

  • name 属性th:field に指定した変数名を代入

  • checked 属性 の設定
    value に指定された値とフィールドに指定した値が同じであるなら checked 属性 が付与される。
    → 詳細は 「checked 属性の設定

  • inputタグ・type="hidden" の付与
    → 詳細は 「inputタグ・type="hidden" の付与

そしてさらに value 属性 の設定にも関与しますが、th:field で指定するフィールドの型が Boolean または boolean かどうかによって提供機能は変化します。

サンプルコードと2種類の checkbox

th:fieldth:object の提供機能はフィールドの型(Boolean または boolean なのか、そうでないのか)によって異なると述べましたが、どのように型を選択するのがいいのでしょうか? 。使い分けもありますが、それぞれの実装イメージを掴むためにもう少し深ぼってみます。

そこで次のようなサンプル画面を作ることを考えましょう。 f:id:shin-kinoshita:20181003234439p:plain

この中に好きな果物はありますか?
何個選んでもいいですよ(選択肢をもっと増やしたい。。)という画面です。

フィールドの型が Boolean または boolean であるかどうかで大きく変わってくるので、ここではしっかりと次のように区別することにします。

  • カテゴリー型: 型が Boolean でも boolean でもないフィールド
  • 要素型: 型が Boolean または boolean であるフィールド

なぜこのような名前にしたのか。。
今回の例では次のような対応関係だと考えてください。

  • カテゴリー: 好きな果物
  • 要素: apple、banana、mango

つまり、入力を受け取り格納するフィールドを各要素にするのか、それともカテゴリーにしてしまうのかということにそれぞれ対応しています。

どちらのフィールド設計でもサンプル画面は実装可能です。
では、実際の実装をそれぞれ見ていきましょう。

カテゴリー型による実装

今回のサンプル画面におけるカテゴリーは「好きな果物」ということでした。
フィールド名を fruits としてフォームクラスを実装していきます。

public class CheckboxCategoryForm {
    private List<String> fruits;

    public void setFruits(List<String> fruits) {
        this.fruits = fruits;
    }

    public List<String> getFruits() {
        return fruits;
    }
}

チェックボックスは複数選択可能であるため、複数要素を格納できる Collection を fruits の型として選択するのが良いでしょうラジオボタンのように一つしか選択できないなら Collection 型にする必要はないのですが)。ここでは List<String> を選択、つまり好きな果物の要素(apple、banana、mango)は String 型で受け取るということにしています。

次は入力画面の view の実装です。

<form action="/binding/input/checkbox/category" method="post" th:object="${checkboxCategoryForm}">
  <p>What is your favorite fruit?</p>
  <div>
    <label th:for="${#ids.next('fruits')}" th:text="apple"></label>
    <input type="checkbox" th:field="*{fruits}" value="apple"/>
  </div>
  <div>
    <label th:for="${#ids.next('fruits')}" th:text="banana"></label>
    <input type="checkbox" th:field="*{fruits}" value="banana"/>
  </div>
  <div>
    <label th:for="${#ids.next('fruits')}" th:text="mango"></label>
    <input type="checkbox" th:field="*{fruits}" value="mango"/>
  </div>
  <input type="submit" value="submit"/>
</form>

結果、実際のレンダリングは次のようになります。
f:id:shin-kinoshita:20181007011255p:plain

要素型による実装

今回のサンプル画面では apple、banana、mango の3つの要素を準備する必要があります。 要素型での実装だと、入力を格納するためのフォームクラスは次のようになります。

public class CheckboxElementForm {
    private Boolean apple;
    private Boolean banana;
    private Boolean mango;

    public void setApple(boolean apple) {
        this.apple = apple;
    }

    public Boolean getApple() {
        return apple;
    }

    public void setBanana(Boolean banana) {
        this.banana = banana;
    }

    public Boolean getBanana() {
        return banana;
    }

    public void setMango(Boolean mango) {
        this.mango = mango;
    }

    public Boolean getMango() {
        return mango;
    }
}

対して、入力画面はこんな感じでしょうか。

<h1>Input page</h1>
<form action="/binding/input/checkbox/element" method="post" th:object="${checkboxElementForm}">
  <p>What is your favorite fruit?</p>
  <div>
    <label th:for="${#ids.next('apple')}" th:text="apple"></label>
    <input type="checkbox" th:field="*{apple}"/>
  </div>
  <div>
    <label th:for="${#ids.next('banana')}" th:text="banana"></label>
    <input type="checkbox" th:field="*{banana}"/>
  </div>
  <div>
    <label th:for="${#ids.next('mango')}" th:text="mango"></label>
    <input type="checkbox" th:field="*{mango}"/>
  </div>
  <input type="submit" value="submit"/>
</form>

レンダリング結果です。 f:id:shin-kinoshita:20181007011322p:plain

二通りの実装を一気にとりあえず書きました(息切れしていないですか?)。
ここからはサンプルコードを通して、inputタグ・type="checkbox" における提供機能を一つ一つ、より深く見ていきます。

連番付きの id 属性

レンダリング結果を順に見ていきます。
まずは id 属性 ですが th:field に指定した変数名に連番をつけた値 となっています。

<!-- カテゴリー型 apple チェックボックスの実装 -->
<div>
  <label th:for="${#ids.next('apple')}" th:text="apple"></label>
  <input type="checkbox" th:field="*{apple}"/>
</div>

f:id:shin-kinoshita:20181014123850p:plain
レンダリング結果

この機能は2種類の checkbox のなかでも カテゴリー型 向けの機能です。

基本入力系では、 name 属性も value 属性も対応するフィールド名が入ってました。そして今回の例ではどのチェックボックスも対応するフィールドは fruits フィールドです。 カテゴリー型のチェックボックスでは、 name 属性がどれも同じで value 属性は異なるものを複数レンダリングしないといけないことがほとんどです。 でも id 属性は重複が許されないので、どのチェックボックスにも同じ fruits を指定するわけにはいきません。これを回避するための連番ということですね。

value 属性の設定

既に述べた通り value 属性値の設定は、カテゴリー型か要素型かによって異なります。

カテゴリー型における value 属性値の設定

カテゴリー型ということはフィールドの型は Boolean でも boolean でもないケースです。
カテゴリー型の場合は th:field とは別に value 属性値を指定してあげる必要があります。
例としてカテゴリー型によるサンプル画面の実装から appleチェックボックスの実装を抜粋します。

<!-- カテゴリー型 apple チェックボックスの実装 -->
<div>
  <label th:for="${#ids.next('fruits')}" th:text="apple"></label>
  <input type="checkbox" th:field="*{fruits}" value="apple"/>
</div>

f:id:shin-kinoshita:20181014123850p:plain
レンダリング結果

ここで実装しているのは appleチェックボックスですが th:field に指定した fruits だけでは apple の情報が一切含まれていません。そのため、value 属性値に apple を別途指定してあげる必要があるのです。

基本入力系では th:field 属性と th:value 属性を併用して実装した場合 th:field 属性 の方が優先されましたがチェックボックスではちゃんと th:value 属性 が優先される仕様になっています。

そしてこの appleチェックボックスをチェックして submit した時、value 属性の値である apple が送信され、フォームオブジェクトの fruits フィールドに格納されます。

要素型における value 属性値の設定

要素型ということはフィールドの型は Boolean または boolean であるケースです。
要素型の場合は value 属性値はかならず true だと決まっています。 Boolean または boolean であるため、チェックされたときの値は一意に決まってしまうのです。

<!-- 要素型 apple チェックボックスの実装 -->
<div>
  <label th:for="${#ids.next('apple')}" th:text="apple"></label>
  <input type="checkbox" th:field="*{apple}"/>
</div>

f:id:shin-kinoshita:20181009213829p:plain
レンダリング結果

例えば appleチェックボックスをチェックして submit した時、value 属性の値である true が送信され、フォームオブジェクトの apple フィールドに格納されます。

checked 属性の設定

th:fieldth:object の提供機能として、value 属性 の値をフィールドが保持した状態でレンダリングされると checked 属性が付与されます。

サンプル画面において、appleチェックボックスchecked 属性を付与され、チェックされた状態でレンダリングすることを考えてみます。カテゴリー型と要素型、それぞれで checked 属性を付与するには、

  • カテゴリー型
    フォームオブジェクトの fruits フィールドに文字列 apple を格納
  • 要素型
    フォームオブジェクトの apple フィールドに true を格納

この状態で入力画面をレンダリングすると checked 属性が付与されます。例としてフォームクラスのフィールドに初期値を与えることで appleチェックボックスがチェックされた状態で表示されるようにして見ましょう。フォームクラスの apple フィールドに次のように初期値を与えることで、チェックされた状態になります。

// カテゴリー型フォームクラスの apple フィールドの変更
private List<String> fruits = Collections.singletonList("apple");
// 要素型フォームクラスの apple フィールドの変更
private Boolean apple = true;

inputタグ・type="hidden" の付与

今回付与されている type 属性が hidden の input タグを観察して見ましょう。
こちらは要素型の input タグですね。

<!-- 要素型 apple チェックボックスの実装 -->
<div>
  <label th:for="${#ids.next('apple')}" th:text="apple"></label>
  <input type="checkbox" th:field="*{apple}"/>
</div>

f:id:shin-kinoshita:20181009213829p:plain
レンダリング結果

name 属性値はフィールド名の先頭に _ が付け足されたものとなっています。これは 該当するフィールドのデフォルト値を設定するためのタグ になります。このタグは、特定のタグがチェックされていない状態で値が送信されなかった時に効果を発揮します。

例として先ほどの checked 属性が付与された appleチェックボックスをもちいて説明します。まず _apple="on" は submit 時にチェックの有無に関わらず必ず送信されます。一方 apple の値はチェックが入っていれば送信され、そうでなければ送信されません。
f:id:shin-kinoshita:20181010231827p:plain

POST により apple_apple の両方が送信されるときは、 _apple は特に意味を持ちませんが、 _apple 属性値のみが送信されたときは、フォームオブジェクトの apple フィールドにデフォルト値を設定してくれます(_apple の値である "on" は特に意味を持ちません)。
ここでいうデフォルト値とは、チェックボックスにチェックが入っていない時に本来フィールドに入っているはずの値です。
つまり、要素型では false が、カテゴリー型では空の Collection です。

この追加されたタグの効力を見るために、hidden タイプの input タグが付与されない実装に改良して比較して見ましょう。
これを実現するために th:field 属性を使わずに実装して見ます。

<!-- 修正後の要素型の実装 -->
<form action="/binding/input/checkbox/element" method="post" th:object="${checkboxElementForm}">
  <p>What is your favorite fruit?</p>
  <div>
    <label th:for="${#ids.next('apple')}" th:text="apple"></label>
    <input type="checkbox" id="apple1" name="apple" value="true" th:checked="*{apple}"/>
  </div>
  <div>
    <label th:for="${#ids.next('banana')}" th:text="banana"></label>
    <input type="checkbox" id="banana1" name="banana" value="true" th:checked="*{banana}"/>
  </div>
  <div>
    <label th:for="${#ids.next('mango')}" th:text="mango"></label>
    <input type="checkbox" id="mango1" name="mango" value="true" th:checked="*{mango}"/>
  </div>
  <input type="submit" value="submit"/>
</form>

注)フォームオブジェクトの apple フィールドに true を代入しておいた状態にしてください(checked 属性値の設定で行った処理です)。

レンダリング結果は hidden タイプの input タグが付与されない点以外は同じです。ここで appleチェックボックスのチェックを外して submit してみましょう。

遷移結果は次のようになるはずです。
f:id:shin-kinoshita:20181004002315p:plain

チェックを外しているのに、apple が好きな食べ物として認識されてしまっています。値が特に何も送信されなければ、フォームオブジェクトのフィールドは値が更新されることもないため初期値のままです。これを防ぐために hidden 属性の input タグでデフォルトの値を送信しているということになります。

まとめ

チェックボックスにおける th:fieldth:object による提供機能、いかがだったでしょうか?
提供機能がてんこ盛りだったので、もうお腹いっぱいかもしれません。。

でもその分、使用した時の効果は大きいです(同じ機能を th:field 使用せずに実装することを考えて見てください。途端に属性値まみれになってしまいます。。)。

だからこそ、ぜひ実際に th:fieldth:object を使用して実装していきたいところでもありますね。
お疲れ様でした〜。

th:field と th:object によるフォームバインディング機能(inputタグ・password編)

th:fieldth:object が提供する機能説明の続きです。
今回は input タグの type="password" をみていきましょう。

th:fieldth:object の提供機能はタグや属性値によって異なります。
サポートされているタグの一覧はこちら

casual-tech-note.hatenablog.com

inputタグ・type="password" における提供機能

パスワード入力を受け付けるinputタグにおける th:fieldth:object の提供機能は以下の三つです。

  • id 属性th:field に指定した変数名を代入
  • name 属性th:field に指定した変数名を代入
  • value 属性空文字 を代入

inputタグ・基本入力系での提供機能と似ていますが、微妙に違いますね。

value 属性 にはフィールドの値は代入されずに必ず空文字が入ります。このような仕様になっている背景をサンプルコードを通して見ていきましょう。

シンプルなサンプルコード

inputタグ・password における th:fieldth:object の提供機能を確認するために次のようなサンプル画面を考えていきましょう。 f:id:shin-kinoshita:20180923193836p:plain

名前とパスワードを入力として受け付け、受け取った値を出力するだけのシンプル画面です。
th:fieldth:object を使用した実際のコードは次の通りです。

<!-- Input Page -->
<h1>Input page</h1>
<form action="/binding/input/password" method="post" th:object="${passwordForm}">
  <p>name</p>
  <input type="text" th:field="*{name}"/>
  <p>password</p>
  <input type="password" th:field="*{password}"/>
  <br/>
  <input type="submit" value="submit"/>
</form>
<!-- Output Page -->
<h1>Output Page</h1>
<p>Thank you for your submitting</p>
<p>Your name is 「<span th:text="${passwordForm.name}"></span></p>
<p>Your password is 「<span th:text="${passwordForm.password}"></span></p>

名前とパスワードの入力値を受け取るためのフォームオブジェクトとして passwordForm を用意しています。パスワード入力のレンダリング結果を見ると、id 属性name 属性th:field で指定したフィールド名、password が代入されていることが確認できます。
f:id:shin-kinoshita:20181007005620p:plain

一方 value 属性 は空になっています( 仮に password フィールドに値が入っていたとして空になります)。

完全なフロントエンドのコードはこちらに用意しておきました。

パスワードのバリデーションチェック

今回の Inputタグの type="password" では、なぜ value 属性 に空文字を代入するのでしょうか?。実はこれによって パスワードのバリデーションチェックに引っかかった際に、フィールド値を空にする手間を省いています。

ここでは例として、 パスワードの入力値は8〜16文字でなければならない という制限をかけることを考えて見ましょう。つまり入力のパスワード長がこの範囲外なら、バリデーションエラーにより再度入力画面に戻されるという仕様をイメージしてください。
f:id:shin-kinoshita:20180923195007p:plain

バリデーションを施している実装を見て見ましょう。
こちらはバックエンドにて実装します(完全版はこちら)。

public class PasswordForm {
    private String name;

    // ここでバリデーションを定義
    @Size(min = 8, max = 16)
    private String password;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}

フォームクラスの password フィールドに @Size を使用することで、文字数制限を実現しています。

そしてバリデーションエラーの有無によって遷移先を変更するする必要があります。これはコントローラーの output メソッドで設定します。

@Controller
@RequestMapping("/binding/input/password")
public class PasswordController {
    @GetMapping
    public String input(Model model) {
        model.addAttribute("passwordForm", new PasswordForm());
        return "binding/input/password/input";
    }

    @PostMapping
    public String output(@Validated PasswordForm passwordForm, BindingResult br) {
        if (br.hasErrors()) {
            // Input Page へ遷移
            return "binding/input/password/input";
        }
        // Output Page へ遷移
        return "binding/input/password/output";
    }
}

バリデーションエラーが存在するときは Input Page へ、存在しないときは Output Page へ遷移するように分岐しています。

またエラーメッセージを表示するために、Input Page の view の実装も修正します。

<h1>Input page</h1>
<form action="/binding/input/password" method="post" th:object="${passwordForm}">
  <p>name</p>
  <input type="text" th:field="*{name}"/>
  <p>password</p>
  <!-- バリデーションエラーはここで表示だよー -->
  <ul th:if="${#fields.hasErrors('password')}">
    <li th:each="e : ${#fields.detailedErrors('password')}" th:text="${e.message}" style="color: red;"></li>
  </ul>
  <!------------------------->
  <input type="password" th:field="*{password}"/>
  <br/>
  <input type="submit" value="submit"/>
</form>

さて、今回のようにバリデーションエラーによってユーザに入力値の修正を求める場合、すでに入力してもらった全項目の値を再入力する手間を防ぐためにそのまま保持する必要があります(今回の例では name の入力値ですね)。具体的には passwordForm を output メソッドの引数で受け取った時点で model に格納されているので、これによって保持しています。

しかしパスワードは中でもデリケートな項目であるため、パスワードの入力値のバリデーション結果に関係なく再入力を促すのが普通です。つまり password フィールドだけ値を初期化したい。こういう事情で、value 属性だけには空文字を設定してくれているというわけです。

まとめ

input タグ・type="password" における th:fieldth:object における提供機能は、value 属性における扱い以外は基本入力系と同じでいわばマイナーチェンジです。
なかなか使っていても見落としがちな機能ですが意外と仕事をしている、そんな黒子役のパスワード入力におけるお話でした。

th:field と th:object によるフォームバインディング機能(inputタグ・基本入力系編)

Spring + Thymeleaf でよく見かける th:fieldth:object
フォームで入力を受け取る際に、ほぼ必ずと言っていいほどセットで使用します。

しかし入門者にとっては、とりあえず書いておけばフォームで値を受け取ることができる構文に見えかねない気もします(少なくとも、自分が最初にお目にかかった時はそんなイメージでした。。。)。

とりあえずルールとして th:object にフォームオブジェクトを、 th:field に対応するフォームオブジェクトのフィールドを書けばいいのはなんとなくわかります。
でもそれ以降の仕組みが一目ではわからず気持ち悪い。。
そんな風に感じていたので、 th:fieldth:object が実際にはどんな機能を提供しているのか、主な機能を解説します。

th:field と th:object がサポートしているタグ一覧

th:fieldth:object はどのタグにでも使用できるわけではありません。
現在デフォルトでサポートされているのは次の4つです。

  • input タグ
  • select タグ
  • option タグ
  • textarea タグ

そして タグによって th:fieldth:object が提供している機能はいくつか異なります。 また input タグに関しては type 属性によっても提供される機能が変わってきます (input タグに関しては type 属性によってはサポートされていないものもあります)。

一方で input タグでサポートしている type 属性における提供機能は大半が同じ だったりします。 今回はこの大半の type 属性群を 基本入力系 と呼ぶこととしましょう。

まずどの type 属性の input タグがこの基本入力系に分類されるのか、以下にまとめてみました(Thymeleaf 3.0現在)。

input タグの type 属性による th:field と th:object の提供機能分類
パターン type 属性一覧
基本入力系 text, hidden, detetime, datetime-local, date, time, month, week, number, range, email, url, search, tel, color
その他 password, checkbox, radio, file

上記の表に含まれない type 属性はサポート外です。
基本入力系のへの提供機能は他のタグとその他に分類されているものと共通している点も多いので、以降こちらを取り上げていきます(その他に分類されるものにも重要なものが多いのでまたの機会に。。)。

基本入力系以外における提供機能は、別記事で紹介します。

サンプルコード

実際にフォームで入力を受け取るための UI にはいろんなものがありますが、最も基本的なものの一つがテキスト入力です。 今回はテキスト入力を受け取り、その内容を出力する単純なプログラムを通して th:fieldth:object の提供機能を見ていきましょう。
つまり基本入力系の中でも input タグの type 属性が text のものを扱っていきます。

具体的には次のような入力画面と出力画面を作ることを考えてみます。

f:id:shin-kinoshita:20180904014409p:plain

例として Input Page にて name に「subaru」、text に「I love india curry」と入力し submit すると Output Page はこのようになります。

これ以上ないほどシンプルな実装です。
以下に実際のコードを書いていきましょう。

<!-- Input Page -->
<h1>Input page</h1>
<form action="/binding/input/text" method="post" th:object="${textForm}">
  <p>name</p>
  <input type="text" th:field="*{name}"/>
  <p>text</p>
  <input type="text" th:field="*{text}"/>
  <br/>
  <input type="submit" value="submit"/>
</form>
<!-- Output Page -->
<h1>Output Page</h1>
<p>Thank you for your submitting</p>
<p>Your name is 「<span th:text="${textForm.name}"></span></p>
<p>Your text is 「<span th:text="${textForm.text}"></span></p>

ここで登場している textForm はコントローラで model に追加した入力値を受け取るためのオブジェクトです。 一応実際のバックエンドフロントエンドの完全版実装も準備して見ました(見るにしてもあまりに単純かもしれませんが。。。)。

th:field と th:object による提供機能

公式のチュートリアルhttps://www.thymeleaf.org/doc/tutorials/3.0/thymeleafspring.html#inputs)によると、th:field は 次のような機能を提供しているとのことです(厳密にはこれ以外にもありますが、本質から外れるので割愛)。

  • id 属性th:field に指定した変数名を代入
  • name 属性th:field に指定した変数名を代入
  • value 属性th:field に指定した変数に格納された値を代入

このことから Input Page の form タグは以下と概ね同じで、書き換えてあげることが可能です。

<!-- Input Page -->
<form action="/binding/input/text" method="post" th:object="${textForm}">
  <p>name</p>
  <input type="text" id="name" name="name" th:value="*{name}"/>
  <p>text</p>
  <input type="text" id="text" name="text" th:value="*{text}"/>
  <br/>
  <input type="submit" value="submit"/>
</form>

元々の Input Page のレンダリングを確認しても、一致することが確認できます。
f:id:shin-kinoshita:20181010223803p:plain

これが th:fieldth:object による基本的な提供機能となります。
他の多くのタグや input の属性値でも 共通しています。

th:field と name, id, value 属性の併用

th:fieldth:object によって id 属性name 属性value 属性 が生成されることはわかりました。 ここで一つ疑問に思うこととして th:field と一緒にこれらの属性を併用して記述した場合はどうなるのでしょうか? どちらかの値が上書きされることは予想できますが、実はここの優先度は属性値によって異なります。 検証用に、以下のような view を用意してみました。

<h1>Input page</h1>
<form action="/binding/input/text" method="post" th:object="${textForm}">
  <p>name</p>
  <input type="text" th:field="*{name}" name="manualName" id="manualId" value="manualValue"/>
  <p>text</p>
  <input type="text" th:field="*{text}"/>
  <br/>
  <input type="submit" value="submit"/>
</form>

レンダリングの結果は次のようになります。
f:id:shin-kinoshita:20181010223503p:plain
この結果からそれぞれの属性についてみてみると、

  • name 属性
    値が name であることから、th:field 属性が優先。
  • id 属性
    値が manualId であることから、id 属性が優先。
  • value 属性
    値が 空の入力値 になっていることから、th:field が優先。

実は id 属性だけ優先度が違うのです。
実際フォームバインディングに使用されるのは name 属性value 属性 のみです(詳しくは後述します)。 id 属性は通常のセレクタとして使用されることを想定しているため、このような優先度になっているものと推測されます。

まとめ

今回は th:fieldth:object を使用するケースの中でも基本的なものを紹介してみました。 th:fieldth:object は構文的に使用しがちということを言いましたが、実際の挙動はタグや属性値によって異なっています。 しっかり挙動を押さえることは、短く可読性よく記述することにも繋がるかと思うので参考になればと思います。