登录相对于前面几章来说是个相对复杂的流程,主要体现在必须记住用户,必须利用加密算法来保证安全性
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列出:
require 'spec_helper'
describe "AuthenticationPages" do
subject { page }
describe "signin" do
before {visit signin_path}
describe "with invalid information" do
before { click_button "Sign in" }
it { should have_title('Sign in') }
it { should have_selector('div.alert.alert-error', text: 'Invalid') }
describe "after visiting another page" do
before { click_link "Home" }
it { should_not have_selector('div.alert.alert-error') }
end
end
describe "with valid information" do
let(:user) {FactoryGirl.create(:user)}
before do
fill_in "Email", with: user.email.upcase
fill_in "Password", with: user.password
click_button "Sign in"
end
it { should have_title(user.name)}
it { should have_link('Profile', href: user_path(user)) }
it { should have_link('Sign out', href: signout_path)}
it { should_not have_link('Sign in', href: signin_path)}
describe "followed by signout" do
before {click_link "Sign out"}
it { should have_link('Sign in')}
end
end
end
end
<%= form_for(:session, url: sessions_path) do |f| %>
<%= f.label :email %>
<%= f.text_field :email %>
<%= f.label :password %>
<%= f.password_field :password %>
<%= f.submit "Sign in", class: "btn btn-large btn-primary" %>
<% end %>
New user? <%= link_to "Sign up now!", signup_path %>
module SessionsHelper
def sign_in(user)
remember_token = User.new_remember_token
cookies.permanent[:remember_token] = remember_token
user.update_attribute(:remember_token, User.encrypt(remember_token))
self.current_user = user
end
def sign_out
self.current_user = nil
cookies.delete(:remember_token)
end
def signed_in?
!current_user.nil?
end
def current_user=(user)
@current_user = user
end
def current_user
remember_token =User.encrypt(cookies[:remember_token])
@current_user ||= User.find_by(remember_token: remember_token)
end
end
上述代码组找了设定的步骤:首先,创建新权标;随后,把未加密的权标存入浏览器的 cookie;然后,把加密后的权标存入数据库;最后,把制定的用户设为当前登入的用户。
注意,保存记忆权标使用的是 update_attribute 方法,这样可以跳过数据验证更新单个属性。我们必须用这个方法,因为我们无法提供用户的密码及密码确认。
因为数据库中保存的记忆权标是加密的,所以在用来查找用户之前要加密从 cookie 中读取的权标
注意最后一个方法,我们获取当前用户用的是remember_token,但是cookies中存储的是为加密的权标,所以必须加密一次。
对于代码 @current_user ||= User.find_by(remember_token: remember_token)
只有第一次调用时才会去执行后面的部分,这里利用了||运算的短路性质
5.处理登录流程的代码如下:
class SessionsController < ApplicationController
def new
end
def create
user = User.find_by(email: params[:session][:email].downcase)
if user && user.authenticate(params[:session][:password])
sign_in user
redirect_to user
else
flash.now[:error] = 'Invalid email/password combination' # Not quite right!
render 'new'
end
end
def destroy
sign_out
redirect_to root_path
end
end
resources :sessions, only: [:new,:create,:destroy]
#get "users/new"
root 'static_pages#home'
match '/signup', to: 'users#new', via:'get'
match '/signin', to: 'sessions#new', via: 'get'
match '/signout',to: 'sessions#destroy', via: 'delete'
注意signout对应的Http方法为delete
class User < ActiveRecord::Base
before_save { self.email = email.downcase }
before_create :create_remember_token
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
validates :name, presence: true, length: { maximum: 50}
validates :email, presence:true,
format: {with: VALID_EMAIL_REGEX},
uniqueness: {case_sensitive: false}
has_secure_password
validates :password, length: {minimum: 6}
def User.new_remember_token
SecureRandom.urlsafe_base64
end
def User.encrypt(token)
Digest::SHA1.hexdigest(token.to_s)
end
private
def create_remember_token
self.remember_token = User.encrypt(User.new_remember_token)
end
end