Außerdem erzeugt der Controller-Generator zunächst Routen und Navigationseinträge für alle drei Actions.
Das ist für normale Inhaltsseiten sinnvoll.
Bei einer Session haben create und destroy jedoch eine andere Aufgabe:
new zeigt unser Login-Formular.
create verarbeitet die Anmeldung.
destroy meldet den Benutzer ab.
Wir benötigen deshalb nicht drei sichtbare Seiten in der Navigation.
11. Die automatisch erzeugten Session-Routen ersetzen
Der Generator hat zunächst Routen wie diese angelegt:
get "sessions/new"
get "sessions/create"
get "sessions/destroy"
Diese entfernen wir aus:
config/routes.rb
Stattdessen tragen wir ein:
get "login", to: "sessions#new", as: :login
post "login", to: "sessions#create"
delete "logout", to: "sessions#destroy", as: :logout
Damit entspricht auch die HTTP-Methode der jeweiligen Aufgabe:
GET /login → Login-Formular anzeigen
POST /login → Login durchführen
DELETE /logout → Session beenden
Anschließend kontrollieren wir die Routen:
bin/rails routes | grep -E "login|logout"
Wir sollten ungefähr Folgendes erhalten:
login GET /login sessions#new
POST /login sessions#create
logout DELETE /logout sessions#destroy
12. Die vom Generator erzeugte Navigation aufräumen
Der b4um Controller-Generator hat beim Erzeugen des SessionsControllers zunächst drei Navigationseinträge angelegt:
New
Create
Destroy
Wir öffnen:
app/views/shared/_navigation.html.erb
und entfernen diese drei automatisch erzeugten Einträge wieder.
Das ist wichtig:
Create und Destroy sind Aktionen und keine Seiten, die der Benutzer direkt über die Navigation öffnen soll.
Auch New soll später nicht New, sondern sinnvollerweise Login heißen.
13. Den SessionsController programmieren
Jetzt öffnen wir:
app/controllers/sessions_controller.rb
und ersetzen den Inhalt durch:
class SessionsController < ApplicationController
def new
end
def create
user = User.find_by(email: params[:email])
if user&.authenticate(params[:password])
session[:user_id] = user.id
redirect_to root_path, notice: "Successfully logged in."
else
flash.now[:alert] = "Invalid email or password."
render :new, status: :unprocessable_content
end
end
def destroy
session.delete(:user_id)
redirect_to root_path, notice: "Successfully logged out."
end
end
Sehen wir uns den entscheidenden Teil genauer an:
user = User.find_by(email: params[:email])
Damit suchen wir den Benutzer anhand der eingegebenen E-Mail-Adresse.
Anschließend:
user&.authenticate(params[:password])
Hier kommt unsere zuvor in der Rails Console getestete authenticate-Methode zum Einsatz.
Stimmt das Passwort, speichern wir:
session[:user_id] = user.id
Damit merkt sich Rails, welcher Benutzer angemeldet ist.
Beim Logout entfernen wir diese Information wieder:
session.delete(:user_id)
14. Das Login-Formular erstellen
Die vom Controller-Generator erzeugte Platzhalterseite ersetzen wir.
Dabei verwenden wir bewusst die bereits vorhandenen b4um Klassen:
form
form-field
form-input
form-label
form-actions
form-submit
Wir bauen also kein neues Formular-CSS für den Login.
Das Login-Formular verwendet die bereits vorhandenen Form-Komponenten des b4um Generators.
15. ApplicationController um Login-Helfer erweitern
Als Nächstes öffnen wir:
app/controllers/application_controller.rb
Die bereits vorhandenen Rails-Einstellungen lassen wir bestehen und ergänzen darunter unsere Authentifizierungsfunktionen.
Der Controller sieht anschließend so aus:
class ApplicationController < ActionController::Base
# Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has.
allow_browser versions: :modern
# Changes to the importmap will invalidate the etag for HTML responses
stale_when_importmap_changes
helper_method :current_user, :logged_in?
private
def current_user
@current_user ||= User.find_by(id: session[:user_id])
end
def logged_in?
current_user.present?
end
def require_login
return if session[:user_id].present?
redirect_to login_path, alert: "Please log in first."
end
end
Schauen wir uns die Ergänzungen einzeln an.
current_user
def current_user
@current_user ||= User.find_by(id: session[:user_id])
end
Damit können wir den momentan angemeldeten Benutzer ermitteln.
logged_in?
def logged_in?
current_user.present?
end
Damit können wir einfach fragen:
logged_in?
und erhalten sinngemäß true oder false.
require_login
def require_login
return if session[:user_id].present?
redirect_to login_path, alert: "Please log in first."
end
Diese Methode verwenden wir später als Schutz für Controller-Aktionen.
Warum helper_method?
Oben haben wir außerdem:
helper_method :current_user, :logged_in?
eingetragen.
Controller-Methoden stehen einer View nicht automatisch als normale View-Helper zur Verfügung.
Mit helper_method machen wir diese beiden Methoden auch in unseren ERB-Templates verfügbar.
Dadurch können wir später beispielsweise schreiben:
<% if logged_in? %>
und abhängig vom Login-Status Elemente anzeigen oder ausblenden.
require_login geben wir dagegen bewusst nicht als Helper frei. Diese Methode gehört in den Controller und schützt dort unsere Actions.
16. Den vorhandenen NavigationHelper erweitern
Für Login und Logout möchten wir weiterhin das bereits vorhandene Navigationssystem des b4um Generators verwenden.
Dabei gibt es allerdings einen wichtigen Unterschied:
Der Login ist ein normaler Link auf:
GET /login
Der Logout verwendet dagegen:
DELETE /logout
Für den Login eignet sich deshalb unser bereits vorhandener navigation_link_to.
Für den Logout möchten wir dagegen Rails button_to verwenden. Dadurch kann Rails sauber einen DELETE-Request absenden.
Damit der Logout-Button trotzdem genauso aussieht wie die übrigen Links in der Navigation, erweitern wir den vorhandenen NavigationHelper.
Wir öffnen:
app/helpers/navigation_helper.rb
Bisher enthält er bereits unseren Helper:
navigation_link_to
Unterhalb davon ergänzen wir:
navigation_button_to
Die vollständige Datei sieht anschließend so aus:
# frozen_string_literal: true
module NavigationHelper
def navigation_link_to(name, path, controller: nil, action: nil)
classes = [ "navigation__link" ]
active =
if controller && action
controller_name == controller.to_s &&
action_name == action.to_s
elsif controller
controller_name == controller.to_s
else
current_page?(path)
end
classes << "navigation__link--active" if active
link_to(
name,
path,
class: classes,
data: { action: "click->navigation#close" }
)
end
def navigation_button_to(name, path, method:)
button_to(
name,
path,
method: method,
class: "navigation__link",
form: {
class: "navigation__form",
data: { action: "click->navigation#close" }
}
)
end
end
Damit besitzen wir jetzt zwei passende Helfer für unsere Navigation:
navigation_link_to
für normale Links und:
navigation_button_to
für Aktionen, die über eine andere HTTP-Methode ausgeführt werden sollen.
Warum verwenden wir für Logout button_to?
Unsere Logout-Route lautet:
DELETE /logout
Ein normaler HTML-Link führt zunächst einen GET-Request aus.
Rails button_to erzeugt dagegen ein kleines Formular und kann dadurch zuverlässig unsere gewünschte HTTP-Methode verwenden:
Unser neuer Helper erzeugt daraus einen Rails-Button, der:
DELETE /logout
aufruft.
Gleichzeitig erhält der Button:
navigation__link
und sieht dadurch genauso aus wie die übrigen Links.
Wir brauchen also weder JavaScript für die DELETE-Methode noch einen gesonderten Logout-Button im Design.
19. Login und Logout testen
Jetzt testen wir das Zusammenspiel.
Zunächst rufen wir auf:
http://localhost:3000/login
Solange wir nicht angemeldet sind, sehen wir in der Navigation:
Login
Auf der Login-Seite wird dieser Navigationspunkt durch unseren vorhandenen navigation_link_to außerdem als aktiv markiert.
Wir melden uns jetzt mit unserem zuvor erstellten Benutzer an.
Nach erfolgreicher Anmeldung führt unser SessionsController aus:
session[:user_id] = user.id
und leitet uns auf die Startseite weiter.
Dort erscheint die Meldung:
Successfully logged in.
Gleichzeitig liefert:
logged_in?
nun true.
Deshalb wird in der Navigation nicht mehr:
Login
angezeigt, sondern:
Logout
Der entscheidende Unterschied ist im Browser nicht mehr sichtbar:
Login ist technisch ein normaler Link.
Logout ist technisch ein von Rails mit button_to erzeugter Button innerhalb eines Formulars.
Durch unseren erweiterten NavigationHelper und die Anpassung von:
.navigation__link
sehen beide jedoch wie normale b4um Navigationspunkte aus.
Nach erfolgreicher Anmeldung wechselt die Navigation von Login zu Logout. Der Logout ist technisch ein Rails button_to, verwendet aber dieselbe Darstellung wie die übrigen b4um Navigationslinks.
Logout testen
Jetzt klicken wir auf:
Logout
Unser Helper:
navigation_button_to
sendet dadurch:
DELETE /logout
an:
SessionsController#destroy
Dort wird:
session.delete(:user_id)
ausgeführt.
Anschließend ist der Benutzer abgemeldet und in der Navigation erscheint wieder:
Login
Damit haben wir jetzt eine Navigation, die nicht nur optisch einheitlich ist, sondern auch die richtigen HTTP-Methoden für Login und Logout verwendet.
20. Article-Aktionen schützen
Jetzt verwenden wir unsere Anmeldung für einen praktischen Zugriffsschutz.
Wir öffnen:
app/controllers/articles_controller.rb
und ergänzen am Anfang:
before_action :require_login,
only: %i[ new create edit update destroy ]
Zusammen mit dem bereits vorhandenen Callback sieht der Anfang beispielsweise so aus:
class ArticlesController < ApplicationController
before_action :require_login,
only: %i[ new create edit update destroy ]
before_action :set_article,
only: %i[ show edit update destroy ]
# ...
end
Damit bleiben:
index
show
öffentlich erreichbar.
Geschützt sind dagegen:
new
create
edit
update
destroy
Ein Besucher darf also Artikel ansehen, aber nicht verändern.
21. Schutz ohne Login testen
Jetzt melden wir uns über Logout ab.
Anschließend versuchen wir direkt:
http://localhost:3000/articles/new
aufzurufen.
require_login erkennt, dass keine Benutzer-ID in der Session vorhanden ist und führt aus:
redirect_to login_path, alert: "Please log in first."
Wir landen deshalb wieder auf der Login-Seite und sehen:
Please log in first.
Dasselbe geschieht, wenn wir die URL einer Edit-Seite direkt aufrufen.
Der Zugriffsschutz funktioniert auch bei direkter Eingabe einer geschützten URL. Der Benutzer wird zum Login weitergeleitet.
22. „New article“ nur nach dem Login anzeigen
Jetzt verbessern wir zusätzlich die Benutzeroberfläche.
<%if@articles.any? %><%= render "bento", articles: @articles%><%else%><sectionclass="b4um-empty-state"><h2class="b4um-empty-state__title">
No articles yet.
</h2><pclass="b4um-empty-state__text">
Create your first article to get started.
</p></section><%end%>
Durch:
<% if logged_in? %>
wird New article nur angezeigt, wenn ein Benutzer angemeldet ist.
Dabei verwenden wir wiederum eine bereits vorhandene b4um Button-Klasse:
button button--primary
Wir benötigen dafür also ebenfalls kein neues CSS.
Öffentliche Inhalte bleiben sichtbar, während Bearbeitungs- und Löschfunktionen für nicht angemeldete Besucher ausgeblendet werden.
24. Warum das Ausblenden allein nicht reicht
An dieser Stelle ist ein Unterschied besonders wichtig.
Mit:
<% if logged_in? %>
verstecken wir Funktionen lediglich in der Benutzeroberfläche.
Das ist komfortabel, aber noch kein ausreichender Zugriffsschutz.
Jemand könnte theoretisch versuchen, die entsprechende URL direkt aufzurufen.
Deshalb haben wir zusätzlich im Controller:
before_action :require_login,
only: %i[ new create edit update destroy ]
eingebaut.
Damit haben wir zwei Ebenen:
View
<% if logged_in? %>
sorgt für eine saubere Benutzeroberfläche.
Controller
before_action :require_login
sorgt für den tatsächlichen serverseitigen Zugriffsschutz.
Beides zusammen ergibt das gewünschte Verhalten.
25. Geschützte Funktion nach dem Login testen
Jetzt melden wir uns wieder an.
Anschließend öffnen wir:
/articles
Jetzt erscheint wieder:
New article
Klicken wir darauf, öffnet sich das vom b4um Scaffold erzeugte Formular.
In unserem Beispiel enthält es:
Title
Description
Image
Nach erfolgreicher Anmeldung stehen die geschützten Funktionen wieder zur Verfügung.
26. Logout testen
Zum Schluss klicken wir in der Navigation auf:
Logout
Unser Link sendet durch:
data: { turbo_method: :delete }
einen DELETE-Request an:
/logout
Dadurch wird im SessionsController ausgeführt:
session.delete(:user_id)
Der Benutzer ist anschließend nicht mehr angemeldet.
In der Navigation erscheint wieder:
Login
und geschützte Funktionen sind nicht mehr verfügbar.
27. Was bcrypt dabei eigentlich macht
Zum Abschluss lohnt sich noch einmal der Blick auf den gesamten Passwortablauf.
Unser Formular besitzt:
password
password_confirmation
Unser Datenbankmodell besitzt dagegen:
password_digest
Durch:
has_secure_password
übernimmt Rails zusammen mit bcrypt die Verarbeitung.
Wenn der Benutzer beispielsweise eingibt:
test1234
wird dieser Wert nicht als Klartext gespeichert.
Stattdessen landet ein bcrypt-Hash in:
password_digest
Bei:
user.authenticate("test1234")
kann bcrypt anschließend überprüfen, ob das eingegebene Passwort zu diesem Hash gehört.
Genau deshalb konnten wir in unserer Rails Console sehen:
user.authenticate("test1234")
→ Benutzer
und:
user.authenticate("falsches-passwort")
→
false
Das Klartextpasswort muss dafür nicht aus der Datenbank zurückgelesen werden.
28. Ergebnis
Wir haben unsere bestehende b4um-Anwendung um eine vollständige einfache Benutzeranmeldung erweitert und sind dabei konsequent von den Funktionen des b4um Generators ausgegangen.
Mit:
bin/rails generate b4um:install
haben wir die bcrypt-Unterstützung aktiviert beziehungsweise überprüft.
Mit:
bin/rails generate b4um:scaffold User name:string email:string password_digest:string
haben wir Benutzer-Modell, Controller und bereits passend gestaltete Formulare erhalten.
Mit:
bin/rails generate b4um:controller Sessions new create destroy
haben wir die Ausgangsstruktur für die Session-Verwaltung erzeugt und anschließend an die besonderen Anforderungen eines Logins angepasst.
Dabei konnten wir außerdem vorhandene b4um Komponenten weiterverwenden:
Damit mussten wir für Login, Logout und die geschützten Article-Funktionen kein separates Designsystem aufbauen.
Unsere Anwendung unterscheidet nun zwischen öffentlichen und geschützten Funktionen:
Öffentlich:
Articles anzeigen
einzelnen Article anzeigen
Login
Nur angemeldet:
neuen Article erstellen
Article bearbeiten
Article löschen
Logout
Und besonders wichtig: Der Schutz besteht nicht nur optisch. Auch ein direkter Aufruf einer geschützten URL wird durch require_login abgefangen.
Damit haben wir mit bcrypt, Rails Sessions und den bestehenden Komponenten des b4um Generators eine kompakte Benutzeranmeldung mit echtem serverseitigem Zugriffsschutz aufgebaut.
Wir verwenden Cookies, um Inhalte und Anzeigen zu personalisieren,
Funktionen für soziale Medien anbieten zu können und die Zugriffe auf
unsere Website zu analysieren.
Sie akzeptieren unsere Cookies, wenn Sie fortfahren diese Webseite zu
nutzen.
Datenschutzerklärung.