|
2007-10-28 19:03
ruby on rails, authentication, rails, user, authorizaiton, system, authenticating
User authentication in Ruby on RailsAutor: Kacper Cieśla (comboy)This tutorial was written for Ruby on Rails noobs. If you already know something about rails, go straight to the model declaration. So, first of all, you must be aware that there is a lot of plugins that implement authentication for your rails app. Some of them are: And there are many more . Some of them looks more like whole application templates rather than just plugins. Plugins are good and there is no point in writing something that somebody has already wrote, but user authentication isn’t that hard thing to implement. That’s why I prefere to write it myself rather than running through pages of documentation, reading somebody’s dirty code and trying to understand it’s meaning to finally rewrite most of it anyway. Let’s do itLet’s start from the scratch:
$ rails userapp
create
create app/controllers
create app/helpers
create app/models
(...)
And in our newly created directory userapp edit file config/database.yml, I’ll use MySQL: development: adapter: mysql database: test username: foo password: pass123 host: localhost Preparing the user modelWe use generator to create user model:
$ ruby script/generate model User
exists app/models/
exists test/unit/
exists test/fixtures/
create app/models/user.rb
create test/unit/user_test.rb
create test/fixtures/users.yml
create db/migrate
create db/migrate/001_create_users.rb
And we edit db/migrate/001_create_users.rb, When you’re done it should look like this: class CreateUsers < ActiveRecord::Migration def self.up create_table :users do |t| t.column :username, :string t.column :password_salt, :string t.column :password_hash, :string t.column :email, :string t.column :created_at, :datetime end end def self.down drop_table :users end end Of course columns email and created_at are optional (btw, created_at is a magic field) And we run the migration: $ rake db:migrate == CreateUsers: migrating ===================================================== -- create_table(:users) -> 0.1500s == CreateUsers: migrated (0.1500s) ============================================ Now it’s time to pimp up our model. First of all, if we set password for an user, it should be saved to columns password_salt and password_hash. What are those? password_salt is just a random string, and password_hash is a md5 hash of password+password_salt (salt makes it harder to crack). def password=(pw) # we generate a random string salt = [Array.new(6){rand(256).chr}.join].pack("m").chomp # 2^48 kombinacji # and save salt and the hash self.password_salt, self.password_hash = salt, Digest::MD5.hexdigest(pw + salt) end Some nice function to check if given password is correct: def password_is?(pw) Digest::MD5.hexdigest(pw + password_salt) == password_hash end And to make it even better we add some validations: validates_presence_of :username validates_uniqueness_of :username So finally, file app/models/user.rb looks like this: class User < ActiveRecord::Base validates_presence_of :username validates_uniqueness_of :username validates_confirmation_of :password attr_reader :password def password=(pw) @password = pw # used by confirmation validator salt = [Array.new(6){rand(256).chr}.join].pack("m").chomp # 2^48 combos self.password_salt, self.password_hash = salt, Digest::MD5.hexdigest(pw + salt) end def password_is?(pw) Digest::MD5.hexdigest(pw + password_salt) == password_hash end end New user registrationThis is an example of creating simple rails app based on the given user model, so if you’re exprienced in rails, you can propably skip this and next paragraph. We need some controller:
$ ruby script/generate controller main
exists app/controllers/
exists app/helpers/
create app/views/main
exists test/functional/s
create app/controllers/main_controller.rb
create test/functional/main_controller_test.rb
create app/helpers/main_helper.rb
Now we create app/views/main/register.rhtml with something like this: <div align="right" style="width: 600px"> <%= '<b>'+flash[:info]+'</b><hr />' if flash[:info] %> <h3 align="center">New user registration</h3> <%= error_messages_for 'user' %> <% form_tag(:action => 'register') do %> Login: <%= text_field 'user', 'username' %> <br /><br /> Password: <%= password_field 'user', 'password' %> <br /><br /> Password again: <%= password_field 'user', 'password_confirmation' %> <br /><br /> E-mail: <%= text_field 'user', 'email' %> <br /><br /> <%= submit_tag "Register me" %> <% end %> </div> And related action in the controller file app/controllers/main_controller.rb: class MainController < ApplicationController def register if request.post? @user = User.new params[:user] if @user.save flash[:info] = 'You are registered now' end end end end And it’s done. Easy, right? After registration user will stay on the registration page so you may want to add some redirection. At the moment we don’t have a good place to redirect to, if we would have it’s enough to add: redirect_to :action => 'index', :controller => 'main' Let’s see if it really works: $ ruby script/server => Booting WEBrick... => Rails application started on http://0.0.0.0:3000 (...) And under our favourite browser we open http://localhost:3000/main/register It does :) Log inTo go further we need some page that will be availible only for logged in users. In this example I assume that we have some panel controller which is user control panel and should be availible only for registered users. Let’s generate it:
$ ruby script/generate controller panel
exists app/controllers/
exists app/helpers/
create app/views/panel
exists test/functional/
create app/controllers/panel_controller.rb
create test/functional/panel_controller_test.rb
create app/helpers/panel_helper.rb
Some tiny form for logging in (app/views/main/login.rhtml): <%= '<font color="red">'+@auth_error+'</font><hr />' if @auth_error %> <% form_tag(:action => 'login') do %> Login: <%= text_field_tag :login, params[:login] %> <br /><br /> Password: <%= password_field_tag :password, params[:password] %> <br /><br /> <%= submit_tag "log in" %> <% end %> (I haven’t say it will be impressive ;) ). And related action in the main controller: def login if request.post? @user = User.find_by_username(params[:login]) if @user and @user.password_is? params[:password] session[:uid] = @user.id redirect_to :controller => 'panel', :action => 'secret' else @auth_error = 'Wrong username or password' end end end Tadaaaaa… One detail: Sucessful login ends up with an error… That’s because we don’t have secret action in controller panel. Checking if user is logged inUser was logged in with this line: session[:uid] = @user.id So when we have session[:uid] set then we know user is in. If control panel should be accessible only for registered users, we need to check if session[:uid] is not nil before every action. There is an elegant way of doing this, and that is before_filter. We need to define a method that check if user is logged in, then add it to the before_filter. Some simple log out action may be useful too. When we write it down, our app/controllers/panel_controller.rb looks like this: class PanelController < ApplicationController before_filter :check_auth def secret render :text => 'This text is only for authenticated users' end def log_out session[:uid] = nil flash[:info] = 'You\'re logged out' redirect_to :controller => 'main' end private def check_auth unless session[:uid] flash[:error] = 'You need to be logged in to access this panel' redirect_to :controller => 'main', :action => 'login' end end end Now try entering http://localhost:3000/panel/secret I assume here that you have action index defined in your main controller, which also prints flash[:info] or flash[:error] – I believe you can prepare it by yourself If you have some problems running it, here is a complete rails app that we’ve made already. Remembering userOn pages that you visit often, there’s usually very nice option “remember me” when you’re logging in. Let’s try to implement it. How to do it? We’re going to store a cookie on the client side, informing that given user is logged in. One cookie is enough, but in this implementation we’re going to use two:
Why some strange hash instead of just password? That’s because of security. I’ve made some assumptions about security:
We’ll need new column to store our cookie hash. Let’s generate a migration: $ ruby script/generate migration AddUserCookieHash It looks like this: class AddUserCookieHash < ActiveRecord::Migration def self.up add_column :users, :cookie_hash, :string end def self.down remove_column :users, :cookie_hash end end We run it (rake db:migrate) and add in the logging in form: <%= check_box_tag :remember %> remember me
Now we modify main controller. To be clear I paste whole log_in action: def login if request.post? @user = User.find_by_username(params[:login]) if @user and @user.password_is? params[:password] session[:uid] = @user.id if params[:remember] # if user wants to be remembered cookie_pass = [Array.new(9){rand(256).chr}.join].pack("m").chomp cookie_hash = Digest::MD5.hexdigest(cookie_pass + @user.password_salt) cookies[:userapp_login_pass] = { :value => cookie_pass, :expires => 30.days.from_now } cookies[:userapp_login] = { :value => @user.username, :expires => 30.days.from_now } User.update(@user.id, :cookie_hash => cookie_hash) end redirect_to :controller => 'panel', :action => 'secret' else @auth_error = 'Bad username or password' end end So we’ve put some cookies in user’s browser wtih data that lets him authenticate. Now let’s write a code that will authenticate him if those cookies are present (and correct). We want to log user in independetly of which controller he’ll be starting with. That’s why we’re gonna use our main controller (app/controllers/application.rb) class ApplicationController < ActionController::Base # Pick a unique cookie name to distinguish our session data from others' session :session_key => '_userapp_session_id' before_filter :check_cookie def check_cookie return if session[:uid] if cookies[:userapp_login] @user = User.find_by_username(cookies[:userapp_login]) return unless @user cookie_hash = Digest::MD5.hexdigest(cookies[:userapp_login_pass] + @user.password_salt) if @user.cookie_hash == cookie_hash flash[:info] = 'You\'ve been automatically logged in' # annoying msg session[:uid] = @user.id else flash[:error] = 'Something is wrong with your cookie' end end end end Now after restarting the browser user is still logged in. to be continued (maybe) your feedback is appreciated
rate this page:
3.0 (11 votes )
|
ver. 0.1.93 alpha (bugtrack, ficzers) Autor tej strony o sobie:
Student AGH (4 rok), autor programuj.com oraz skryptu do tego community. Po jakichś 6 latach PHP przerzuciłem się na Ruby i Railsy. Obecnie zajmuje się głównie frameworlami www, wcześniej pomacałem...
Statystyki (ostatnie 14 dni):
Kto linka podrzucał (alpha test shit escape):
Pokaż innym:
Zalogowani:
|