如何用Rails 6从头开始设置用户认证

146 阅读5分钟

如何用Rails 6从头开始设置用户认证

用户认证是网络资源安全中的一个基本特征。在rails 程序中设置用户认证时,devise gem是一个常用的工具。然而,有时它可能过于庞大和复杂,难以定制,特别是在构建一个简单的应用程序时。

在本教程中,我们将在Rails 6中从头开始设置用户认证。

前提条件

要跟上本文的进度,具备以下条件很有帮助。

  • [红宝石]
  • [SQLite3]
  • [节点]
  • [纱线]
  • 配置了[Rails]框架。
  • 具有Ruby编程语言的基本知识。
  • 对Rails框架有良好的理解。
  • 对面向对象编程(OOP)范式的基本理解。
  • 安装了文本编辑器。最好是VS Code

项目设置

通过运行生成一个新的rails应用程序。

rails new auth -T

我们使用-T 参数来排除默认的测试框架。

导航到新创建的auth 文件夹。在app文件夹中,Rails ,维护控制器、模型和视图的文件。

对MVC的基本理解

模型视图控制器是一种设计模式,它划分了相关的编程逻辑,使其更容易推理。Rails遵循这种设计模式。

  • 创建一个路由。在config/routes.rb
  Rails.application.routes.draw do
    root 'welcome#index'
  end

在上面的片段中,我们指示Rails ,将根路由(/ )设置为WelcomeController 中的index 动作。因此,当用户访问根路由时,索引动作将被触发。

  • 我们将运行以下命令来创建控制器。
  rails generate controller Welcome index --skip-routes

我们添加了--skip-routes ,意味着我们已经定义了路由和跳转。

该生成器将创建文件,但最重要的文件是位于的控制器文件。app/controllers/welcome_controller.rb

  class WelcomeController < ApplicationController
    def index
    end
  end

这个生成器还在app/views/welcome/ 中创建了一个文件,名为index.html.erb 。默认情况下,Rails会渲染一个与控制器动作名称相对应的视图。

将视图内容替换为。

  <h1>Rails Authentication</h1>
  • 我们已经有了控制器和视图,让我们来创建模型。

model 是一个Ruby 类,作为数据库表的模板,用于保存数据。

我们可以用一个生成器来定义它,如下所示。

  rails generate model User email:string password_digest:string

这将生成几个文件,我们将只关注app/models/users.rbdb/migrate/<timestamp>_create_users.rb

  • 更新db/migrate/<timestamp>_create_users.rb 到。
  class CreateUsers < ActiveRecord::Migration[6.1]
    def change
      create_table :users do |t|
      t.string :email, null: false
      t.string :password_digest
      t.timestamps
    end
  end
end  

我们正在为我们的email 字段添加模型级别的验证,password_digest ,用于在rails中创建密码字段。密码的加密是使用bcyrpt_gem完成的。

  • 在你的Gemfile中包括bcyrpt
  # Gemfile
  gem 'bcrypt', '~> 3.1.7'
  • 通过运行bundle install 来安装它。

  • app/models/users.rb 更新为。

  class User < ApplicationRecord
     # adds virtual attributes for authentication
     has_secure_password
     # validates email
     validates :email, presence: true, uniqueness: true, format: { with: /\A[^@\s]+@[^@\s]+\z/, message: 'Invalid email' }
  end

我们将确保在用户被保存到数据库之前,电子邮件字段是存在的并且是唯一的。

电子邮件地址也应该符合一个模式,我们可以借助于给定的regular expression ,这是一个强大的字符串模式匹配语言。

  • 记住要运行你的迁移。迁移是Active Record ,它允许你随着时间的推移不断发展你的数据库模式。

在运行迁移之前,数据库中不会有表。之后会在数据库中创建表。

  rails db:migrate

让我们通过运行来测试我们的应用程序。

rails server

我们将看到一个带有主页的Rails应用程序。

localhost

让我们通过运行来测试我们的模型。

rails console

创建一个新的用户。

railsconsole

配置路由

更新config/routes.rb

我们正在用各自控制器的动作填写我们的routes.rb ,为我们的应用程序生成路径和URL。

Rails.application.routes.draw do
  root 'welcome#index'
  get 'sign_up', to: 'registrations#new'
  post 'sign_up', to: 'registrations#create'
  get 'sign_in', to: 'sessions#new'
  post 'sign_in', to: 'sessions#create', as: 'log_in'
  delete 'logout', to: 'sessions#destroy'
  get 'password', to: 'passwords#edit', as: 'edit_password'
  patch 'password', to: 'passwords#update'
  get 'password/reset', to: 'password_resets#new'
  post 'password/reset', to: 'password_resets#create'
  get 'password/reset/edit', to: 'password_resets#edit'
  patch 'password/reset/edit', to: 'password_resets#update'
end

当我们的应用程序收到一个传入的请求。

GET /sign_up

它将要求路由器将其与控制器的动作相匹配,如果匹配的路由是------。

get 'sign_up', to: 'registrations#new'

那么该请求将被派发到registrations 控制器的new 动作。

添加控制器

路由器决定使用哪个控制器来处理请求。然后,控制器将接收请求,并从我们的模型中保存或获取数据。

  • 让我们通过运行touch app/controllers/registrations_controller.rb 命令来创建registrations_controller.rb
  class RegistrationsController < ApplicationController
    # instantiates new user
    def new
      @user = User.new
    end
    def create
      @user = User.new(user_params)
      if @user.save
      # stores saved user id in a session
        session[:user_id] = @user.id
        redirect_to root_path, notice: 'Successfully created account'
      else
        render :new
      end
    end
    private
    def user_params
      # strong parameters
      params.require(:user).permit(:email, :password, :password_confirmation)
    end
  end

这个控制器负责创建一个新的用户并将其保存到数据库中。

new 动作在用户模型中初始化一个新的对象,并将其存储为一个实例变量,这可以在视图中被访问。

create 动作创建用户实例,将其id设置为session 。如果这个过程成功,它将重定向到我们的root path ,否则将渲染一个new 视图。

session 在一个请求中存储数据并在另一个请求中使用。

  • 让我们通过运行以下命令来创建sessions_controller.rb touch app/controllers/sessions_controller.rb
  class SessionsController < ApplicationController
    def new; end
    def create
      user = User.find_by(email: params[:email])
      # finds existing user, checks to see if user can be authenticated
      if user.present? && user.authenticate(params[:password])
      # sets up user.id sessions
        session[:user_id] = user.id
        redirect_to root_path, notice: 'Logged in successfully'
      else
        flash.now[:alert] = 'Invalid email or password'
        render :new
      end
    end
    def destroy
      # deletes user session
      session[:user_id] = nil
      redirect_to root_path, notice: 'Logged Out'
    end
  end

SessionsController 为现有的用户提供登录功能,也可以通过删除会话数据来注销一个用户。

create 动作在数据库中找到有相应电子邮件地址的用户。它使用逻辑运算符来检查一个用户是否存在并经过认证,如果这两个约束条件都是真的,则登录该用户。

destroy 动作将用户会话设置为nil,注销了用户。

  • 让我们通过运行以下命令来创建passwords_controller.rb touch app/controllers/passwords_controller.rb
  class PasswordsController < ApplicationController
    # allows only logged in users
    before_action :require_user_logged_in!
    def edit; end
    def update
      # update user password
      if Current.user.update(password_params)
        redirect_to root_path, notice: 'Password Updated'
      else
        render :edit
      end
    end
    private
    def password_params
      params.require(:user).permit(:password, :password_confirmation)
    end
  end

PassWordsController 允许 signed_in_user 更新密码。

before_action :require_user_logged_in! 是一个Railscallback ,这个定义在application_controller.rb ,它只允许登录的用户访问update 动作,更新用户的密码。

  • 将我们的app/controllers/application_controller.rb 更新为。
  class ApplicationController < ActionController::Base
    before_action :set_current_user
    def set_current_user
      # finds user with session data and stores it if present
      Current.user = User.find_by(id: session[:user_id]) if session[:user_id]
    end
    def require_user_logged_in!
      # allows only logged in user
      redirect_to sign_in_path, alert: 'You must be signed in' if Current.user.nil?
    end
  end
  • 注意这个类的继承层次,因为所有的控制器都继承自ApplicationControllerset_current_user 将在所有的控制器中被访问。

这个控制器通过session[:user_id] 找到signed_in_user,如果存在的话,将其存储为Current.user

  • Current.user可以在我们的视图中被访问。

  • 让我们通过运行touch app/models/current.rb 命令来创建current.rb 类,这将允许我们在视图中调用Current.user

  class Current < ActiveSupport::CurrentAttributes
    # makes Current.user accessible in view files.
    attribute :user
  end

配置视图

控制器使模型数据在视图中可用,这些数据可以显示给用户。

Rails为我们提供了由form_with 产生的表单生成器对象,其中包含生成表单元素的辅助方法。

  • 创建一个sign_up 表单。
  touch `app/views/registrations/new.html.erb`
  <h1>Sign Up</h1>
  <%= form_with model: @user, url: sign_up_path do |f| %>
    <p>
    <%= f.label 'email' %><br>
    <%= f.text_field :email %>
    </p>
    <p>
    <%= f.label 'password' %><br>
    <%= f.password_field :password %>
    </p>
    <p>
    <%= f.label 'password_confirmation' %><br>
    <%= f.password_field :password_confirmation %>
    </p>
    <p>
    <%= f.submit 'Sign Up' %>
    </p>
  <% end %>

sign_up 表单创建了一个 form_tag,它的范围是我们的User 模型,使我们可以用User 对象的属性来填充我们的字段。

  • 创建一个sign_in 表单。
  touch `app/views/sessions/new.html.erb
  <h1>Sign In</h1>
  <%= form_with url: sign_in_path do |f| %>
    <p>
    <%= f.label 'email:'%><br>
    <%= f.text_field :email, id: 'email' %>
    </p>
    <p>
    <%= f.label 'password:'%><br>
    <%= f.password_field :password, id: 'password' %>
    </p>
    <p>
    <%= f.submit 'Log In' %>
    </p>
  <% end %>
  • 创建一个password_edit 表单。
  touch `app/views/passwords/edit.html.erb`
  <h1>Edit Password</h1>
  <%= form_with model: Current.user, url: edit_password_path do |f| %>
    <p>
    <%= f.label 'password:'%><br>
    <%= f.password_field :password %>
    </p>
    <p>
    <%= f.label 'password_confirmation:'%><br>
    <%= f.password_field :password_confirmation %>
    </p>
    <p>
    <%= f.submit 'Update' %>
    </p>
  <% end %>
  • 打开app/views/layouts/application.html.erb ,将<body> 标签更新为。
   <body>
     <p class="notice"><%= notice %></p>
     <p class="alert"><%= alert %></p>
     <%= yield %>
   </body>

在布局的上下文中,<%= yield %> 标识一个部分,在这个部分中应该插入视图中的内容。

  • 打开app/views/welcome/index.html.erb 并添加。
  <% if Current.user %>
    Logged in as: <%= Current.user.email %><br>
    <= link_to 'Edit Password', edit_password_path %>
    <%= button_to 'Logout', logout_path, method: :delete %>
    <% else %>
    <%= link_to 'Sign Up', sign_up_path %>
    or
    <%= link_to 'Login', sign_in_path %>
  <% end %>

然后我们检查Current.user 是否存在,并提供一个edit_password_link 和一个sign_out_button 。如果没有,就会看到一个sign_up_linklogin_link

  • 刷新你的应用程序,并检查你是否可以创建一个新的帐户,sign_in和编辑密码。

sign_up

重置密码

我们已经有了我们的路由,现在我们可以更新我们的控制器和视图来重置密码。

  • touchapp/controllers/password_resets_controller.rb
  class PasswordResetsController < ApplicationController
    def new; end
    def edit
      # finds user with a valid token
      @user = User.find_signed!(params[:token], purpose: 'password_reset')
      rescue ActiveSupport::MessageVerifier::InvalidSignature
        redirect_to sign_in_path, alert: 'Your token has expired. Please try again.'
    end
    def update
      # updates user's password
      @user = User.find_signed!(params[:token], purpose: 'password_reset')
      if @user.update(password_params)
        redirect_to sign_in_path, notice: 'Your password was reset successfully. Please sign in'
        else
        render :edit
      end
    end
    private
    def password_params
      params.require(:user).permit(:password, :password_confirmation)
    end
  end

上面的控制器负责重设用户密码。

edit 动作找到具有有效令牌和目的的签名用户,密码只能用有效的令牌来更改,如果没有,就会引发ActiveSupport::MessageVerifier

update 动作用有效的令牌更新用户的密码,并重定向到sign_in_path 。在完成这个动作之前,我们必须configure our mailers ,让用户收到一封邮件并重置密码。在配置邮递员之前,让我们通过运行命令touch app/views/password_resets/edit.html.erb ,创建视图。

  <h1>Reset your password?</h1>
  <%= form_with model: @user, url: password_reset_edit_path(token: params[:token]) do |f| %>
    <p>
    <%= f.label 'password:' %><br />
    <%= f.password_field :password %>
    </p>
    <p>
    <%= f.label 'password_confirmation:' %><br />
    <%= f.password_field :password_confirmation %>
    </p>
    <p>
    <%= f.submit 'Reset Password' %>
    </p>
  <% end %>
  • 创建另一个文件app/views/password_resets/new.html.erb ,并添加以下内容。
  <h1>Forgot your password?</h1>
  <%= form_with url: password_reset_path do |f| %>
    <p>
    <%= f.label 'email:' %><br />
    <%= f.text_field :email %>
    </p>
    <p>
    <%= f.submit 'Reset Password' %>
    </p>
  <% end %>

设置邮件服务器

  • 我们使用下面的命令来生成我们的邮件。
  rails generate mailer Password reset

生成器会创建几个文件,但最重要的文件是app/mailers/password_mailer.rb 和视图文件。

  • app/mailers/password_mailers.rb 更新为这样。
  class PasswordMailer < ApplicationMailer
    def reset
      # assigns a token with a purpose and expiry time
      @token = params[:user].signed_id(purpose: 'password_reset', expires_in: 15.minutes)
      # sends email
      mail to: params[:user].email
    end
  end

PasswordMailer 负责设置passwordsresets_controller.rb 中使用的令牌。它定义了一个reset 动作,该动作创建一个令牌并向用户发送电子邮件。

我们的视图应该看起来像这样。

touch `app/views/password_mailer/reset.html.erb`
Hi <%= params[:user].email %>,<br>
Someone requested a password reset.
Click the link above if you recognise the activity, link expires in 15 minutes
<%= link_to 'Reset Password', password_reset_edit_url(token: @token) %>

而我们的app/views/password_mailer/reset.text.erb

Hi <%= params[:user].email %>
Someone requested a password reset.
Click the link above if you recognise the activity, link expires in 15 minutes
<%= password_reset_edit_url(token: @token) %>

在编辑动作上方的app/controllers/password_resets_controller.rb 中添加一个创建动作。

def create
    @user = User.find_by(email: params[:email])
    if @user.present?
      # send mail
      PasswordMailer.with(user: @user).reset.deliver_later
      # deliver_later is provided by ActiveJob
    end
    redirect_to root_path, notice: 'Please check your email to reset the password'
  end
  • 注意:在我们的app/controllers/password_reset_controller.rb ,我们已经调用了邮件类PasswordMailer.with(user: @user).reset.deliver_laterdeliver_laterAction_job的一部分,它使我们能够排队后台作业。

  • 在我们发送邮件之前还有一个设置,我们需要打开我们的app/config/environments/development.rb ,并在该块中添加config.action_mailer.default_url_options = { host: "localhost:3000" }

  • app/config/environments/development.rb ,让我们设置为使用Gmail。

在区块中添加以下内容。

  config.action_mailer.delivery_method = :smtp
    config.action_mailer.smtp_settings = {
      user_name:      ENV['email'],
      password:       ENV['password'],
      domain:         ENV['localhost:3000'],
      address:       'smtp.gmail.com',
      port:          '587',
      authentication: :plain,
      enable_starttls_auto: true
    }

改变emailpassword 以符合你的证书。

  • 现在创建一个欢迎邮件。
  rails generate mailer Welcome
  • 更新app/mailers/welcome_mailer.rb
  class WelcomeMailer < ApplicationMailer
    # sends a welcome email
    def welcome_email
      @user = params[:user]
      @url = 'http://localhost:3000/sign_in'
      mail(to: @user.email, subject: 'Welcome to my awesome tutorial')
    end
  end
  • WelcomeMailer 定义了一个welcome_email 动作,它负责向签到的用户发送欢迎邮件。

app/views/welcome_mailer/welcome_email.html.erb 中更新你的邮件视图。

  <!DOCTYPE html>
  <html>
    <head>
      <meta content='text/html; charset=UTF-8' http-equiv='Content-Type' />
    </head>
    <body>
      <h1>Welcome to example.com, <%= @user.email %></h1>
      <p>
        You have successfully signed up to example.com,
        your user email is: <%= @user.email %>.<br>
      </p>
      <p>
        To login to the site, just follow this link: <%= @url %>.
      </p>
      <p>Thanks for joining and have a great day!</p>
    </body>
  </html>
  • 和我们的app/views/welcome_mailer/welcome_email.text.erb 到。
  Welcome to example.com, <%= @user.email %>
  ===============================================
  You have successfully signed up to example.com,
  your user email is: <%= @user.email %>.
  To login to the site, just follow this link: <%= @url %>.
  Thanks for joining and have a great day!
  • 记得在app/views/sessions/new.html.erb 中添加一个重设密码的链接,更新文件以配合上述内容。
  <h1>Sign In</h1>
  <%= form_with url: sign_in_path do |f| %>
    <p>
    <%= f.label 'email:'%><br>
    <%= f.text_field :email, id: 'email' %>
    </p>
    <p>
    <%= f.label 'password:'%><br>
    <%= f.password_field :password, id: 'password' %><br>
    <%= link_to 'Forgot your password?', password_reset_path %>
    </p>
    <p>
    <%= f.submit 'Log In' %>
    </p>
  <% end %>

试着重设你的密码,你应该看到与此接近的东西。

pass_reset_mail

  • 为了发送欢迎邮件,我们要在app/controllers/registrations_controller.rb 中更新我们的create 动作为。
  def create
    @user = User.new(user_params)
    if @user.save
      WelcomeMailer.with(user: @user).welcome_email.deliver_now
      # deliver_now is provided by ActiveJob.
      session[:user_id] = @user.id
      redirect_to root_path, notice: 'Successfully created account'
    else
      render :new
    end
  end

欢迎邮件应该与此类似。

welcome_mailer

总结

在这篇文章中,我们通过下面的步骤实现了一个完整的Rails认证系统。

我们已经了解了MVC设计,并从头开始建立了一个认证系统,还设置了一个Action Mailer和ActiveJob来发送我们的邮件和Rails中的会话安全。