Devise只允许每个用户在同一时间有一个会话

112 阅读3分钟

有时出于安全考虑,或者你不想让用户分享他们的账户,实现一个检查以确保每个用户在同一时间只允许一个登录(会话)是很有用的。

为了检查是否只有一次登录,我们将需要一个列来存储当前的登录信息。我们可以使用一个字符串列来存储一个令牌,这个令牌将在用户每次登录时随机生成,然后我们比较当前会话的登录令牌是否与这个令牌相等。

假设你的用户信息存储在用户表中(你可以在下面的命令中改成其他名称),为该表创建一个 "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,以跳过这个检查。

下面是这段代码的演示视频。

demo

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