Websiteentwicklung: Ruby on Rails: User und Login

In diesem Kapitel werden wir eine App um eine n:m Beziehung erweitern: Ausgangspunkt ist tvtogether, eine App die Serien und Folgen von tvrage.com laden kann.

In diesem Kapitel werden wir als ersten Schritt in die App die Möglichkeit einbauen, dass User sich anmelden, einloggen und ausloggen. Dabei unterstützt uns Rails 3 sehr, wir brauchen nur wenig selbst zu Programmieren.

Diese App werden wir um ein Model 'user' erweitern. Zwischen users und episodes besteht eine n:m Beziehung: eine Person kann mehrere Episoden gesehen haben, eine Episode wurde von mehreren Personen gesehen.


User und Authentisierung Bearbeiten

Wir werden ein Model "User" brauchen, das Namen, E-Mail und das gehashte Passwort gespeichert.

Ausserdem werden wir den Hash session verwenden. Dieser wird von Rails zur Verfügung gestellt. Daten die wir hier speichern sind noch vorhanden, wenn vom gleichen Browser auf die nächste Seite aufgerufen wird. (Das wird mittels Cookies erreicht, diese werden automatisch von Rails gesetzt)

User Bearbeiten

  Dazu gibt es einen Railscast [1] Authentication in Rails 3.1


Diese Funktionalität braucht ein zusätzliches gem: Wir fügen in das Gemfile ein:

 gem 'bcrypt-ruby'

und führen danach einmal

 bundle install

aus. Damit wird das gem installiert. Damit es in der App verwendet wird, müssen wir den Webserver stoppen (mit STRG-C) und neu starten

 rails server


Nun legen wir das User-Model an.

 rails g model user name:string email:string password_digest:string
 rake db:migrate

Dem Model fügen wir eine Zeile hinzu:

  class User < ActiveRecord::Base
    has_secure_password
    validates_presence_of :password, :on => :create
    attr_accessible :name, :email, :password, :password_confirmation
  end

Diese eine Zeile fügt sehr viel Automatik ein: Methoden zum Setzen und Verifizieren des eingegeben Passworts, validations. In der Datenbank wird dabei nie das Original-Passwort gespeichert, sondern nur eine gehashte Version. Damit sich neue User anmelden können brauchen wir einen Controller:

   rails g controller users

Der Code des Controllers ist kurz:

   class UsersController < ApplicationController
     def new
       @user = User.new
     end
     def create
       @user = User.new(params[:user])
       if @user.save
         redirect_to root_url, :notice => "Erfolgreich angemeldet!"
       else
         render "new"
       end
     end
   end

Und dann brauchen wir noch eine View zur Anmeldung:

   <h1>Anmeldung</h1>

    <%= form_for @user do |f| %>
      <% if @user.errors.any? %>
        <div class="error_messages">
          <h2>Fehler bei der Anmeldung</h2>
          <ul>
            <% for message in @user.errors.full_messages %>
              <li><%= message %></li>
            <% end %>
          </ul>
        </div>
      <% end %>
      <div class="field">
        <%= f.label :name %><br>
        <%= f.text_field :name %>
      </div>
      <div class="field">
        <%= f.label :email %><br>
        <%= f.text_field :email %>
      </div>
      <div class="field">
        <%= f.label :password %><br>
        <%= f.password_field :password %>
      </div>
      <div class="field">
        <%= f.label :password_confirmation %><br>
        <%= f.password_field :password_confirmation %>
      </div>
      <div class="actions"><%= f.submit %></div>
    <% end %>

Session, Login, Logout Bearbeiten

Wie oben erwähnt ist die session ein Hash in Rails der in jedem Controller zugänglich ist, ähnlich wie params. Wir werden nun aber so tun, als ob sesssion ein model wäre, und eine View und einen Controller dafür anlegen.

Das Login-Formular wird as view sessions/new gespeichert. Dies ist das erste Formular, das nicht mit form_for erstellt wird, sonder mit from_tag. Der Unterschied: form_for ist immer an ein Model gebunden, mit form_tag kann man beliebige Formulare ganz unabhängig von Datenbank und Model, erzeugen.

  <h1>Login</h1>
  <%= form_tag sessions_path do %>
    <div class="field">
      <%= label_tag :email %>
      <%= text_field_tag :email, params[:email] %>
    </div>
    <div class="field">
      <%= label_tag :password %>
      <%= password_field_tag :password %>
    </div>
    <div class="actions"><%= submit_tag "Log in" %></div>
  <% end %>

Zu dieser View gehört ein Controller:

 rails g controller sessions

Der Controller hat 3 Actions, eine ist hier noch nicht vollständig implementiert:

   class SessionsController < ApplicationController
     def new
     end
     def create
       # ......
       if # .....
         session[:user_id] = user.id
         redirect_to root_url, :notice => "Logged in!"
       else
         flash.now.alert = "Falsche E-Mail or falsches Passwort"
         render "new"
       end
     end
     def destroy
       session[:user_id] = nil
       redirect_to root_url, :notice => "Logged out!"
     end
   end

Wenn das Login erfolgreich ist speichern wir die user_id in der Session - und nicht mehr.

Wie können wir nun prüfen ob der Login erfolgreich ist? Zuerst holen wir den user mit der Passenden E-Mail aus der Datenbank. Mit der authenticate-Methode des user-Objekts können wir prüfen ob das Passwort passt:

     user = User.find_by_email(params[:email])
     if user && user.authenticate(params[:password])

Damit wir in jedem Controller und jeder View die Information zur Verfügung haben ob jemand eingeloggt ist legen wir im applications_controller eine helper-Funktion an:

    class ApplicationController < ActionController::Base
      protect_from_forgery

      private

      def current_user
        @current_user ||= User.find(session[:user_id]) if session[:user_id]
      end

      helper_method :current_user
    end


Navigation mit Login und Logout Bearbeiten

Ähnlich wie hier in Wikibooks soll auch in der App tvtogether oben auf jeder Seite angezeigt werden ob mein eingloggt ist, bzw. Links für Login und Logout angeboten werden.

Das kann man in das Layout einfügen: app/views/layouts/application.html.erb

 <nav>
    <% if current_user.nil? %>
      <%= link_to "Login", new_session_path %>
    <% else %>
      Eingeloggt als <%= current_user.name %>.
      <%= link_to "Logout", session_path( session ), :method => :delete %>
    <% end %>
  </nav>

Quellen

  1. Railscast Authentication in Rails 3.1