Creating a Login System
Problem
You want your application to support a login system based on user accounts. Users will log in with a unique username and password, as in most commercial and community web sites.
Solution
Create a users table that contains nonnull username and password fields. The SQL to create this table should look something like this MySQL example:
use mywebapp_development; DROP TABLE IF EXISTS users; CREATE TABLE users ( id INT(11) NOT NULL AUTO_INCREMENT, username VARCHAR(255) NOT NULL, password VARCHAR(40) NOT NULL, PRIMARY KEY (id) );
Enter the main directory of the application and generate a User model corresponding to this table:
$ ./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
Open the generated file app/models/user.rb and edit it to look like this:
class User < ActiveRecord::Base validates_uniqueness_of :username validates_confirmation_of :password, :on => :create validates_length_of :password, :within => 5..40 # If a user matching the credentials is found, returns the User object. # If no matching user is found, returns nil. def self.authenticate(user_info) find_by_username_and_password(user_info[:username], user_info[:password]) end end
Now youve got a User class that represents a user account, and a way of validating a username and password against the one stored in the database.
Discussion
The simple User model given in the Solution defines a method for doing username/password validation, and some validation rules that impose limitations on the data to be stored in the users table. These validation rules tell User to:
- Ensure that each username is unique. No two users can have the same username.
- Ensure that, whenever the password attribute is being set, the password_confirmation attribute has the same value.
- Ensure that the value of the password attribute is between 5 and 40 characters long.
Now lets create a controller for this model. Itll have a login action to display the login page, a process_login action to check the username and password, and a logout action to deauthenticate a logged-in session. So that the user accounts will actually do something, well also add a my_account action:
$ ./script/generate controller user login process_login logout my_account exists app/controllers/ exists app/helpers/ create app/views/user exists test/functional/ create app/controllers/user_controller.rb create test/functional/user_controller_test.rb create app/helpers/user_helper.rb create app/views/user/ login.rhtml create app/views/user/process_login.rhtml create app/views/user/logout.rhtml
Edit app/controllers/user_controller.rb to define the three actions:
class UserController < ApplicationController def login @user = User.new @user.username = params[:username] end def process_login if user = User.authenticate(params[:user]) session[:id] = user.id # Remember the users id during this session redirect_to session[:return_to] || / else flash[:error] = Invalid login. redirect_to :action => login, :username => params[:user][:username] end end def logout reset_session flash[:message] = Logged out. redirect_to :action => login end def my_account end end
Now for the views. The process_login and logout actions just redirect to other actions, so we only need views for login and my_account. Heres a view for login:
<% if @flash[:message] %><%= @flash[:message] %><% end %> <% if @flash[:error] %><%= @flash[:error] %><% end %> <%= form_tag :action => process_login %> Username: <%= text_field "user", "username" %> Password: <%= password_field "user", "password" %> <%= submit_tag %> <%= end_form_tag %>
The @flash instance variable is a hashlike object used to store temporary messages for the user between actions. When the logout action sets flash[:message] and redirects to login, or process_login sets flash[:error] and redirects to login, the results are available to the view of the login action. Then they get cleared out.
Heres a very simple view for my_account:
Account Info
Your username is <%= User.find(session[:id]).username %>
Create an entry in the users table, start the server, and youll find that you can log in from http://localhost:3000/user/login , and view your account information from http://localhost:3000/user/my_account.
$ ./script/runner User.create(:username => "johndoe", :password => "changeme")
Theres just one missing piece: you can visit the my_account action even if you e not logged in. We don have a way to close off an action to unauthenticated users. Add the following code to your app/controllers/application.rb file:
class ApplicationController < ActionController::Base before_filter :set_user protected def set_user @user = User.find(session[:id]) if @user.nil? && session[:id] end def login_required return true if @user access_denied return false end def access_denied session[:return_to] = request.request_uri flash[:error] = Oops. You need to login before you can view that page. redirect_to :controller => user, :action => login end end
This code defines two filters, set_user and login_required, which you can apply to actions or controllers. The set_user filter is run on every action (because we pass it into before_filter in ApplicationController, the superclass of all our controllers). The set_user method sets the instance variable @user if the user is logged in. Now information about the logged-in user (if any) is available throughout your application. Action methods and views can use this instance variable like any other. This is useful even for actions that don require login: for instance, your main layout view might display the name of the logged-in user (if any) on every page.
You can prohibit unauthenticated users from using a specific action or controller by passing the symbol for the login_required method into before_filter. Heres how to protect the my_account action defined in app/controllers/user_controller.rb:
class UserController < ApplicationController before_filter :login_required, :only => :my_account end
Now if you try to use the my_account action without being logged in, youll be redirected to the login page.
See Also
- Recipe 13.14, "Validating Data with ActiveRecord"
- Recipe 15.6, "Integrating a Database with Your Rails Application"
- Recipe 15.9, "Storing Hashed User Passwords in the Database"
- Recipe 15.11, "Setting and Retrieving Session Information"
- Rather than doing this work yourself, you can install the login_generator gem and use its login generator: it will give your application a User model and a controller that implements a password-based authentication system; see http://wiki.rubyonrails.com/rails/pages/LoginGenerator; also see http://wiki.rubyonrails.com/rails/pages/AvailableGenerators for other generators (including the more sophisticated model_security_generator)
Категории