The "remember me" checkbox
2 months ago
Here's a nifty solution that I came up with for implementing the "remember me" checkbox on login forms in Rails.
First you'll need this extension to CGI::Session::CookieStore
:
# This adds the ability to dynamically set the expiry on session cookies, # so that a session can persists across browser restarts. # # In your controller, just do something like: # # session[:expires] = 2.weeks.from_now # # The expiry is also stored in the session, and double checked when the # cookie is loaded to prevent malicious reuse of old cookies. class CGI::Session::ExpiringCookieStore < CGI::Session::CookieStore def unmarshal(cookie) session = super(cookie) session = nil if session && session[:expires] && session[:expires] <= Time.now session end def write_cookie(options) options["expires"] = @data[:expires] if @data super(options) end end
You'll need to change the session store in your environment.rb
:
config.action_controller.session_store = :expiring_cookie_store
In your controller, if the "remember me" checkbox is set, just do this:
session[:expires] = 2.weeks.from_now
Voilà! Now your session (which also holds the id of the logged in user, if you implement logins in the usual way) will stick around for up to 2 weeks, even between browser restarts.
(Obviously this only works with the cookie-based session store.)
If you spot any problems or security holes with this, please let me know.
Auto-login 28
One of my midnight Rails projects is a “time tracking” application for which I needed auto-login. You know, the “Remember me” check box so that you don’t have to login each time you visit the application. I found a nice article written by Matt McCray describing how this was implemented for TaskThis.com at http://www.mattmccray.com/archives/category/software/rails/taskthis/. Even further he provides the full source code for the application. I didn’t take directly his auto_login.rb module but was greatly inspired by it. I also used the Login Engine Plugin that was not providing this feature, maybe this changed, so it could be simpler, but how simple implementing the auto-login can be. Note these are not the full classes just pertinent code extracts.
1. Remember me
When the user login and checks the “Remember me” checkbox, the :save_login parameter is set, the User instance remember_me method invoked and the :auth_token cookie set.
class AccountController < ApplicationController
def login
case @request.method
when :post
if @session[:user] = User.authenticate(@params[:user_login], @params[:user_password])
flash['notice'] = "Login successful"
if @params[:save_login] == "1"
@session[:user].remember_me
cookies[:auth_token] = { :value => @session[:user].remember_token , :expires => @session[:user].remember_token_expires }
end
redirect_back_or_default :controller => "time"
else
flash.now['notice'] = "Login unsuccessful"
@login = @params[:user_login]
end
end
end
def logout
@session[:user].forget_me if @session[:user]
@session[:user] = nil
cookies.delete :auth_token
end
end
2. login_from_cookie
The next time the user visits the website the “login_from_cookie” filter is triggered. This method checks that the user is not logged in and that the :auth_token cookie is set. If that’s the case the user matching the :auth_token is searched and the token_expiration verified the the user is automatically logged in. Et voila! I guess auto_login would be more appropriate as method name.
class ApplicationController < ActionController::Base
before_filter :login_from_cookie
def login_from_cookie
return unless cookies[:auth_token] && @session[:user].nil?
user = User.find_by_remember_token(cookies[:auth_token])
if user && !user.remember_token_expires.nil? && Time.now < user.remember_token_expires
@session[:user] = user
end
end
end
3. the User class
The User class has two methods to set and remove the token from the database. It’s pretty secure as from the token the user cannot be identified without having the salt, the email, and the token expiration, which is most unlikely to be recreated. It could be even more secure by just encrypting some random unique identifier. The only issue I encountered was that the user class always forces the password validation and encryption when saving. For now I just bypass validation and encryption when setting and clearing the remember_me token.
class User < ActiveRecord::Base
def remember_me
self.remember_token_expires = 2.weeks.from_now
self.remember_token = Digest::SHA1.hexdigest("#{salt}--#{self.email}--#{self.remember_token_expires}")
self.password = "" # This bypasses password encryption, thus leaving password intact
self.save_with_validation(false)
end
def forget_me
self.remember_token_expires = nil
self.remember_token = nil
self.password = "" # This bypasses password encryption, thus leaving password intact
self.save_with_validation(false)
end
end
关于记住用户状态的实现,大部分用户认证的插件都有。参考
User+authentication+in+Ruby+on+Rails:
添加一个cookie_hash字段到user中:
CODE:
class AddUserCookieHash < ActiveRecord::Migration
def self.up
add_column :users, :cookie_hash, :string
end
def self.down
remove_column :users, :cookie_hash
end
end
接着在登录页面,如login.html.erb中,加入:
CODE:
<%= check_box_tag :remember %> remember me
然后在管理login的controller中添加:
CODE:
def login
if request.post?
@user = User.find_by_username(params[:login])
if @user and @user.password_is? params[:password]
session[:uid] = @user.id
# 当用户需要被记住时,开始对cookie进行处理
# 对cookie生成一个密钥之后放入cookie和存入数据库(user表中)
# 其中还指定了一个cookies失效时间,默认为30天,其实可以把这个参数提出来
if params[:remember]
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
最后在ApplicationController中加入:
CODE:
session :session_key => '_userapp_session_id'
before_filter :check_cookie
def check_cookie
return if session[:uid]
if cookies[:logowanie_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
而关于角色的认证可以使用插件:
ActiveRBAC