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

2005年09月15日

RailsでWikiクローンを作る 13: 認証の適用

session
HTTP はもともとステートレスな(状態を持たない)プロトコルです。 なので、例えば今どんなユーザでログインしているかという情報を保持するには、一連のセッションを識別して管理する仕組みが必要になります。 セッション管理の方法はいろいろありますが、Rails は Cookie をベースにしたセッション管理機構を標準で装備しています。

実際に Rails でセッションの機構を使う場合は、Cookie を意識する必要は全くありません。 コントローラから参照できる session というハッシュのような属性があり、任意のキーで任意のオブジェクト(一部制限あり)を session に格納しておくことができます。 そしてsessionの内容は複数のアクションにまたがって参照することができます。

既に、似たようなものとして flash について説明していますが、flash の内容は次のアクションに限って伝達されるのに比べ、session はもっと永続的です (Railsデフォルトでは、同一ブラウザでアクセスしている間ずっと保持される)。
login アクション
では、ユーザ名とパスワードの入力を求める login アクションをapp/controllers/admin_controller.rb に追加します。 入力されたユーザ名とパスワードが正しければ、そのユーザ情報を session に保持してログイン状態にする、という動作をします。
  def login
    if request.get?
      session[:user] = nil
      @user = User.new
    else
      user = User.new(params[:user])
      @user = User.find_by_name(user.name)
      if user.password.crypt(@user.crypted_password) == @user.crypted_password
        session[:user] = @user
        jumpto = session[:jumpto] ||
          {:controller => 'wiki', :action => 'show', :id => 'FrontPage'}
        session[:jumpto] = nil
        redirect_to(jumpto)
      else
        flash[:notice] = "ユーザ名またはパスワードが間違っています。"
      end
    end
  end
login メソッドでは、basic と同じように request method によって動作を変えています。 GETメソッドの場合は、session に記録されている :user 情報を消して、ログイン用フォームを表示します。 GETメソッドでない場合は POSTメソッドと思われるので、params で受けとったユーザ名・パスワードと、DBに記録されているものと比較します。 一致していたら session[:user] にユーザ情報を記録し、これ以降ログインしている状態になります。 また、無事ログイン状態になったらリダイレクトするのですが、もし session[:jumpto] に値が入っていたらそれで指定される先(後述)に、値がないなら FrontPage にリダイレクトします。

ビューのテンプレート app/views/admin/login.rhtml は以下の通りです。
<% @title = "login" %>

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

<%= start_form_tag %>
<div class="body">
<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/login):

admin/login

フィルタ
認証に必要な準備はひととおり整いました。 次は、この認証を必要なページに適用します。 認証を適用するには Rails のフィルタ機能を使います。 フィルタ機能は、各アクションの実行時に割り込んで、指定のメソッドを実行する機能で、複数のアクションにまたがった前処理・後処理や、アクセス制御に利用することができます。

実は、Minki では既にフィルタ機能を使っています。 準備段階の 「UTF-8で運用する」において、before_filter というアクション実行前に実行されるフィルタを利用して、set_charset というメソッドを実行するようにしています。
class ApplicationController < ActionController::Base
  before_filter :set_charset

  protected
  def set_charset
    @headers["Content-Type"] = "text/html; charset=utf-8" 
  end
end
フィルタはコントローラ単位で設定しますが、set_charset は、すべてのコントローラの上位クラスである ApplicationController で before_filter として設定されているため、Minki アプリケーションのすべてのアクション前に set_charset が実行されることになります。 これは、フィルタを前処理として利用した例です。

では、before_filter を認証に使います。 まずは、before_filter で指定するメソッド authorize を作ります。 authorize は、session を調べてユーザ情報があるかないか、つまり現在ログイン中かどうかを調べ、ログイン中でなければログイン画面にリダイレクトするメソッドです。 これを必要なアクションの前に適用すれば、ログインしないとアクションを実行できなくなります。
認証は複数のコントローラのアクションにしかけますので、authorize メソッドは ApplicationController (app/controllers/application.rb)に定義することにします。
class ApplicationController < ActionController::Base
  before_filter :set_charset

  protected
  def set_charset
    @headers["Content-Type"] = "text/html; charset=utf-8" 
  end

  def authorize
    unless session[:user] and
        (session[:user].name == 'admin' or params[:controller] != 'admin')
      flash[:notice] = "ログインしてください"
      session[:jumpto] = request.parameters
      redirect_to :controller => 'admin', :action => 'login'
    end
  end
end
ApplicationController の before_filter で authorize メソッドを指定してはいけません。 もしやってしまうと、すべてのアクションに認証がかかってしまします。
authorize メソッドでは、session[:user] を見てログイン中かどうかを調べ、以下の条件を満たしているかを判断します。 この条件を満たしていれば、何もせずにスルーします。 つまり、本来実行するアクションが普通に実行されます。 条件を満たしていなければ、adminコントローラのloginアクションにリダイレクトします。 ログインしない限りloginアクションにリダイレクトされ続けることになります。
リダイレクトする直前に、session[:jumpto] = request.parametersとして、sessionにリクエストのパラメータを保存しています。 こうすることにより、loginアクションで認証を通過したら、session[:jumpto] にリダイレクトすることにより元のアクションに戻ってくることができるようになります。

そして、authorize メソッドを各コントローラの before_filter で設定します。 まずは admin コントローラ app/controllers/admin_controller.rb:
class AdminController < ApplicationController
  before_filter :authorize, :except => :login
  #(以下略)
before_filter として authorize メソッドを登録しています。 その後ろはオプションで、:except でフィルタを適用しないアクション名を指定しています。 login アクションに認証をかけるわけにはいかないので login はフィルタ適用対象外にし、それ以外のすべての(adminコントローラの)アクションに認証をかけます。 :except と、次に出てくる :only には、アクション名単体あるいはアクション名の配列のどちらも指定できます。

次に wiki コントローラ app/controllers/wiki_controller.rb:
class WikiController < ApplicationController
  before_filter :authorize, :only => [:new, :create, :edit, :update]
  #(以下略)
こちらは admin コントローラとは逆に :only オプションを指定して、new, create, edit, update の各アクションのみに認証をかけています。 この行を書かなければ、Wikiページは誰でも作成・変更が可能のままになります。

logout
logout アクションは簡単です。 session[:user] をクリアして、FrontPage へリダイレクトするだけです。 ビューはありません。
  def logout
    session[:user] = nil
    redirect_to :controller => 'wiki', :action => 'show', :id => 'FrontPage'
  end
そして、app/views/layouts/wiki.rhtml に以下のコードを追加して、 ログイン状態ならば各ページの上部に表示されるadminmenuに「ログアウト」のリンクを表示するようにしておきます。
 <% if session[:user] %>
  <span class="adminmenu">
  <%= link_to 'ログアウト', :controller => 'admin', :action => 'logout' %>
  </span>
 <% end %>
このコードは、<div class="adminmenu">の中ならばどこに入れても良いですが、最後に入れたとすると、ログイン中のadminmenuは以下のようになります。

adminmenu

投稿者 tam : 2005年09月15日 09:42

トラックバック

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

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

» Great post! I'm looking forward for more. from
[続きを読む]

トラックバック時刻: 2005年12月08日 01:54

コメント

コメントしてください




保存しますか?