【Rails学习笔记】登录和退出功能实现

来源:互联网 发布:朗诵配乐知乎 编辑:程序博客网 时间:2024/06/10 01:36


登录相对于前面几章来说是个相对复杂的流程,主要体现在必须记住用户,必须利用加密算法来保证安全性


1.分析下登录流程和需要的工作:

首先表单必须写好,界面部分需要处理用户为登录状态和登出状态两种情况

用户登录成功时,如何让系统记住用户,如何保证安全性

用户退出时,后台如何处理。

另外需要设置好路由规则


如何实现Session:

网络中常见的 session 处理方式有好几种:可以在用户关闭浏览器后清除 session;也可以提供一个“记住我”单选框让用户选择永远保存,直到用户退出后 session 才会失效。 我们选择使用第二种处理方式,即用户登录后,会永久的记住登录状态,直到用户点击“退出”链接之后才清除 session。


很显然,我们可以把 session 视作一个符合 REST 架构的资源,在登录页面中准备一个新的 session,登录后创建这个 session,退出则会销毁 session。不过 session 和 Users 资源有所不同,Users 资源使用数据库(通过 User 模型)持久的存储数据,而 Sessions 资源是利用 cookie 来存储数据的。cookie 是存储在浏览器中的简单文本。实现登录功能基本上就是在实现基于 cookie 的验证机制。


如何记住用户:

因为 HTTP 是无状态的协议,所以如果应用程序需要实现登录功能的话,就要找到一种方法记住用户的状态。维持用户登录状态的方法之一,是使用常规的 Rails session(通过 session 函数),把用户的 id 保存在“记忆权标(remember token)”中:

session[:remember_token] = user.id


session 对象把用户 id 保存在浏览器的 cookie 中,这样在网站的所有页面就都可以使用了。浏览器关闭后,cookie 也随之失效。在网站中的任何页面,只需调用 User.find(session[:remember_token]) 就可以取回用户对象了。Rails 在处理 session 时,会确保安全性。倘若用户企图伪造用户 id,Rails 可以通过每个 session 的 session id 检测到。


根据示例程序的设计目标,我们计划要实现的是持久保存的 session,即使浏览器关闭了,登录状态依旧存在,所以,登入的用户要有一个持久保存的标识符才行。为此,我们要为每个用户生成一个唯一而安全的记忆权标,长期存储,不会随着浏览器的关闭而消失。


如何存储权标?

我们计划在浏览器中存储base64 权标,在数据库中存储加密后的版本。如果要自动登入用户,就可以从 cookie 中取出记忆权标,加密后查询数据库。数据库之所以只保存加密后的权标是因为,即便整个数据库都泄露了,攻击者也无法使用记忆权标登入网站。为了让记忆权标更安全,我们计划每次会话都生成不一样的权标,这样即使会话被劫持了(攻击者偷取 cookie 伪装成某个用户登录),用户下次登录时前一个会话就会失效。


用户注册成功后会自动登录吗:


真实的应用程序都会自动登入刚注册的用户(这样做的一个副作用就是创建了一个新的记忆权标),但是我们不想这么做,我们要用一种更好的方式,确保从一开始用户就有可用的记忆权标。



2.将登录和退出的测试文件authentication_pages_spec列出:

[ruby] view plaincopyprint?
  1. require 'spec_helper'  
  2.   
  3. describe "AuthenticationPages" do  
  4.   subject { page }  
  5.   
  6.   describe "signin" do  
  7.     before {visit signin_path}  
  8.   
  9.    
  10.     describe "with invalid information" do  
  11.       before { click_button "Sign in" }  
  12.   
  13.       it { should have_title('Sign in') }  
  14.       it { should have_selector('div.alert.alert-error', text: 'Invalid') }  
  15.   
  16.       describe "after visiting another page" do  
  17.         before { click_link "Home" }  
  18.         it { should_not have_selector('div.alert.alert-error') }  
  19.       end  
  20.     end  
  21.   
  22.     describe "with valid information" do  
  23.         let(:user) {FactoryGirl.create(:user)}  
  24.         before do  
  25.             fill_in "Email",    with: user.email.upcase  
  26.             fill_in "Password", with: user.password  
  27.             click_button "Sign in"  
  28.         end  
  29.   
  30.         it { should have_title(user.name)}  
  31.         it { should have_link('Profile', href: user_path(user)) }  
  32.         it { should have_link('Sign out', href: signout_path)}  
  33.         it { should_not have_link('Sign in', href: signin_path)}  
  34.   
  35.       describe "followed by signout" do  
  36.         before {click_link "Sign out"}  
  37.         it { should have_link('Sign in')}  
  38.       end  
  39.     end  
  40.   end  
  41. end  

3.用户登录的表单如下:

[html] view plaincopyprint?
  1. <div class="row">  
  2.   <div class="span6 offset3">  
  3.     <%= form_for(:session, url: sessions_path) do |f| %>  
  4.   
  5.       <%= f.label :email %>  
  6.       <%= f.text_field :email %>  
  7.   
  8.       <%= f.label :password %>  
  9.       <%= f.password_field :password %>  
  10.   
  11.       <%= f.submit "Sign in", class: "btn btn-large btn-primary" %>  
  12.     <% end %>  
  13.   
  14.     <p>New user? <%= link_to "Sign up now!", signup_path %></p>  
  15.   </div>  
  16. </div>  

4.用户认证需要的方法Sessions_Helps 如下:


[ruby] view plaincopyprint?
  1. module SessionsHelper  
  2.     def sign_in(user)  
  3.         remember_token = User.new_remember_token  
  4.         cookies.permanent[:remember_token] = remember_token  
  5.         user.update_attribute(:remember_token, User.encrypt(remember_token))  
  6.         self.current_user = user          
  7.     end  
  8.   
  9.     def sign_out   
  10.         self.current_user = nil  
  11.         cookies.delete(:remember_token)  
  12.     end  
  13.   
  14.     def signed_in?  
  15.         !current_user.nil?  
  16.     end  
  17.   
  18.     def current_user=(user)  
  19.         @current_user = user  
  20.     end  
  21.   
  22.     def current_user  
  23.         remember_token  =User.encrypt(cookies[:remember_token])  
  24.         @current_user ||= User.find_by(remember_token: remember_token)  
  25.   
  26.     end  
  27. end  

对于signin方法有以下说明:

上述代码组找了设定的步骤:首先,创建新权标;随后,把未加密的权标存入浏览器的 cookie;然后,把加密后的权标存入数据库;最后,把制定的用户设为当前登入的用户。

注意,保存记忆权标使用的是 update_attribute 方法,这样可以跳过数据验证更新单个属性。我们必须用这个方法,因为我们无法提供用户的密码及密码确认。


因为数据库中保存的记忆权标是加密的,所以在用来查找用户之前要加密从 cookie 中读取的权标


注意最后一个方法,我们获取当前用户用的是remember_token,但是cookies中存储的是为加密的权标,所以必须加密一次。

对于代码 @current_user ||= User.find_by(remember_token: remember_token)

只有第一次调用时才会去执行后面的部分,这里利用了||运算的短路性质


5.处理登录流程的代码如下:

[ruby] view plaincopyprint?
  1. class SessionsController < ApplicationController  
  2.     def new  
  3.   
  4.     end  
  5.   
  6.     def create  
  7.         user = User.find_by(email: params[:session][:email].downcase)  
  8.         if user && user.authenticate(params[:session][:password])  
  9.             sign_in user  
  10.             redirect_to user  
  11.         else  
  12.             flash.now[:error] = 'Invalid email/password combination' # Not quite right!  
  13.             render 'new'  
  14.         end  
  15.     end  
  16.   
  17.     def destroy  
  18.         sign_out  
  19.         redirect_to root_path  
  20.     end  
  21.   
  22. end  

6.路由规则如下:

[ruby] view plaincopyprint?
  1. resources :sessions, only: [:new,:create,:destroy]  
  2.   
  3.   #get "users/new"  
  4.   root 'static_pages#home'  
  5.   match '/signup', to: 'users#new', via:'get'  
  6.   match '/signin', to: 'sessions#new', via: 'get'  
  7.   match '/signout',to: 'sessions#destroy', via: 'delete'  


注意signout对应的Http方法为delete


7.一些加密/产生记忆权标的方法将其放在Model中:

[ruby] view plaincopyprint?
  1. class User < ActiveRecord::Base  
  2.     before_save { self.email = email.downcase }  
  3.     before_create :create_remember_token  
  4.   
  5.     VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i  
  6.     validates :name, presence: true, length: { maximum: 50}  
  7.     validates :email, presence:true,   
  8.         format: {with: VALID_EMAIL_REGEX},  
  9.         uniqueness: {case_sensitive: false}  
  10.       
  11.     has_secure_password  
  12.     validates :password, length: {minimum: 6}  
  13.   
  14.     def User.new_remember_token  
  15.         SecureRandom.urlsafe_base64  
  16.     end  
  17.   
  18.     def User.encrypt(token)  
  19.         Digest::SHA1.hexdigest(token.to_s)  
  20.     end  
  21.   
  22.     private  
  23.         def create_remember_token  
  24.             self.remember_token = User.encrypt(User.new_remember_token)  
  25.         end  
  26. end  



0 0
原创粉丝点击