« RailsでWikiクローンを作る | メイン | Wiki新設 »

2005年09月13日

RailsでWikiクローンを作る 12: 認証用ユーザ登録

今のままでは、誰でも管理画面を表示して操作できてしまいます。 それではまずいので、アクションに認証をかけて特定のユーザしか操作できないようにしなければなりません。 認証にはいろいろな方法がありますが、Minki では一般的なユーザ名とパスワードによる認証を行うことにします。

ユーザ認証の仕様は、以下のようにしたいと思います。 なお、標準添付ではありませんが Rails には Login Generatorというものがあります。 しかし、ここではこれを使わずに、仕組みを勉強するため自前で認証機構を作ります。 実際に実用的なシステムをつくる場合は Login Generator を使った方が良いかもしれません。
usersテーブルの作成
では、まずユーザ情報を格納する users テーブルと User モデルを作成します。 users テーブルを作る sqlite3 用のスキーマは以下のようになります。
-- user
create table users (
  id                    integer primary key,
  name                  varchar(10),
  crypted_password      varchar(14)
);

insert into users (name, crypted_password) values ('admin', 'aaLR8vE.jjhss');
name はユーザ名、crypted_password は crypt によって暗号化されたパスワードを保持するカラムです。 また、insert で初期ユーザとして admin というユーザ名とそのパスワード(パスワードもadmin)を登録しています。

Ruby の String#crypt は、暗号化したい文字列と salt と呼ばれる2バイト以上のランダムな文字列(ただし有効なのは英数字と '.' と '/')から、暗号化された文字列を生成します。 暗号化された文字列から、元の文字列を求めることは極めて難しいとされています。
'admin'.crypt('aa')  #=> "aaLR8vE.jjhss"
saltは2バイト以上あっても先頭の2バイトだけが使われ、それが暗号化された文字列の先頭に付加されます。 よって、ユーザが入力したパスワードが password という変数に、暗号化されたパスワードが crypted_password という変数に入っている場合、
password.crypt(crypted_password) == crypted_password
が成り立てば、ユーザが入力したパスワードは crypted_password の元の文字列と等しいということになります。

ActiveRecord の callback hook
Userモデルはいつも通り generate で生成します。
% ruby script/generate model User
その後、生成されたファイル app/models/user.rb に少し手を加えます。
class User < ActiveRecord::Base
  attr_accessor :password
  validates_uniqueness_of :name
  validates_presence_of :name, :password
  validates_format_of :name, :with => /^[-\w]+$/

  def self.crypt_password password
    salt = [[rand(4096)].pack('v')].pack('m').tr('+', '.')
    password.crypt salt
  end

  def before_create
    self.crypted_password = User.crypt_password(@password)
  end

  def after_create
    @password = nil
  end
end
DBのテーブルには crypted_password のフィールドしかなく、生の(暗号化される前のplain textな)パスワード文字列を保存するところはありません。 しかし、アクションでユーザからの入力を受けとるのに生パスワードのフィールドが必要なので、attr_accessor によって、User クラスに password という属性を追加します。 これで、みかけ上Userモデルにはid, name, crypted_password, password という四つのカラムがあるように見えます。 もちろん password 属性の値はメモリ上にのみ存在し、DBに保存されることはありません。
また、ついでに validation 用のメソッドもいくつか追加しておきます。 validationのメソッドについて、詳しくは http://api.rubyonrails.com/classes/ActiveRecord/Validations/ClassMethods.htmlを参照してください。

User のクラスメソッド self.crypt_password は、salt をランダムに生成し、引数 password を crypt で暗号化します。 すぐ後に出てくるbefore_createで使用します。

before_create と after_create は、ActiveRecord の callback hook と呼ばれているものです。 callback hook にはいくつか種類があり、決まった名前のメソッドを定義しておくと、モデルクラスの動作のいろいろな場面で、そのメソッドが実行されるというものです。
例えば before_create は、新しい行がデータベースに保存される前に実行されます。 ここでは、ユーザが入力した暗号化される前のパスワード文字列を crypt して crypted_password 属性に入れています。 after_create は、行がデータベースに保存される後に実行されるメソッドで、ここでは生パスワードを(もう不要なので)消しています。
ユーザ登録・削除アクション
次に、ユーザ登録(追加)・削除を行うための、以下の二つのアクションを作成します。 edit_user と destroy アクションは管理関連なので、どちらも admin コントローラ app/controllers/admin_controller.rb の中に入れることにします。
  def edit_user
    @users = User.find :all
    if request.get?
      @user = User.new
    else
      @user = User.new(params[:user])
      if @user.save
        flash[:notice] = "#{@user.name} を登録しました。"
        redirect_to :action => 'edit_user'
      end
    end
  end

  def destroy
    user = User.find(params[:id])
    user.destroy
    flash[:notice] = "#{user.name} を削除しました。"
    redirect_to :action => 'edit_user'
  end
edit_user では、basic アクションと同じように HTTP の request method によって動作を変えています。 GETメソッドの場合は、User のインスタンスを一つ作ってビューに渡します。 GETメソッドでない場合は POSTメソッドと思われるので、params で受けとったユーザ名・パスワードを元に、新規ユーザを DB に一行追加します。 ユーザが入力するのは名前と(生)パスワードで、@user.save するときに before_create フックによって crypted_password に暗号化されたパスワードが入り、それがDBに保存されるということに注意してください。

destroy は、IDで指定されたユーザを削除します。 削除後は edit_user にリダイレクトしているため、destroy はビューを持ちません。 なお destroy アクションは、呼び出されたら特に確認をせずに即ユーザを削除してしまいます。 destroy 自身は認証をかけるので admin でログインしているユーザ以外は呼び出せませんが、いわゆるCSRF攻撃を受けると問題が発生する可能性があります (Railsのscaffoldにも同じ問題があります)。 ここでは話を簡単にするため、これ以上この問題には触れませんが、対策についてはCSRF対策(Journal In Time)等を参照してください。

対応するビューのテンプレート app/views/admin/edit_user.rhtml は以下の通りです。 destroy はビューを持たないので、edit_user.rhtml で削除ビューも兼ねています。
<% @title = "ユーザ編集" %>

<%= error_messages_for 'user' %>

<%if flash[:notice] %>
<p style="margin-left: 2em; color: green"><%= flash[:notice] %></p>
<% end %>

<%= start_form_tag %>

<div class="body">
<h3 class="subtitle">ユーザの削除</h3>
<table>
<% for user in @users %>
<tr>
<td><%= user.name %></td>
<td>
<%= link_to '削除', {:action => 'destroy', :id => user}, :confirm => 'よろしいですか?' %>
</td>
</tr>
<% end %>
</table>

<h3 class="subtitle">ユーザの追加</h3>

<p><label for="user_name">ユーザ名</label>
<%= text_field("user", "name") %></p>
<p><label for="user_password">パスワード</label>
<%= password_field("user", "password") %></p>
<%= submit_tag "登録" %>
</div>

<%= end_form_tag %>
動作画面(http://localhost:3000/admin/edit_user):

admin/edit_user

ユーザ削除用に、既存ユーザ名の一覧と、各ユーザの削除をするための destroy アクションへのリンクを link_to で生成しています。 link_to のパラメータ:id => userは、:id => user.idと同じ意味です。 また、:confirm オプションを指定すると、link_to はリンクをクリックした際にJavaScriptで「よろしいですか?」というダイアログを表示して確認を求めてくるようになります。

confirm

確認できないとリンク先に移動しません。

なお、destroy で admin も簡単に消せてしまいます。注意してください。

ユーザの追加のフォームに関しては、既出のやり方を使っているだけですので説明は省略します。

投稿者 tam : 2005年09月13日 13:40

トラックバック

このエントリーのトラックバックURL:
http://tam.qmix.org/mt3/mt-tb.cgi/19

このリストは、次のエントリーを参照しています: RailsでWikiクローンを作る 12: 認証用ユーザ登録:

» spreader asian pussy from spreader asian pussy
[続きを読む]

トラックバック時刻: 2007年04月07日 12:19

» real estate monument colorado from real estate monument colorado
[続きを読む]

トラックバック時刻: 2007年04月09日 13:56

コメント

コメントしてください




保存しますか?