[译]Rails中使用Authlogic进行用户鉴定(Rails Authentication with Authlogic)
近日,发现Authlogic这个gem,看上去比devise清爽了不少,于是打算用用,于是发现了这篇文章,遂翻成中文当做学习。
今天,我很高兴的为大家介绍 AuthLogic,一个简单地Ruby下的用户鉴定解决方案,它的作者是 Ben Johnson.
AuthLogic 它很隐蔽和底层。它不依赖于生成的代码 (举个例子,例如 Devise) ——相反地,它提供了一些工具使得你可以用来安装你喜欢的方式构建你的应用。感觉上,, AuthLogic和 Sorcery有些相似。
下面让我们创建一个Rails应用,它可以允许用户注册、登入登出,同时还可以重置密码。在实现这些的同时,我们探讨一些 AuthLogic的特性和设置。阅读本文之后,你已经准备好在你的实际应用中使用AuthLogic了。
你可以在GitHub.找到本文的代码。
演示应用在这里: sitepoint-authlogic.herokuapp.com.
开始
我将为你们演示的demo app 基于Rails 4.1,当然AuthLogic是支持 Rails 3, 甚至是 Rails 2的 (需要独立的分支 separate branch ).
创建一个新的应用,名为 “Logical” ,并不生成标准的测试套件:
$ rails new Logical -T
添加如下的gems:
Gemfile
[...]
gem 'bootstrap-sass'
gem 'authlogic', '3.4.6'
[...]
然后执行下述命令:
$ bundle install
如果你愿意,就可以使用 Bootstrap的样式了:
stylesheets/application.scss
@import 'bootstrap-sprockets';
@import 'bootstrap';
然后,编辑view的layout:
views/layouts/application.html.erb
[...]
<nav class="navbar navbar-inverse">
<div class="container">
<div class="navbar-header">
<%= link_to 'Logical', root_path, class: 'navbar-brand' %>
</div>
<div id="navbar">
<ul class="nav navbar-nav">
<li><%= link_to 'Home', root_path %></li>
</ul>
</div>
</div>
</nav>
<div class="container">
<% flash.each do |key, value| %>
<div class="alert alert-<%= key %>">
<%= value %>
</div>
<% end %>
<%= yield %>
</div>
[...]
现在,创建静态页面的controller:
pages_controller.rb
class PagesController < ApplicationController
def index
end
end
然后,创建响应的view:
views/pages/index.html.erb
<div class="jumbotron">
<div class="container">
<h1>Welcome!</h1>
<p>Sign up to get started.</p>
<p>
<%= link_to '#', class: 'btn btn-primary btn-lg' do %>
Sign Up »
<% end %>
</p>
</div>
接下来路由:
config/routes.rb
[...]
root to: 'pages#index'
[...]
好,打完收工,接下来就让我们将AuthLogic 集成进来!
安装设置AuthLogic
Models
使用AuthLogic需要包含两个model:一个基础的我们通常称为User,一个特殊的
需要继承自Authlogic::Session::Base,并称之为
UserSession
(你可以看看 这篇说明 ,快速地了解它的工作原理)。
第一步,当然是新建一个数据库的 migration:
$ rails g model User email:string crypted_password:string password_salt:string persistence_token:string
(这里 是所有可能需要的字段列表)
然后需要在生成的migration中添加下面一行:
migrations/xxx_create_users.rb
[...]
add_index :users, :email, unique: true
[...]
执行migrate:
$ rake db:migrate
这几个字段 email
, crypted_password和
password_salt都实际上是可选的
。AuthLogic并不关心你使用什么手段鉴定用户——使用LDAP 亦或 OAuth 2,, 这里是一份列表 list of AuthLogic “add-ons” 用来支持不同的鉴定方式,然而,它们中的许多已经没人维护了。
顺便说一下,你可以添加一个login字段,这样
AuthLogic将会用它来替代email。
字段persistence_token
是必须的。 AuthLogic 用它来存放用户的会话安全码。
下面修改User model:
models/user.rb
[...]
acts_as_authentic
[...]
这已经给 User
model添加了 AuthLogic 功能了。 acts_as_authentic
将会接受一个block,用来重载默认的设置项(比如口令验证规则——我们将会在本文最后一段来讨论这个话题)。
现在我们要创建一个全新的,特殊的model:
models/user_session.rb
class UserSession < Authlogic::Session::Base
end
Windows用户特别说明
There is a pretty serious error that Windows users are likely to encounter when using AuthLogic with Ruby 2.1 or 2.2 (I have not tested with Ruby 1.9). Full discussion can be found on GitHub, but, in short, this error is related to SCrypt, a gem that implements secure password hashing algorithm. AuthLogic uses SCrypt as a default crypto provider, but on Windows it constantly returns segmentation error and the server crashes.
The quickest way is to use another provider – AuthLogic offers a handful of them.
In this demo I will stick to SHA512, so tweak the model:
models/user.rb
[...]
acts_as_authentic do |c|
c.crypto_provider = Authlogic::CryptoProviders::Sha512
end
[...]
Controllers 和Helpers
让我们完成管理用户的controller :
users_controller.rb
class UsersController < ApplicationController
def new
@user = User.new
end
def create
@user = User.new(users_params)
if @user.save
flash[:success] = "Account registered!"
redirect_to root_path
else
render :new
end
end
private
def users_params
params.require(:user).permit(:email, :password, :password_confirmation)
end
end
看上去,这就是一个基本的controller,响应用的注册动作。注意一定要同时permit password
和 password_confirmation
两个属性,因为,默认的,AuthLogic将会检查他们是否一致。
下面是管理用户登入登出的controller:
user_sessions_controller.rb
class UserSessionsController < ApplicationController
def new
@user_session = UserSession.new
end
def create
@user_session = UserSession.new(user_session_params)
if @user_session.save
flash[:success] = "Welcome back!"
redirect_to root_path
else
render :new
end
end
def destroy
current_user_session.destroy
flash[:success] = "Goodbye!"
redirect_to root_path
end
private
def user_session_params
params.require(:user_session).permit(:email, :password, :remember_me)
end
end
正如你看到的,我们使用 UserSession
model 来鉴定用户,它自动持久化用户会话,因此controller本身显得非常清爽。
current_user_session
? 是什么?它是一个helper 方法:
application_controller.rb
[...]
private
def current_user_session
return @current_user_session if defined?(@current_user_session)
@current_user_session = UserSession.find
end
def current_user
return @current_user if defined?(@current_user)
@current_user = current_user_session && current_user_session.user
end
helper_method :current_user_session, :current_user
[...]
UserSession.find
自动使用 persistence_token
来找到当前的会话。
我还同时添加了 current_user来容易的访问当前用户的记录
。
路由
我们设置了一下路由:
config/routes.rb
[...]
resources :users, only: [:new, :create]
resources :user_sessions, only: [:create, :destroy]
delete '/sign_out', to: 'user_sessions#destroy', as: :sign_out
get '/sign_in', to: 'user_sessions#new', as: :sign_in
[...]
视图
最后,我们来搞定视图。
首先,是 layout:
views/layouts/application.html.erb
[...]
<nav class="navbar navbar-inverse">
<div class="container">
<div class="navbar-header">
<%= link_to 'Logical', root_path, class: 'navbar-brand' %>
</div>
<div id="navbar">
<ul class="nav navbar-nav">
<li><%= link_to 'Home', root_path %></li>
</ul>
<ul class="nav navbar-nav pull-right">
<% if current_user %>
<li><span><%= current_user.email %></span></li>
<li><%= link_to 'Sign Out', sign_out_path, method: :delete %></li>
<% else %>
<li><%= link_to 'Sign In', sign_in_path %></li>
<% end %>
</ul>
</div>
</div>
</nav>
[...]
在这里,我简单的写了一个顶部的菜单。
然后是 “注册”页面:
views/users/new.html.erb
<div class="page-header"><h1>Register</h1></div>
<%= form_for @user do |f| %>
<%= render 'shared/errors', object: @user %>
<div class="form-group">
<%= f.label :email %>
<%= f.email_field :email, class: 'form-control' %>
</div>
<div class="form-group">
<%= f.label :password %>
<%= f.password_field :password, class: 'form-control' %>
</div>
<div class="form-group">
<%= f.label :password_confirmation %>
<%= f.password_field :password_confirmation, class: 'form-control' %>
</div>
<%= f.submit 'Register', class: 'btn btn-primary btn-lg' %>
<% end %>
AuthLogic 自动验证 e-mail,验证passwords是否一致并且最少4个字母长。你可以在model/user.rb 中重新定义这些——我们将会进一步讨论他。
下面,添加共享的显示错误的部分:
views/shared/_errors.html.erb
<% if object.errors.any? %>
<div class="panel panel-warning errors">
<div class="panel-heading">
<h5><i class="glyphicon glyphicon-exclamation-sign"></i> Found errors while saving</h5>
</div>
<ul class="panel-body">
<% object.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
还有,不要忘记在 index.html.erb添加到注册页面的链接:
views/pages/index.html.erb
<div class="jumbotron">
<div class="container">
<h1>Welcome!</h1>
<p>Sign up to get started.</p>
<p>
<%= link_to new_user_path, class: 'btn btn-primary btn-lg' do %>
Sign Up »
<% end %>
</p>
</div>
</div>
接下来,终于要写登录的视图了:
views/user_sessions/new.html.erb
<div class="page-header"><h1>Sign In</h1></div>
<%= form_for @user_session do |f| %>
<%= render 'shared/errors', object: @user_session %>
<div class="form-group">
<%= f.label :email %>
<%= f.email_field :email, class: 'form-control' %>
</div>
<div class="form-group">
<%= f.label :password %>
<%= f.password_field :password, class: 'form-control' %>
</div>
<div class="form-group">
<%= f.label :remember_me %>
<%= f.check_box :remember_me %>
</div>
<%= f.submit "Log in!", class: 'btn btn-primary btn-lg' %>
<% end %>
到这里,你最好启动server来注册你的的第一个用户。
保存额外的信息
AuthLogic支持一些“魔法属性” 他们会自动的产生。你可以用这些来保存更多的用户相关信息,比如最后的登录日期或者IP地址。
创建一个新的migration:
$ rails g migration add_magic_columns_to_users
打开生成的文件并修改:
xxx_add_magic_columns_to_users.rb
class AddMagicColumnsToUsers < ActiveRecord::Migration
def change
add_column :users, :login_count, :integer, :null => false, :default => 0
add_column :users, :failed_login_count, :integer, :null => false, :default => 0
add_column :users, :last_request_at, :datetime
add_column :users, :current_login_at, :datetime
add_column :users, :last_login_at, :datetime
add_column :users, :current_login_ip, :string
add_column :users, :last_login_ip, :string
end
end
然后执行migrate:
$ rake db:migrate
接下来,为了简单起见,我们在首页上显示这些信息(如果用户登录了):
views/pages/index.html.erb
<% if current_user %>
<div class="page-header"><h1>Welcome back!</h1></div>
<h2>Some info about you...</h2>
<div class="well well-lg">
<ul>
<li>Login count: <%= current_user.login_count %></li>
<li>Failed login count: <%= current_user.failed_login_count %></li>
<li>Last request: <%= current_user.last_request_at %></li>
<li>Current login at: <%= current_user.current_login_at %></li>
<li>Last login at: <%= current_user.last_login_at %></li>
<li>Current login IP: <%= current_user.current_login_ip %></li>
<li>Last login IP: <%= current_user.last_login_ip %></li>
</ul>
</div>
<% else %>
[...]
<% end %>
登录然后看看显示结果。这些信息可能会很有用,比如你想要了解你的用户访问网站的频率。
重置密码
Users tend to forget their passwords, therefore it is crucial to present them with a way to reset it. AuthLogic provides you with a tool to add this functionality, as well.
The author of AuthLogic suggests using a simple mechanism where a user first enters an e-mail, then receives a link to update the password, and then follows the link to actually set the new password. This link contains a special “perishable” token that has to be reset.
Therefore we need to add a new field called perishable_token
to the users
table. Note that we do not call it something like reset_password_token
. AuthLogic does not dictate that this token can be used only for password resetting – you may use it, for example, to activate users’ accounts.
Apart from perishable_token
, AuthLogic also supports single_access_token
that is ideal for APIs – it provides access, but does not persist. Read more here.
Okay, so create and apply a new migration:
$ rails g migration add_perishable_token_to_users perishable_token:string
$ rake db:migrate
Obviously we need a special controller to manage password resets and a mailer.
Start with controller:
password_resets_controller.rb
class PasswordResetsController < ApplicationController
def new
end
def create
@user = User.find_by_email(params[:email])
if @user
@user.deliver_password_reset_instructions!
flash[:success] = "Instructions to reset your password have been emailed to you."
redirect_to root_path
else
flash[:warning] = "No user was found with that email address"
render :new
end
end
def edit
@user = User.find_by(perishable_token: params[:id])
end
def update
@user = User.find_by(perishable_token: params[:id])
if @user.update_attributes(password_reset_params)
flash[:success] = "Password successfully updated!"
redirect_to root_path
else
render :edit
end
end
private
def password_reset_params
params.require(:user).permit(:password, :password_confirmation)
end
end
new
is the action that will be called when a user clicks on the “Forgot your password?” link.
create
processes the sent data and tries to fetch a user by e-mail. If this user is found, password reset instructions are sent to their e-mail.
edit
is called when a user visits the link that was sent to the provided e-mail. This link contains our perishable token that is employed to find the user record. The edit page contains another form to enter a new password.
Inside the update
action, fetch the user and update their password.
We’ll need the routes, as well:
config/routes.rb
...
resources :password_resets, only: [:new, :create, :edit, :update]
...
Now, add a new method to your model:
models/user.rb
[...]
def deliver_password_reset_instructions!
reset_perishable_token!
PasswordResetMailer.reset_email(self).deliver_now
end
[...]
reset_perishable_token!
is a method supplied by AuthLogic – it simply sets perishable_token
to a new random value and saves the record.
We also have to create our new mailer:
mailers/application_mailer.rb
class ApplicationMailer < ActionMailer::Base
default from: "from@example.com"
layout 'mailer'
end
mailers/password_reset_mailer.rb
class PasswordResetMailer < ApplicationMailer
def reset_email(user)
@user = user
mail(to: @user.email, subject: 'Password reset instructions')
end
end
views/layouts/mailer.text.erb
<%= yield %>
views/password_reset_mailer/reset_email.text.erb
Here is your link to reset password: <%= edit_password_reset_url(@user.perishable_token) %>.
Also don’t forget to set the default URL options:
config/environments/development.rb
config.action_mailer.default_url_options = { host: '127.0.0.1:3000' }
Please note that in the development environment, e-mails won’t actually be sent, but you’ll be able to see their contents inside the console. Also note that my demo app won’t send e-mails, but it’s easy to setup for a production environment. Read more here.
Lastly, update the views to include the “Forgot your password?” link:
views/user_sessions/new.html.erb
<div class="page-header"><h1>Sign In</h1></div>
<%= form_for @user_session do |f| %>
[...]
<%= f.submit "Log in!", class: 'btn btn-primary btn-lg' %>
<br/><br/>
<%= link_to 'Forgot your password?', new_password_reset_path, class: 'btn btn-sm btn-default' %>
<% end %>
Now go ahead and check how this is working!
调整默认设置
正如我曾经说过的, AuthLogic提供了一些默认设置,但是你能够容易的改变它们,只有简单地给acts_as_authentic传递一个代码块。想要了解更多,你可以浏览
文档,我将在这里重点说明一些:
暴力破解Brute Force
默认情况下,如果你的user表中存在字段 failed_login_count
,AuthLogic 会试图防止暴力破解 (某些人使用程序和字典来猜测密码)。这里有两项设置:
consecutive_failed_logins_limit
(默认 50次) – 允许的连续登陆失败次数。设置为0,将会禁止防暴力破解保护。failed_login_ban_for
(默认值:2小时) ——用户账户将会被锁定多长时间。设置为0 ,账户将永久锁定。
了解更多?看 这里。
HTTP Basic Auth
AuthLogic 支持HTTP basic authentication,默认是打开的。
Password配置
有 不少设置 用来改变默认的字段来保存login 和password信息,同样用户会话查找方法也有不少设置。
同样地这篇 列出了密码验证和一致性验证相关的设置项。
登出和超时
你可以让AuthLogic 在用户登录一段时间后标记为登出了。只要设置logout_on_timeout
为 true
然后使用stale?
来检查用户是否需要重新登录。
点击 这里.了解更多信息。
你也可以通过设置logged_in_timeout
来确定用户是否登录了。 更多信息.
E-mail 配置
Visit this page to learn about various options related to e-mails (field to store e-mail, validation rules, and more).
There are many more options that you can use for AuthLogic customization, so be sure to browse the documentation.
总结
本文中,我们对AuthLogic稍作了解并且构建了一个简单的应用。学习观察不同的用户鉴定解决方案并进行比较是非常有趣的,你认为呢?
你首选哪一种用户鉴定解决方案,为什么? 你是否曾经遇到某些限制或者特殊情况导致你不得不开发你自己的用户鉴定系统?有的话,请共享一下你的经验!
欢迎回馈,像通常那样。感谢你浪费时间陪伴我到这里,再会。
作者信息就不译了:)
译者注:
1,如何在代码中自己验证密码?
if @user.valid_password? params[:current_password] @user.password = params[:password] @user.password_confirmation = params[:password_confirmation] if @user.changed? && @user.save UserSession.create(:login => @user.login, :password => params[:password]) redirect_to user_path(current_user) else render :action =>"modipass" end end