有时出于安全考虑,或者你不想让用户分享他们的账户,实现一个检查以确保每个用户在同一时间只允许一个登录(会话)是很有用的。
为了检查是否只有一次登录,我们将需要一个列来存储当前的登录信息。我们可以使用一个字符串列来存储一个令牌,这个令牌将在用户每次登录时随机生成,然后我们比较当前会话的登录令牌是否与这个令牌相等。
假设你的用户信息存储在用户表中(你可以在下面的命令中改成其他名称),为该表创建一个 "current_login_token "列:
rails g migration AddCurrentLoginTokenToUsers current_login_token:string
这将产生以下迁移文件:
class AddCurrentLoginTokenToUsers < ActiveRecord::Migration[6.1]
def change
add_column :users, :current_login_token, :string
end
end
现在运行rails db:migrate来运行这个迁移。
接下来,我们需要覆盖Devise中SessionsController的create方法,这样我们就可以在用户每次登录时为current_login_token列生成一个随机字符串。
如果你还没有创建一个自定义的SessionsController,你可以使用这个命令生成一个:
rails g devise:controllers users -c=sessions
(假设模型名称是user)
-c标志意味着要生成的控制器,因为我们在这里只需要生成SessionsController。(你可以在这里阅读更多关于devise generator的命令)
这将在app/controllers/users/sessions_controller.rb中生成一个控制器,这将覆盖Devise的默认SessionsController。
接下来,我们将覆盖app/controllers/users/sessions_controller.rb中的 "create"方法(当用户成功登录时将调用该方法):
class Users::SessionsController < Devise::SessionsController
skip_before_action :check_concurrent_session
# POST /resource/sign_in
def create
super
# after the user signs in successfully, set the current login token
set_login_token
end
private
def set_login_token
token = Devise.friendly_token
session[:login_token] = token
current_user.current_login_token = token
current_user.save
end
end
我们使用Devise.friendly_token生成一个随机字符串作为令牌,然后将其存入当前会话**(session[:login_token]),同时也保存到当前用户的current_login_token**列。
skip_before_action :check_concurrent_session将在后面解释,暂时先放在那里。
接下来,我们将编辑application_controller.rb文件(或者大部分需要授权的控制器都继承自哪个控制器):
class ApplicationController < ActionController::Base
# perform the check before each controller action are executed
before_action :check_concurrent_session
private
def check_concurrent_session
if already_logged_in?
# sign out the previously logged in user, only left the newly login user
sign_out_and_redirect(current_user)
end
end
def already_logged_in?
# if current user is logged in, but the user login token in session
# doesn't match the login token in database,
# which means that there is a new login for this user
current_user && !(session[:login_token] == current_user.current_login_token)
end
end
由于我们在application_controller.rb中写了before_action :check_concurrent_session,所有继承自此的控制器都会在执行控制器动作方法之前运行check_concurrent_session方法,这将检查是否有任何新的登录会话为同一用户发起。
我们希望当用户在登录部分,即session#create时,免除这个检查,因此我们在Users::SessionController类中加入了skip_before_action :check_concurrent_session,以跳过这个检查。
下面是这段代码的演示视频。

请注意,在第二次登录尝试后,之前登录的用户在点击一个链接(执行一个控制器动作)后被注销。