カジュアルな技術ノート

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

【Spring Security】Spring Security の csrf トークンの仕組み

f:id:shin-kinoshita:20190406183111p:plain
Spring で開発した web アプリケーションの csrf 対策を行うときは Spring Security のトークン生成機能を利用することが多いと思います。
今回はこの csrf トークン生成の流れや仕組みを扱っていきます。

この記事で扱うこと

csrf トークン付与の方法

以下、すでに Spring Security をアプリケーションに導入済みという前提で話を進めます。

Spring Security を有効にしていればデフォルトで csrf トークンは生成されるようになっています。
必要なことは生成されているトークンを POST 時に送信されるようにリクエストに付与するだけです。

次にサンプルコードとして、csrf トークンが付与された POST リクエストを送信する form タグを Thymeleaf で書きます。

<form method="post" action="/csrf/passed">
  <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
  <input type="submit" value="submit" />
</form>

また form タグならば次のように th:action を使用するだけで付与することもできます。

<form th:action="@{/csrf/passed}" method="post">
  <input type="submit" value="submit" />
</form>

どちらのコードも次のようにレンダリングされます。

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

_csrf.parameterName と _csrf.token

ここまでで csrf トークンの付与の方法はわかりましたが突然 _csrf..... といった見覚えのない変数が出てきましたね。。
これは一体どこから現れたのでしょうか?
この _csrf は HttpServletRequest の属性値に格納されているオブジェクトです。
よって controller クラスでもこれらの値を参照することができます。

@GetMapping
public String index(HttpServletRequest request) {
    CsrfToken csrf = ((CsrfToken)request.getAttribute("_csrf"));
    System.out.println(csrf.getParameterName());
    System.out.println(csrf.getToken());
    return "index";
}

Thymeleaf でも同じく HttpServletRequest からこれらの値にアクセスしていたというわけです。

csrf トークンの裏側の仕組み

ここまでは view や controller クラスでの csrf トークンの扱いを見てきましたが、普段の開発で意識しないより裏側の部分を見ていきます。

Spring Security の csrf トークンの裏側での処理を追うにあたって、登場人物は主に次の二つです。

  • CsrfFilter
    フィルター処理で csrf トークンに必要な諸々の処理を担当しているクラス
  • CsrfTokenRepository
    csrf トークンを生成したり保持したりしているクラス

裏側の処理を追うなら CsrfFilter クラスを見ていけばいいですが、内部で CsrfTokenRepository クラスを参照しています。
具体的には CsrfFilter が行う処理は以下の二つです。

トークンを HttpServletRequest の属性値に格納

すでに触れた HttpServletRequest に csrf トークンを格納する処理です。
csrf トークンの値自体は CsrfTokenRepository から取得しています。

csrf トークンの照合

ここまでは csrf トークンを埋め込む部分に関する話でしたが、無事に埋め込んだトークンがちゃんとリクエストで送信されているかどうかの確認処理です。
この csrf トークンの照合は更新系の Http メソッドのみ(GETHEADTRACEOPTIONS 以外)で適応されます。
リクエストで飛んできた csrf トークンと CsrfTokenRepository のトークンを比較し、同じであれば controller クラスに到達し通常通り view を返します。
異なれば例外をスローし、デフォルトではエラー画面が表示されます。

まとめ

簡単ながら csrf トークン付与の方法と裏側の処理の話を書きました。
csrf トークン周りに関してブラックボックスに感じている人がいれば参考になればと思います。

メールの事故から考えるメールライブラリとテンプレート

先日の jflute さん勉強会で MailFlute のソースコードリーディングをやりました。
今回はメールライブラリとテンプレートのお話です。

java でメール送信するときの手順を考えると、

  1. Velocity などのテンプレートエンジンを使用してメール本文を完成
  2. Java Mail などのメールライブラリを使って送信

という流れが一般的ではないでしょうか。
そしてここで使うテンプレートやライブラリのことをしっかりと考えたことありますか?
僕はこの勉強会受けるまで一切考えたことなかったです(笑)。

実はメールの特性を考えると、めちゃくちゃ重要だなと感じたので紹介します。

この記事で扱うこと

  • メールの特性
    メールは事故りやすく、事故るとやばいです。
    その理由を取り上げていきます。
  • メール事故を防ぐ仕組みと工夫
    メール事故を防ぐための仕組みや工夫を盛り込んだライブラリとして MailFlute をみていきます。

メールは事故りやすく、事故るとやばい

基本的に web アプリケーション開発をしている現場ということを想定していますが web アプリケーションで生じるバグよりもメールでの事故の方がシリアスであることが多いです
その理由をいくつか考えてみました。

  1. 一度送信すると取り消すことができない
    web アプリケーションで生じたバグはアプリケーションを修正することで改修することができます。
    しかしメールは送ってしまうと取り消すことができません。
    送信相手の名前を間違えて送ってしまったとしましょう。
    やることはバグ修正ではなく、謝罪のメール作成ですね。。
  2. 間違いに気づきにくい
    メールは間違いに気づきにくいです。
    手動で送ったメールならまだしも、アプリケーションが送信したメールを送り手が読むケースはあまりないでしょう。
    そして web アプリケーション上のバグのように多くのユーザーの目に触れることもなく、間違いメールを読むのは受信者のみということが多いです。
  3. ステークホルダーが増え、間違いが生じがち
    メールテンプレート作成するとき、その本文を書くのはセールスなど開発者でない場合が多いです。
    通常テンプレートファイルにはテンプレート変数を埋め込みますが、開発経験がない人は断然間違える可能性も高くなります。
    このように開発者以外のステークホルダーが絡むことで通常のアプリケーション開発と比べるとバグの混入率が高くなるということができるでしょう。

こう考えると恐ろしいですね。。。
メールの事故を防ぐための仕組みや工夫が欲しくなります。
その一例として、メールライブラリやテンプレートで事故防止するということを考えていきましょう。

MailFlute の紹介

ここまであげたようなメール特有の怖さを回避する仕組みや工夫を盛り込んだライブラリとして MailFlute を紹介します。
MailFlute はメールライブラリであり、かつテンプレートエンジンも包含しています。

dbflute.seasar.org

↑から抜粋ですがメール送信時の java コードは次のようなものになります。

WelcomeMemberPostcard.droppedInto(postbox, postcard -> {
    postcard.setFrom("from@example.com", LABELS_OFFICE_MAIL);
    postcard.addTo("to@example.com");
    postcard.setMemberName("sea");
    postcard.setBirthdate(birthdate);
    postcard.addReplyTo("replyto@example.com");
});

MailFlute は DBFlute ライクなコードでメールが送信できるのが特徴です。
いくつか特徴をみていきましょう!

テンプレートごとにクラスを自動生成

上のコードでは WelcomeMemberPostcard が自動生成されたクラスとなります。

WelcomeMemberPostcard.droppedInto(postbox, postcard -> {
    ...
});

テンプレートごとにクラスが生成されるので テンプレートファイルのパスを開発者が指定する必要がありません
間違ったテンプレートが選択されることを防ぐことを目的としているとのことです。

テンプレート変数ごとにメソッドを自動生成

取り上げたコードでは memberName や birthdate をテンプレート変数として指定しています。
MailFlute ではテンプレート変数ごとに固有のメソッドで値を指定するのが特徴です。

WelcomeMemberPostcard.droppedInto(postbox, postcard -> {
    ...
    postcard.setMemberName("sea");
    postcard.setBirthdate(birthdate);
    ...
});

これによって テンプレート変数指定のタイポを防げる、タイプセーフであることから間違った値を指定しにくくなる ことが期待できます。
比較として Velocity だと次のようなコードとなり、MailFlute に比べると間違いが検知しにくくなります。

Map<String, Object> contextMap = new HashMap<>();
contextMap.put("memberName", "sea");
contextMap.put("birthdate", birthdate);
...

コメント必須なテンプレート

これはオプションによって設定できる機能ですが、テンプレートファイルにコメントを必須にすることができます。

/*
 [新規会員メール]
 会員登録を申し込みした時に送信されます。
 注)本登録時のメールではありません。
*/
subject: Welcome to your sign up, /*pmb.memberName*/
option: +html
-- !!List<Integer> seaList!!
>>>
Hello, /*pmb.memberName*/

コメントは /* ... */ にあたるところですね。
ステークホルダーが多いメールテンプレートだと、認識に齟齬が生じやすくなると思います。
だからこそメールに関する情報や注意事項をコメントに残しておくことが重要という思想から生まれた機能です。

まとめ

今回みてきた MailFlute の機能一つ一つはとても小さな工夫です。
しかし事故を防ぐために必要なこともまた一つ一つの小さな工夫の積み重ねだと思います。
そしてできるならそのような工夫も仕組みとして導入した方が現場のストレスがより少なく安全な状態を生み出すことができると思います。
そんな事例の一つとして、メールライブラリとテンプレートについて参考にしていただけると嬉しいです。

O/R マッパー比較・入門編にて

今週の jflute さん勉強会で O/R マッパーの比較・入門編をやっていただきました。
新卒入社してそろそろ1年ですが DBFlute しか使ったことがない僕としては、他の O/R マッパーに関してはほぼ知識ゼロ状態。。
初めて聞く話が多かったですが、その一部を書いていきます。

使用した資料

Masatoshi Tada さんのスライドをみながら jflute さん流の解説で進めていただきました。

www.slideshare.net

O/R マッパーの分類

上記資料から完全抜粋ですが O/R マッパーを4分類しています。

  • JDBC ラッパー型
    JDBC をゆるくラップしたタイプ。
    例) Spring JDBC
Employee employee = jdbcTemplate.queryForObject(
  "SELECT id, name, salary FROM employee e WHERE e.id = ?",
  new Object[] { 1 },
  new RowMapper<Employee> () {
    public Employee mapRow(ResultSet rs, int rowNum) throws SQLException {
      int id = rs.getInt("id");
      String name = rs.getString("name");
      BigDecimal salary = rs.getBigDecimal("salary");
      return new Employee(id, name, salary);
    }
  }
);
  • SQL マッパー型
    SQL とクラスの詰め替えに特化したタイプ。
    例) MyBatis
<mapper namespace="com.example.mapper.EmployeeMapper">
  <select id="findById" resultType="Employee">
    SELECT e.id AS id,
           e.name AS name,
           e.salary AS salary
    FROM employee e
    WHERE e.id = #{id}
  </select>
</mapper>
Employee employee = employeeManager.findById(1);
  • クエリビルダー型
    SQL を直接書かずに対応したクラスやメソッドでデータ取得するタイプ。
    例) jOOQ
Result<Record> employee = create.select(EMPLOYEE.ID, EMPLOYEE.NAME, EMPLOYEE.SALARY)
                                .from(EMPLOYEE)
                                .where(EMPLOYEE.ID.equal(1))
                                .fetch();
  • OR マッパー型
    RDB を全く意識させずにデータを取得するタイプ。
    例) JPA
Employee employee = entityManager.find(Employee.class, 1);

さて、これらの O/R マッパーの特徴を学習コストとコードの書き方という視点でまとめてみました。

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

あくまでざっくりとですが、JDBC ラッパー型に近づくほど備わっている機能が少なく学習コストが低い。
OR マッパー型に近づくほどより SQL を意識せずにデータ取得ができ、その分学習コストが高いという特徴を持ちます。

開発者を問わない SQL マッパー型

SQL マッパー型の代表格が MyBatis ですが、Java の O/R マッパーの中でもかなりのシェアを獲得しています。
その大きな理由の一つが SQL マッパー型の特徴である学習コストの低さ です。
MyBatis でデータを取得する際、取得条件は外部の SQL ファイルに書くのが基本です。
つまり DB アクセスの基本である SQL を知っていればほとんど学習コストなく扱うことができます。

jflute さんによると 開発者を選ばずとりあえず DB アクセスをして動くものを作りたいという現場で使われる傾向が強い とのこと。
しかし結果的にですが、誰でも扱える分共通化などがされない傾向があるとのことです。
アプリケーション側の画面が違えばほとんど同じ SQL なのに共通化していない現場が多くなってしまいがちなのだとか。

クエリビルダー型と OR マッパー型の比較

クエリビルダー型も OR マッパー型も SQL を直接書くことはせず、メソッドのみでデータ取得する点は共通しています。
大きな違いは 開発者がテーブル構成を意識するメソッドになっているかどうか です。
クエリビルダー型は完全に SQL に則った書き方になっているのでテーブル構成も意識する必要が出てきます。
一方でテーブル構成を意識することなく短いコード量でデータ取得が可能なのが OR マッパー型のメリットです。

しかし OR マッパー型で DB アクセス時のパフォーマンスを気にする必要があるケースでは N + 1 問題などが発生しやすくなります
OR マッパー型でもこれを避けるための実装することはもちろんできますが、基本 RDB を意識しないためいきなりパフォーマンスを意識するのも難しそうだなと感じます(あくまでクエリビルダー型である DBFlute を普段使っている僕の意見ですが、、)。

ちなみにこの理由から DBFlute は LazyLoad の採用をやめたそうです。
DBFlute で関連テーブルからデータを取得するときは LoadReferrer を使うことで一度の SQL で一気に取得するという思想になっています。

dbflute.seasar.org

O/R マッパーとは?人によって認識が違う

最後にこれは jflute さんから聞いた話ですが O/R マッパーと聞いて思い浮かべるものには人によってズレがある とのことです。
O/R マッパーの本来の意味を調べてみると

「アプリケーションで扱うオブジェクトとリレーショナルデータベースのデータをマッピングするもの」

なので機能としては少なくても JDBC ラッパー型も立派な O/R マッパーということができます。
一方で O/R マッパーはオブジェクトとリレーショナルデータベースをマッピングするものだから、テーブル構成等を意識させない OR マッパー型こそ真の O/R マッパーだと認識している人もいるとのことです。
そのため O/R マッパーについて話をするときは、どんな O/R マッパーについて話をしているのか認識を合わせることが大切 ということです。

【DBFlute】SQL はどう作られてるの?ConditionBean と Behavior の責務

http://dbflute.seasar.org/image/parts/top/logo.png

ビズリーチで開いていただいてる隔週の jflute さん勉強会。
先日 DBFlute の ConditionBean のソースコードリーディングをやっていただきました。
ConditionBean のソースコードを読み進める中で、内部構造のこととかいくつか興味深い話があったのでまとめますー。

この記事で扱うこと

  • 表示 SQL ってどこで生成されているのか
  • SQL 生成における ConditionBean と Behavior の責務

DBFlute の表示 SQL

DBFlute 使ってガシガシ DB アクセスしている人ならとても馴染みの深い機能である SQL の表示機能。
例えばこんなコードで欲しいデータを表示させると。。

memberBhv.selectEntity(cb -> {
    cb.query().setMemberId_Equal(1);
    cb.query().setMemberName_LikeSearch("S", LikeSearchOption::likePrefix);
});

ログにはこんな感じで、対応した SQL が表示されます。

select dfloc.MEMBER_ID as MEMBER_ID, dfloc.MEMBER_NAME as MEMBER_NAME, ....
  from MEMBER dfloc
 where dfloc.MEMBER_ID = 1
   and dfloc.MEMBER_NAME like 'S%' escape '|'

指定した ConditionBean が期待した通りになっているのか SQL から確認できるという便利な機能です。
この SQL って一体どこで作られているのでしょうか?
実は完全に ConditionBean の責務となっていて ConditionBean 単体で toDisplay メソッドによって SQL を表示させられます!

MemberCB memberCB = new MemberCB();
memberCB.query().setMemberId_Equal(1);
memberCB.query().setMemberName_LikeSearch("S", LikeSearchOption::likePrefix);
System.out.println(memberCB.toDisplaySql());

本家のドキュメントを見るとこちらに詳細があります。

dbflute.seasar.org

ConditionBean -> 2WaySQL -> 表示 SQL

さてこの toDisplaySql メソッドの中を見てみるとわかるのですが、ConditionBean から表示 SQL を直接生成しているのではなく一度 2WaySQL に変換しています。
実際に対応する 2WaySQL は次で表示できます。

MemberCB memberCB = new MemberCB();
memberCB.query().setMemberId_Equal(1);
memberCB.query().setMemberName_LikeSearch("S", LikeSearchOption::likePrefix);
System.out.println(memberCB.getSqlClause().getClause());

表示される 2WaySQL です。

select/*$pmb.selectHint*/ dfloc.MEMBER_ID as MEMBER_ID, dfloc.MEMBER_NAME as MEMBER_NAME, ....
  from MEMBER dfloc
 where dfloc.MEMBER_ID = /*pmb.conditionQuery.memberId.fixed.query.equal*/null
   and dfloc.MEMBER_NAME like /*pmb.conditionQuery.memberName.varying.likeSearch.likeSearch0*/null escape '|'

この 2WaySQL をさらに表示 SQL 変換しているという処理の流れです。

ConditionBean と Behavior の責務

なぜ一度 2WaySQL に変換するのか。。
jflute さんによると、そもそも ConditionBean は指定した絞り込み条件を 2WaySQL に変換することが大きな責務となってるため、それならば 2WaySQL から変換した方が簡単であるためとのことです。
そして生成された 2WaySQL を Behavior クラスに渡して、実行用の SQL に変換し実際に DB に実行してもらうと言う設計になっています。

つまり SQL 生成については

f:id:shin-kinoshita:20190310204103p:plain
SQL 生成での ConditionBean と Behavior の責務
という役割分担と言うことです。

なぜ ConditionBean は 2WaySQL を生成して Behavior クラスに渡すのでしょうか?
これは DBFlute は DB に対して SQL を実行するときはバインド変数を使用していますが、その時の変数のパースが 2WaySQL だとやりやすかったため、採用したとのことでした。
ちなみに外だし SQL と比較してみるとどちらも 2WaySQL を生成することを行なっていますが、

  • ConditionBean: ConditionBean クラスが 2WaySQL を生成
  • 外だし SQL: 開発者が手動で 2WaySQL を生成

という違いになっているとのことです。

まとめ

  • 表示 SQL は ConditionBean で生成
  • ConditionBean の責務は 2WaySQL を生成すること
  • Behavior の責務は 2WaySQL を実行 SQL に変換して実行すること

知っているから即役立つと言ったものでもないですが DBFlute の内部構造のお話でした。

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 Me 認証
    初回だけしかユーザー名とパスワードを入力しないパスワード認証方法。
  • 代理ログイン認証
    別のアカウントとして代理でログインする認証方法。

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

そして認証方法によってログイン済みのユーザーかを判断するために必要な情報も異なります。
これに合わせて 今まで扱ってきた 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 を使用して簡単な実装をすることで、コントローラーのメソッドで引数を受け取る仕組みを解説してみました。
ミニマム実装なので大したことはしてないですが、イメージ湧いたでしょうか?
より複雑な実装も、またの機会に掘り下げていきたいですね〜。
ではまた。。