FrontPage  Index  Search  Changes  Login

RailsでWikiクローンを作る10

認証用ユーザ登録

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

ユーザ認証の仕様は、以下のようにしたいと思います。

  • ユーザは複数登録することができる。
  • adminユーザだけが管理画面を表示・操作できる。
  • その他のユーザはwikiページの編集ができる。
  • 登録されていないユーザはwikiページの編集や新規作成はできない。表示は可能。

なお、標準添付ではありませんが Rails には Login Generator というものがあります。 しかし、ここではこれを使わずに、仕組みを勉強するため自前で認証機構を作 ります。実際に実用的なシステムをつくる場合は Login Generator を使った 方が良いかもしれません。

追記: 今なら LoginEngine を使うのが良いと思います。「LoginEngineを使ってみる」参照。

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: ユーザの削除を行う。ビューは edit_user に相乗り。

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 も簡単に消せてしまい ます。注意してください。

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

最後に、edit_user アクションを呼び出すためのリンクを追加するため、 app/views/admin/basic.rhtml の末尾に以下を追加します。

<%= link_to 'ユーザの編集', :action => 'edit_user' %>
Last modified:2006/01/06 15:12:33
Keyword(s):
References:[LoginEngineを使ってみる] [MigrationによるDB管理] [RailsでWikiクローンを作る]