ちょっと前に「Javaで乱数を生成する」というタイトルで、Javaで疑似乱数を生成する方法について書いてきた。
- Javaで乱数を生成する(1) : 疑似乱数生成器の動作概要
- Javaで乱数を生成する(2) : JavaのAPIの紹介
- Javaで乱数を生成する(3) : Tomcatのソースを読む準備
- Javaで乱数を生成する(4) : Tomcatでの実装方法
予測困難な疑似乱数を得るためには、疑似乱数生成器を用いるにせよ、何らかのノイズ(エントロピ)が必要となることがわかった*1。正直、面倒。疑似乱数を生成しなくて済むならば、是非そうしたい。Webアプリケーションであれば、アプリケーションサーバやフレームワークが(きっと推測困難な)セッションIDを発行しているので、それを用いればよい。下手に自作しない方がよい。
セッションIDを用いれば(そのまま、あるいは加工して用いれば)よい例としては、Webアプリケーションにおいてよくある“画面遷移強制機能(処理の直前に、必ずある画面を経由させるための機能)”というか、“二重送信防止機能(あるコンテキストで、サブミットのリクエストが複数回送信されても、処理は1回しか実行しない機能)”というか、そういう機能が身近でよいかな。
仕組みとしては、画面内に何らかの値(ここではトークンと呼ぶことにする)を埋め込んでおいて、その画面のフォームがサブミットされた時に、リクエストに含まれるトークンとサーバ側で保持していたトークンとが一致している場合だけ処理を実行する。トークンが間違っていると処理されないため、Webアプリケーションの利用者は、必ずトークンを埋め込んだ画面からサブミットすることが必要となる。また、トークン一致後にサーバ側で保持していたトークンをクリアすることで、利用者がたくさんリクエストを送信したとしても、2番目以降のリクエストは(トークンが正しくても)処理されない。
このような機能を実現するために、Struts 1.xには、トランザクショントークン(Transaction Token)という仕組みが用意されている。例えば Struts 1.3.10 では、次のようにして毎回異なるトークンを生成している。
/** * Generate a new transaction token, to be used for enforcing a single * request for a particular transaction. * * @param request The request we are processing */ public synchronized String generateToken(HttpServletRequest request) { HttpSession session = request.getSession(); return generateToken(session.getId()); } /** * Generate a new transaction token, to be used for enforcing a single * request for a particular transaction. * * @param id a unique Identifier for the session or other context in which * this token is to be used. */ public synchronized String generateToken(String id) { try { long current = System.currentTimeMillis(); if (current == previous) { current++; } previous = current; byte[] now = new Long(current).toString().getBytes(); MessageDigest md = MessageDigest.getInstance("MD5"); md.update(id.getBytes()); md.update(now); return toHex(md.digest()); } catch (NoSuchAlgorithmException e) { return null; } } /** * Convert a byte array to a String of hexadecimal digits and return it. * * @param buffer The byte array to be converted */ private String toHex(byte[] buffer) { StringBuffer sb = new StringBuffer(buffer.length * 2); for (int i = 0; i < buffer.length; i++) { sb.append(Character.forDigit((buffer[i] & 0xf0) >> 4, 16)); sb.append(Character.forDigit(buffer[i] & 0x0f, 16)); } return sb.toString(); }
ほらね。セッションIDを使っているでしょ(ソース見る前からそうだと思っていたよ)。セッションIDと時刻のメッセージダイジェスト(ハッシュ値)をトークンにしている。たまたま同じ時刻が取れてしまったら、時刻をインクリメントしているから、結局、毎回異なるトークンが生成されるというわけだ。
セッション内で済むなら、ムダに疑似乱数を発生させず、セッションIDを使っていこう。
ちなみに、データベースを更新する処理の前に必ずこのトークンチェックを実施するという仕組みは、自分のまわりでは、2003年頃には既に開発現場に浸透していたので、CSRF(クロスサイトリクエストフォージェリ)対策で困ったことはなかった*2。