#Rails中打造用户认证系统
相信很多人都知道或者用过Devise这个gem,它在github上有将近1w3的star。但是它十分庞大和复杂,代码很难读懂,据说想学习Rails源码的话可以先看Devise的源码。使用Devise最大的问题就是定制比较困难,需要搜索文档后覆盖一些方法,但是却不知道原理。
其实实现用户认证并不复杂,完全可以从头写一个,没必要使用gem来实现。Rails的ActiveModel里面有SecurePassword,用它可以很容易的实现认证功能。本文使用Rails的SecurePassword 一步步完善用户认证系统。
##Model
首先使用 $rails g model users email:string password_digest:string
新建一个User model,有email和password_digest两个字段。其中password_digest很重要的,是存放加密后的密码的字段。
接下来需要在 User 中调用 has_secure_password 方法。
class User < ActiveRecord::Base
has_secure_password
#可以加上email的validation等
end
添加之后User 增加了三个实列方法(instance method),分别是password=, password_confirmation=, authentication, 以及一个password 属性(attribute),同时还增加了三个validation,后面会提到。用户的验证的Model中的功能就完成了。可以这样使用:
user = User.new(email: ‘user@example.com’, password: ‘’, password_confirmation: ‘nomatch’)
user.save # => false, password required
user.password = ‘mUc3m00RsqyRe’
user.save # => false, confirmation doesn’t match
user.password_confirmation = ‘mUc3m00RsqyRe’
user.save # => true
user.authenticate(‘notright’) # => false
user.authenticate(‘mUc3m00RsqyRe’) # => user
User.find_by(email: ‘user@example.com’).try(:authenticate, ‘notright’) # => false
User.find_by(email: ‘user@example.com’).try(:authenticate, ‘mUc3m00RsqyRe’) # => user
只添加了一行代码就获得了认证功能!
##has_secur_password 做了什么? 下面分析SecurePassword的源码,内容基于Rails 4.2.1。
def has_secure_password(options = {})
# Load bcrypt gem only when has_secure_password is used.
# This is to avoid ActiveModel (and by extension the entire framework)
# being dependent on a binary library.
begin
require ‘bcrypt’
rescue LoadError
$stderr.puts “You don’t have bcrypt installed in your application. Please add it to your Gemfile and run bundle install”
raise
end
include InstanceMethodsOnActivation
if options.fetch(:validations, true)
include ActiveModel::Validations
# This ensures the model has a password by checking whether the password_digest
# is present, so that this works with both new and existing records. However,
# when there is an error, the message is added to the password attribute instead
# so that the error message will make sense to the end-user.
validate do |record|
record.errors.add(:password, :blank) unless record.password_digest.present?
end
validates_length_of :password, maximum: ActiveModel::SecurePassword::MAX_PASSWORD_LENGTH_ALLOWED
validates_confirmation_of :password, allow_blank: true
end
end
has_secure_password require了bcrypt,所以我们也需要在Gemfile中添加这个gem。然后使include 了 InstanceMethodsOnActivation 这个module,最后添加了三个validations。
###InstanceMethodsOnActivation 添加了三个实例方法和一个属性方法。
- password=
def password=(unencrypted_password)
if unencrypted_password.nil?
self.password_digest = nil
elsif !unencrypted_password.empty?
@password = unencrypted_password
cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost
self.password_digest = BCrypt::Password.create(unencrypted_password, cost: cost)
end
end
如果unencrypted_password时nil 那么password_digest 也是nil,如果unencrypted_password 不是空字符串则会调用bcrypt的BCrypt::Password.create(unencrypted_password, cost: cost)根据password的值生成一个加盐后的hash赋给password_digest, 也即对passsword赋值回生成或更新password_digset的值, 调用user.save后会把password_digest存储到数据库。
-
attr_reader :password 获上个函数赋值的@password
-
password_confirmation= 添加@password_confirmation实例变量
def password_confirmation=(unencrypted_password)
@password_confirmation = unencrypted_password
end
- authenticate
def authenticate(unencrypted_password)
BCrypt::Password.new(password_digest).is_password?(unencrypted_password) && self
end
如果password正确返回self,否则返回false。
- 辅助函数BCrypt::Password.create 和 BCrypt::Password.new 以及 BCrypt::Password#is_password? 感兴趣可以看bcrypt的源码,这里只列出了重要部分并在注释中稍作说明。
module BCrypt
class Password < String
class << self
#对一个密钥(也即password)进行哈希(hash),然后根据哈希的结果生成一个Password实例,Password继承了String。BCrypt::Engine.generate_salt(cost)生成一个salt值,cost越大越难破解,相应耗费的计算机的计算时间也越多。BCrypt::Engine.hash_secret(secret, BCrypt::Engine.generate_salt(cost))则会根据密钥和salt值生成一个哈希值。我对加密不熟悉,以上只是根据代码进行了推测。
def create(secret, options = {})
cost = options[:cost] || BCrypt::Engine.cost
raise ArgumentError if cost > 31
Password.new(BCrypt::Engine.hash_secret(secret, BCrypt::Engine.generate_salt(cost)))
end
end
#检测哈希值,如果有效则调用String的replace函数替换自身为这个哈希值。
def initialize(raw_hash)
if valid_hash?(raw_hash)
self.replace(raw_hash)
@version, @cost, @salt, @checksum = split_hash(self)
else
raise Errors::InvalidHash.new(“invalid hash”)
end
end
end
# 比较密钥。BCrypt::Engine.hash_secret(secret, @salt)根据新密钥和salt值计算新的哈希值,如果新哈希值和原来的哈希值相同则返回true,否则返回false。
def ==(secret)
super(BCrypt::Engine.hash_secret(secret, @salt))
end
alias_method :is_password?, :==
end
###validations validation 默认开启,可以通过has_secure_password(validations: false) 来关闭。
- password 是否存在
validate do |record|
record.errors.add(:password, :blank) unless record.password_digest.present?
end
这个validatioin检查password_digest是否存在,对新纪录(User.new)和已存在的记录(从数据库加载)都会执行检查, 但是错误信息回家到password 这个属性上。
- 检查password长度和confirmation
validates_length_of :password, maximum: ActiveModel::SecurePassword::MAX_PASSWORD_LENGTH_ALLOWED
validates_confirmation_of :password, allow_blank: true # password_confirmation可以为空或nil, 上面的password_confirmation=是为了validation关闭的时候还有这个方法
##注册
$rails g controller users new
生成UsersController
class UsersController < ApplicationController
def new
end
def create
@user = User.new(params.require(:user).permit(:email, :password, :password_confirmation))
if @user.save
flash[:notice] = ‘User was successfully created.’
redirect_to(@user)
else
render :new
end
end
end
app/views/users/new.html.slim
= form_for @user do |f|
= render “share/form_error_messages”, form: f
.form-group
= f.label :email, class: “control-label”
= f.email_field :email, class: “form-control”, tabindex: 2, required: true
.form-group
= f.label :password, class: “control-label”
= f.password_field :password, class: “form-control”, tabindex: 4, required: true
.form-group
= f.label :password_confirmation, class: “control-label”
= f.password_field :password_confirmation, class: “form-control”, tabindex: 5, required: true
.form-group
= f.submit “注册用户”, class: “btn btn-success”, tabindex: 6
##登陆
$rails g controller sessions new
class SessionsController < ApplicationController
def new
end
def create
user = User.find_by(email: params[:email])
if user && user.authenticate(params[:password])
session[:user_id] = user.id
redirect_to root_url
else
flash[:warning] = “wrong email/password”
render :new
end
end
end
view ‘’’ = form_tag login_path, method: “post” do .form-group = label_tag :email, nil, class: “control-label” = email_field_tag :email, nil, class: “form-control” .form-group = label_tag :password, nil, class: “control-label” = password_field_tag :password, nil, class: “form-control” = submit_tag “登陆”, class: “btn btn-success” ‘’’
最后再添加一个helper method:
helper_method :current_user
def current_user
@current_user = User.find(session[:user_id]) if session[:user_id].present?
end
用户认证的基本功能就算完成了,可以在view里面根据是否已经登陆显示不同内容了。
##需要完善的功能
- 发送确认邮件
- 记住我的功能
- 找回密码功能