トップ «前の日記(2007-12-08 (土)) 最新 次の日記(2007-12-10 (月))»  

まちゅダイアリー


2007-12-09 (日)

WEB+DB PRESS Tech Meeting

こないだ応募した WEB+DB PRESS のイベントの参加票がメールで届いた。 楽しみだなぁ。

Shibuya.trac のミーティング

Heretic Programmer(2007-12-05)で周知されてた。 1月19日に開催だそうな。 個人的には時期的にいけるかどうかは微妙なところ。

以下、開催趣旨(?)より引用。

Shibuya.tracは、オープンコミュニティを目指したいと思います。どういうことかというと、コミュニティリーダ、理事会は作らない。コミュニティ自身を運営、盛り上げていくのは、コミュニティの成果の開発者、そして全てのユーザ。開発者がユーザより偉いということもないし、ユーザが開発者より偉いということもないフラットな組織を目指していきたいと思います。

フラットな組織ということで、 Yapoo さんが言うように、

Shibuya.tracはcodereposでいいんじゃないか

に一票。

山本 陽平さん監修の REST 本

RESTful Webサービス(Leonard Richardson/Sam Ruby/山本 陽平/株式会社クイープ)

WEB+DB PRESS の REST 連載を担当されている山本さんによる監修の REST 本。 Amazon で予約注文した。 Rails も2.0でかなり REST 指向になっているみたいだし、流れは把握しておこうと思って。

そういえば発売前に予約することって珍しいなぁ。

ActiveRecord で Salted SHA1 を使ってパスワードを保存

ActiveRecordでパスワード(Password)を上手に保存するより。

やはりパスワードをDBに生でいれるのはどうかと思うので、

before_save {|user| user.password = Digest::SHA256.digest(user.password)}

こんな感じに書いてやりましたところ、新規登録時は問題ないのですが、パスワード以外のデータを更新したときにもパスワードが勝手に変わるようになってしまいました。

「パスワード(確認用)」のフィールドを新たに設けて以下のようにすることで解決。

before_save {|user| user.password = Digest::SHA256.digest(user.password) unless user.password_confirmation.blank?}
validates_confirmation_of :password

もうちょっとキレイに書けるはず…と思って ActiveRecord::Base のマニュアルを読んだら、 Overwriting default accessors というのがあった。 なので、 User モデルにこう書けば OK のはず。

def password=(password)
  self[:password] = Digest::SHA256.hexdigest(password)
end

こうすれば password 属性に値を代入するときにハッシュ化してくれるので確実。

ハッシュに salt を付ける

ハッシュ化するだけだと、たまたま2人が同じパスワードを登録した場合に、ハッシュ後の値も同じになる。

>> user1 = User.new(:password => 'kagamine')
>> user2 = User.new(:password => 'kagamine')
>> user1.password
=> "e4450a612f7223eae6db2f8bbb4d9f9e13099059c8e061c922b632b6061c8e05"
>> user2.password
=> "e4450a612f7223eae6db2f8bbb4d9f9e13099059c8e061c922b632b6061c8e05"

MD5破りにGoogleを活用って話もあるし、これじゃちょっとなので、 パスワードの保存に SMD5 (Salted MD5) や SSHA1を使うで書いたように salt をつけて保存するようにしてみた。(users テーブルに salt という列が必要) salt は8バイトのデータをランダムに生成するようにしている。

require 'digest/sha2'

class User < ActiveRecord::Base
  def salt
    self[:salt] ||= OpenSSL::Random.random_bytes(8).unpack('H*').first
  end

  def password=(password)
    self[:password] = Digest::SHA256.hexdigest(password + salt)
  end
end

ユーザごとに salt が作られるので、2人が同じパスワードを使っていても、 DB には違う値が格納される。

>> user1 = User.new(:password => 'kagamine')
>> user2 = User.new(:password => 'kagamine')
>> user1.salt
=> "2403f75bfb83d74c"
>> user1.password
=> "5f4d5a848dfa49f321d1318a1ac5b8404a8da7045e1365bf4376a0f503228c56"
>> user2.salt
=> "5ee895b472a073a1"
>> user2.password
=> "97cea15a4a8ca748ef5b00110fe41a91fe418518269bf5e74c747b0ac09f55f2"

パスワードの比較のために password_match? メソッドを作った。 同じ salt と password を持つ User オブジェクトを作って比較するという、ちょっと手抜きな処理。

  def password_match?(password)
    self.password == User.new(:salt => salt, :password => password).password
  end

これで OK。

>> user1.password_match?('kagamine')
=> true
>> user1.password_match?('miku')
=> false

補足

  • 思いつきで作ったので、間違ってたらごめんなさい
  • これを使うよりも login_generator を参考にしたほうがいいかも?
    • ちゃんと確かめてないけど、あっちは salt が全ユーザで共通だったような…

それから、本当に使うときは使用したハッシュアルゴリズム (MD5 とか SHA1 とか) も User オブジェクトに格納していた方がいいと思う。

User.new(:password => 'kagamine', :hash_alg => 'MD5')
User.new(:password => 'kagamine', :hash_alg => 'SHA1')

みたいにね。 5年も使えば、他のアルゴリズムに切り替える必要もでてくるだろうし、その時にユーザごとにアルゴリズムを切り替えられないと、データの移行ができなくなるから。 データの移行ってのは MD5 のハッシュ値を SHA1 のハッシュ値に変換するって意味じゃない(それは無理)。 MD5 でハッシュ化するユーザと SHA1 でハッシュ化するユーザが同じシステムで共存できるって意味ね。次にパスワードを変えたときに、新しいハッシュアルゴリズムを使うようにして、少しずつ移行していく。

疑問

Rails でのユーザ管理のスタンダードってあるのかな? やっぱり login_generator を使っているんだろうか。

追記

Twitter で acts_as_authenticated があるよって教えてもらった。 yugui さんのRails勉強会@東京 第22回に詳しく書かれている。 password と crypted_password の2つの属性を持つようにしておいて、 password と salt から生成した crypted_password だけを DB に格納するようになってる。 確かに、フォームでのエラー時にパスワードを自動補完しようと思ったら、生のパスワードも必要だよなぁ。

本日のツッコミ(全2件) [ツッコミを入れる]
bottleneck (2007-12-10 (月) 12:50)

ハッシュ化をそのタイミイングで行うと、パスワード文字列のチェックができなかったんですよね。validates_lenght_ofで字数チェックしたりしたかったので。saltの件はなるほどです。勉強になりました。

まちゅ (2007-12-10 (月) 17:26)

なるほど。僕も勉強になりました。<br>モデルには password と crypted_password の両方を持たせて、 DB には crypted_password だけを格納するのがよさそうですね。