28 luty 2010

Zmuszenie Authlogic do obsługi 3 parametrów logowania

Kategorie:  RoR  Ruby  Web

Jak zmusić szkielet uwierzytelnienia dla RoR Authlogic do obsługi trzech parametrów logowania zamiast standardowo dostępnych dwóch

Authlogic jest jednym z najbardziej popularnych rozwiązań wspomagających uwierzytelnianie dla platformy Ruby on Rails. Jego sukces jest spowodowany wysoką funkcjonalnością i elastycznością. Wykorzystując authlogic możesz wykonać niemalże każde typowe zadanie związane z uwierzytelnianiem użytkownika w swojej aplikacji WWW. Dodatkowo dzięki dostępnym wtyczkom, możesz łatwo zintegrować uwierzytelnienie z wykorzystaniem konta Open ID, Facebook lub Twitter ze swoją stroną Internetową. Jeśli nie zetknąłeś się z Authlogickiem, powinieneś obejrzeć ten i ten film Ryan Bates'a lub wykonać tutorial dostępny w pliku REDME Authlogica.

W 90% aplikacji sieci Web będziesz musiał podać dwa parametry logowanie, nazwę użytkownika lub email i hasło. Taki typ uwierzytelnienie jest domyślnie wbudowany w authlogica. Projekt, nad którym pracowałem wymagał ode mnie podawania 3 parametrów logowania. W tym projekcie mam wiele warsztatów, i każdy z nich może posiadać wiele kont użytkowników, którzy współdzielą między sobą i manipulują jego danymi. W ramach każdego warsztatu chciałbym mieć unikalne nazwy użytkowników, ale pomiędzy warsztatami nazwy użytkowników mogą się powtarzać. Na początku myślałem, że będzie wyjątkowo trudno osiągnąć taki efekt, ale kiedy cierpliwie przeczytałem dokumentację api, elastyczność authlogica i języka ruby pozwoliła mi to zrobić w całkiem prosty sposób. Postaram się podzielić moim rozwiązaniem mając nadzieję, że okaże się ono użyteczne także dla innych lub być może ktoś zasugeruje mi lepsze rozwiązanie tego problemu.

Zakładam, że jeśli to czytasz wiesz jak działa szkielet Ruby On Rails, i możesz czytać ze zrozumieniem kod źródłowy języka ruby. Nie będę się starał wyjaśniać każdej pojedynczej linii kodu, który tutaj znajdziesz. Skupię się na wyjaśnieniu części kodu, które są istotne ze względu na obsługę 3 parometrowego uwierzytelnienia przy użyciu authlogica.

Rozpocznijmy od pokazania części widoku mojego projektu. Poniżej znajdziesz partial odpowiedzialny za wyświetlanie użytkownikowi formularza logowania.

Wyświetl kod źródłowy partiala
  1. <fieldset id="form">
  2. <legend>Submit login information</legend>
  3. <% form_remote_for @user_session,
  4. :url=>{:action=>'create'},
  5. :html=>{ :autocomplete => :off },
  6. :before => "Element.show('loging-indicator')",
  7. :success => "Element.hide('loging-indicator')" do |f| %>
  8. <%= f.error_messages %>
  9. <p>
  10. <%= f.label :workshop_unique, "Workshop id:" %><br />
  11. <%= f.text_field :workshop_unique, :value => @workshop, :size => 30 %> <br />
  12. <%= check_box_tag :remember_workshop, 1, checked = false %>
  13. <%= label_tag :remember_workshop, "Remeber workshop id" %>
  14. </p>
  15. <p>
  16. <%= f.label :username, "User name: " %><br />
  17. <%= f.text_field :username, :value => @username %> <br />
  18. <%= check_box_tag :remember_username, 1, checked = false %>
  19. <%= label_tag :remember_username, "Remember user name" %>
  20. </p>
  21. <p>
  22. <%= f.label :password, "Password:" %><br />
  23. <%= f.password_field :password %>
  24. </p>
  25. <p><%= f.submit "Zaloguj" %></p>
  26. <% end %>
  27. </fieldset>
  28. <div id="loging-indicator" style="display:none;">
  29. <span>Data manipulation in progress:<br />
  30. <%= image_tag("processingbar.gif",
  31. :align => "absmiddle",
  32. :border => 0) %>
  33. </span>
  34. </div>

Tutaj tworzymy Ajaxowy formularz logowania, używając metody pomocniczej form_remote_for która operuje na obiekcie @user_session. Ten obiekt jest typowym wykorzystaniem API authlogica, co nie jest typowe w tym formularzu to fakt, że mamy 3 pola logowania. Użytkownik będzie musiał wprowadzić swój identyfikator warsztatu, swoją nazwę użytkownika i swoje hasło aby zalogować się do systemu. Znajdziesz tu także pola wyboru, które pozwalają użytkownikowi zapamiętać jego informacje logowania ale są one poza zakresem interesującej nas treści.

Teraz spójrzmy na kod kontrolera, który obsługuje wymianę informacji pomiędzy widokiem i modelem podczas fazy logowania i wylogowywania użytkownika.

Wyświetl kod kontrolera
  1. class UserSessionsController < ApplicationController
  2.  
  3. # access control:
  4. # logged in users can destroy session (and be logged out)
  5. # anonymous users can create new session (login in to system)
  6. access_control :debug => true do
  7. allow logged_in, :to => [:destroy]
  8. allow anonymous, :to => [:new, :create]
  9. end
  10.  
  11. def new
  12. @user_session = UserSession.new
  13. @workshop = cookies[:workshop]
  14. @username = cookies[:username]
  15. end
  16.  
  17. def create
  18. if params[:remember_workshop]=="1"
  19. if cookies[:workshop] != nil
  20. cookies.delete :workshop
  21. end
  22. cookies[:workshop] = {:value=> params[:user_session][:workshop_unique],
  23. :expires => 1.year.from_now}
  24. end
  25. if params[:remember_username]=="1"
  26. if cookies[:username] != nil
  27. cookies.delete :username
  28. end
  29. cookies[:username] = {:value=> params[:user_session][:username],
  30. :expires => 1.year.from_now}
  31. end
  32. login = {:workshop_unique => params[:user_session][:workshop_unique],:username => params[:user_session][:username]}
  33. @user_session = UserSession.new(:username=>login,:password=>params[:user_session][:password])
  34. if @user_session.save
  35. flash[:notice] = "Login succsessfull."
  36. respond_to do |format|
  37. format.html { redirect_to root_url }
  38. format.js {
  39. render :update do |page|
  40. page.redirect_to root_url
  41. end
  42. }
  43. end
  44. else
  45. unless cookies[:workshop].nil?
  46. @workshop = cookies[:workshop]
  47. else
  48. @workshop = params[:user_session][:workshop_unique]
  49. end
  50. unless cookies[:username].nil?
  51. @username = cookies[:username]
  52. else
  53. @username = params[:user_session][:username]
  54. end
  55. respond_to do |format|
  56. format.html { render :action => 'new'}
  57. format.js {
  58. render :update do |page|
  59. page.replace_html :login, :partial => 'login_form'
  60. page.visual_effect(:shake,:login,:duration => 1)
  61. end
  62. }
  63. end
  64. end
  65. end
  66.  
  67. def destroy
  68. @user_session = UserSession.find
  69. @user_session.destroy
  70. flash[:notice] = "Log off succsesfull."
  71. current_user = nil
  72. redirect_to :controller => 'szws', :action => 'goodbye'
  73. end
  74.  
  75. end

Tworzenie kontrolera sesji użytkownika jest typowym podejściem przy wykorzystywaniu API authlogic. Ten kontroler zadba o logowanie i wylogowanie naszego użytkownika. Na samej górze kodu źródłowego znajdziesz mały kawałek kodu, który nie należy do zakresu naszego zainteresowania, ta część jest typowa dla rozwiązania autoryzacyjnego acl9 jakie wykorzystuje w mojej aplikacji. To co jest tutaj ważne to nasze metody. New, która jest odpowiedzialna za wyświetlenia nowego formularza logowania użytkownikowi, create, która zajmuje się logowaniem i odmową logowania jeśli użytkownik przekazał złe parametry uwierzytelnienia. Trzeci metoda destroy jest wykorzystana do wylogowania użytkownika. Jeśli spojrzysz na metody new i destroy nie znajdziesz w nich niczego niezwykłego. W metodzie destroy znajdujemy obiekt @user_session aktualnie zalogowanego użytkownika i niszczymy go, jest to w prostej linii wykorzystanie API authlogica.

To co jest najważniejsze tutaj to nasza metoda create. Pierwsza część tej metody to obsługa ciasteczek, pominiemy ją i przejdziemy poniżej. Tutaj znajdziesz część tworzącą obiekt @user_session. W normalny warunkach stworzylibyśmy ten obiekt, przekazując do niego dwa parametry pobrane z naszego żądania. Tak postąpilibyśmy w 90% wypadków ale tutaj możesz zauważyć drobny trik jaki zastosowałem aby sprawić, że trzy parametry logowania zaczną działać. Jako że konstruktor obiektu sesji przyjmuje tylko dwa parametry username i password. Authlogic przekazuje tylko te dwa parametry do modelu, który obsługuje proces autoryzacji. To co tutaj zrobimy to najpierw stworzymy tablicę asocjacyjną, która przechowa zarówno parametr workshop_unique jak i username następnie przypiszemy tę tablicę do obiektu login. Aby na końcu przekazać ten obiekt jako username, w konstruktorze UserSession.new.

To wszystko co zrobiłem aby sprawić, że 3 parametry logowania zadziałają w kontrolerze. Oczywiście to nie wszystko, co trzeba zrobić aby authlogic poprawnie zweryfikował 3 parometrowy login użytkownika. Ostatnia część to model. Będziemy musieli się przyjrzeć dwóm modelom, pierwszy to model user_session drugi to model user. Zacznijmy do modelu user_session.

View the user_session model source code
  1. class UserSession < Authlogic::Session::Base
  2. find_by_login_method :find_user_in_workshop
  3. #Make sure the right messege is being displayed to user if he provides incorrect login information
  4. generalize_credentials_error_messages "You provided wrong login information, please try again"
  5.  
  6. @workshop_unique
  7.  
  8. def workshop_unique
  9. @workshop_unique
  10. end
  11.  
  12. def worksop_unique=(value)
  13. @workshop_unique=value
  14. end
  15.  
  16. end

Jak widzisz klasa UserSession dziedziczy z Authlogic::Session::Base, jest to kolejne typowe rozwiązanie dla API authlogic. W górnej częsći tego pliku źródłowego znajdzejsz najpobieżniejszą rzecz. Definiując find_by_login_method, możesz zastąpić standardową metodę znajdująca użytkownika w systemie. Jak napisał autor w dokumentacji API: "tylko niebo cię ogranicza", i ma tu pełną rację. Zajrzymy do naszej metody find_user_in_workshop analizując kod źródłowy modelu user. Poniżej możesz znaleźć definicję komunikatu błędu jaki wyświetlimy użytkownikowi jeśli wprowadzi błędne informacje logowania. Ostania rzecz w modelu user_session to definicja zmiennej instancji @workshop_unique. Dodałem ją aby móc użyć jej w metodzie pomocniczej widoku form_remote_for. Spójrz do kodu widoku. Teraz spójrzmy na model user.

Wyświetl kod źródłowy modelu user
  1. class User < ActiveRecord::Base
  2. #This is authentication subject
  3. acts_as_authentic do |c|
  4. c.validate_email_field = false
  5. c.validate_login_field = true
  6. #make sure that one workshop wont be able to have two users of the same name
  7. c.validations_scope = :workshop_id
  8. c.logged_in_timeout=30.minutes;
  9. c.maintain_sessions=false
  10. c.ignore_blank_passwords=false
  11. end
  12.  
  13. belongs_to :workshop
  14.  
  15. def self.find_user_in_workshop(login)
  16. workshop_unique=login[:workshop_unique]
  17. username = login[:username]
  18. workshop = Workshop.find_by_workshop_unique(workshop_unique)
  19. unless workshop.nil?
  20. find_by_username_and_workshop_id(username,workshop.id)
  21. else
  22. return nil
  23. end
  24. end
  25.  
  26. end

To co widzisz w górnej części kodu źródłowego, to konfiguracja podmiotu uwierzytelnienia. Ustawiamy tu weryfikację naszego pola loginu i ignorowanie walidacji pola email. Następnie upewniamy się, że nasza nazwa użytkownika będzie unikalna w ramach jednego warsztatu. Ustawiamy czas wygasania sesji logowania użytkownika. Jak widzisz także uniemożliwiam automatyczne zarządzanie sesją dlatego, że w moim systemie jedni użytkownicy mogą tworzyć konta innych. Nie chciałbym aby następowało automatyczne zalogowanie użytkownika na konto, które właśnie założył dla kogoś innego. Ostatnią rzeczą jest wyłączenie ignorowania pustych haseł.

Drugą rzeczą jest typowa konfiguracja relacji jak wiemy każdy użytkownik może należeć do jednego warsztatu.

Ostatnią ale bynajmniej nie najmniej ważną rzeczą jest definicja metody: find_user_in_workshop. Ta metoda przyjmuje nasz parametr login, który jest tablicą asocjacyjną zawierającą w sobie: workshop_unique i username, pobrane z formularza logowania użytkownika. To co tutaj robimy to najpierw staramy się znaleźć warsztat używając jego unikalnego identyfikatora. Jeśli to się udaje, znajdujemy użytkownika używając zarówno username jak i workshop.id. I to jest wszystko, to rozwiązanie działa całkiem nieźle w systemie, który buduję.

Sources:




Komentarze

Jeśli znalazłeś jakieś błędy w powyższej informacji lub po prostu chcesz wypowiedzieć swoje zdanie na jej temat, będę wdzięczny za pozostawienie komentarza.
Wszystkie komentarze będą pokazywać się na stronie po tym jak zostaną zatwierdzone. Przepraszam za to ale chcę mieć pewność, że moja strona będzie wolna od obraźliwych lub wulgarnych treści. Nie mam nic przeciwko krytyce ale zrób to właściwie dobierając słowa.

Pozostaw komentarz