如何用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.rb 和db/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应用程序。

让我们通过运行来测试我们的模型。
rails console
创建一个新的用户。

配置路由
更新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.rbtouch 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.rbtouch 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
- 注意这个类的继承层次,因为所有的控制器都继承自
ApplicationController,set_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_link 和login_link 。
- 刷新你的应用程序,并检查你是否可以创建一个新的帐户,sign_in和编辑密码。

重置密码
我们已经有了我们的路由,现在我们可以更新我们的控制器和视图来重置密码。
- touch
app/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_later,deliver_later是Action_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
}
改变email 和password 以符合你的证书。
- 现在创建一个欢迎邮件。
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 %>
试着重设你的密码,你应该看到与此接近的东西。

- 为了发送欢迎邮件,我们要在
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
欢迎邮件应该与此类似。

总结
在这篇文章中,我们通过下面的步骤实现了一个完整的Rails认证系统。
我们已经了解了MVC设计,并从头开始建立了一个认证系统,还设置了一个Action Mailer和ActiveJob来发送我们的邮件和Rails中的会话安全。