[译]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 &raquo;
    <% 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

这几个字段 emailcrypted_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 &raquo;
      <% 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,如何在代码中自己验证密码?

 

     user.valid_password?
2,如何修改密码?
     
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

 

 

 

posted @ 2020-03-08 19:48  柒零壹  阅读(571)  评论(0编辑  收藏  举报