Rails Authlogic身份验证方法

4
在Authlogic中,我是否可以在身份验证方法中添加条件?我知道通过使用find_by_login_method可以指定另一个要使用的方法,但是当我使用它时,我需要传递另一个参数,因为find_by_login_method方法只传递被视为“login_field”的参数。
我需要做的是检查身份验证模型的关联内容。以下是我想要使用的方法。
  # make sure that the user has access to the subdomain that they are 
  # attempting to login to, subdomains are company names
  def self.find_by_email_and_company(email, company)
    user = User.find_by_email(email)

    companies = []
    user.brands.each do |b|
      companies << b.company.id
    end

    user && companies.include?(company)
  end

但是这种方法失败了,因为只有一个参数被发送到find_by_email_and_company方法。

公司实际上是子域名,所以为了在这里获取它,我只能将其放置在表单中的隐藏字段中(这是我能想到的唯一方法来将其传递给模型)。

有没有一种方法可以覆盖某些方法..?

使用下面的答案,我得出了以下解决方案:

用户模型(User.rb)

  def self.find_by_email_within_company(email)
    # find the user
    user = self.find_by_email(email)

    # no need to continue if the email address is invalid
    return false if user.nil?

    # collect the subdomains the provided user has access to
    company_subdomains = user.brands.map(&:company).map(&:subdomain)

    # verify that the user has access to the current subdomain
    company_subdomains.include?(Thread.current[:current_subdomain]) && user
  end

应用程序控制器

  before_filter :set_subdomain

  private

    def set_subdomain
      # helper that retreives the current subdomain 
      get_company

      Thread.current[:current_subdomain] = @company.subdomain
    end

User Session Model (UserSession.rb)

find_by_login_method :find_by_email_within_company

我读了一些关于使用Thread.current和名称空间冲突的东西。这是一个对我有用的好方法,但在赏金过期之前,我很想听听其他建议,否则,+100给Jens Fahnenbruck :)


你需要交换 "user && companies.include?(company)" 为 "companies.include?(company) && user",这样 User#find_by_email_and_company 方法返回用户模型而不是 Array#include? 方法的 "true" 或 "false"。 - jigfox
1
你关于Thread.current可能存在的问题是正确的。因此,我更新了我的答案,提供了另一种替代方式。 - jigfox
很有趣,你链接的帖子就是我读过的那篇告诉我 Thread.current 不好的帖子,谢谢你的帮助 Jens! - Rabbott
另外,您可能需要更新您的解决方案,您的私有方法没有名称,只是“def”,以防其他人遇到这个问题并尝试使用您的代码。 - Rabbott
抱歉,现在已经修正了。 - jigfox
3个回答

2

Authlogic提供了处理基于子域的身份验证的API

class User < ActiveRecord::Base
  has_many :brands
  has_many :companies, :through => :brands
  acts_as_authentic
end

class Brand < ActiveRecord::Base
  belongs_to :user
  belongs_to :company
end

class Company < ActiveRecord::Base
  has_many :brands
  has_many :users, :through => :brands
  authenticates_many :user_sessions, :scope_cookies => true
end

会话控制器:

class UserSessionsController < ApplicationController      

  def create
    @company = Company.find(params[:user_session][:company])
    @user_session = @company.user_sessions.new(params[:user_session])
    if @user_session.save 
    else
    end    
  end
end

另一方面

以下是使用您当前方法解决问题的方法(我会使用第一种方法):

设置自定义数据 - 将其设置为用于创建UserSession对象的哈希键email。AuthLogic将将此值传递给find_by_login方法。在find_by_login方法中访问所需的值。

假设: 子域ID设置在表单中名为company的字段中。

class UserSessionsController < ApplicationController      

  def create
    attrs = params[:user_session].dup #make a copy
    attrs[:email] = params[:user_session] # set custom data to :email key

    @user_session = UserSession.new(attrs)
    if @user_session.save 
    else
    end    
  end
end

模型代码

查找给定电子邮件和子域名的用户的代码可以简化和优化如下:

class User < ActiveRecord::Base
  def find_by_email params={}
   # If invoked in the normal fashion then ..
   return User.first(:conditions => {:email => params}) unless params.is_a?(Hash)

   User.first(:joins => [:brands => :company}],
     :conditions => ["users.email = ? AND companies.id = ?", 
                      params[:email], params[:company]])
  end
end

编辑1

用户通过身份验证后,系统应该提供访问授权数据的权限。

如果您在同一张表中维护所有域的数据,则必须按子域和经过身份验证的用户来限定数据范围。假设您有一个具有company_iduser_id列的Post模型。当用户登录时,您希望显示用户在子域中的帖子。这是限定用户在子域中的数据的一种方法:

Posts.find_by_company_id_and_user_id(current_company, current_user)

Posts.for_company_and_user(current_company, current_user) # named scope

如果您不限定数据范围,系统可能存在潜在的安全漏洞。

将公司作为表单字段(我猜是隐藏的)是否安全,因为使用Firebug可以显示隐藏字段并修改其内容。因此,如果用户更改登录表单字段中的值,但一旦登录,他们会获得特定于子域的信息,这可能会导致问题,对吗? - Rabbott
即使用户更改子域,只有在该子域中拥有帐户的情况下才允许登录。身份验证后的调用应确保用户正在访问授权内容(这是必需的)。 - Harish Shetty
一旦登录,内容将被限定在子域中,如果没有进行授权检查,则无法在每次页面加载时检查内容。因此,我可以在表单中更改公司为我拥有访问权限的公司,这将允许我登录。然后一旦登录,我的帐户将不再在每个页面加载时进行检查以验证我是否有权访问内容。 - Rabbott
作用域内容。companya.appname.com允许那个公司有权限的用户登录,所有内容都限定在Company A范围内。 - Rabbott
1
糟糕,第一种方法在Rails 3中失效了。=( 我真的很喜欢第一种方法。 - Joshua Partogi
显示剩余3条评论

1
在您的lib文件夹中添加一个具有以下内容的文件:
class Class
  def thread_local_accessor name, options = {}
    m = Module.new
    m.module_eval do
      class_variable_set :"@@#{name}", Hash.new {|h,k| h[k] = options[:default] }
    end
    m.module_eval %{
      FINALIZER = lambda {|id| @@#{name}.delete id }

      def #{name}
        @@#{name}[Thread.current.object_id]
      end

      def #{name}=(val)
        ObjectSpace.define_finalizer Thread.current, FINALIZER  unless @@#{name}.has_key? Thread.current.object_id
        @@#{name}[Thread.current.object_id] = val
      end
    }

    class_eval do
      include m
      extend m
    end
  end
end

我在这里找到了链接

然后在控制器中添加如下代码:

class ApplicationController < ActionController
  before_filter :set_subdomain
  private
  def set_subdomain
    User.subdomain = request.subdomains[0]
  end
end

现在你可以在用户模型中执行以下操作(假设你的公司模型有一个名为 subdomain 的方法):

class User < ActiveRecord::Base
  thread_local_accessor :subdomain, :default => nil

  def self.find_by_email_within_company(email)
    self.find_by_email(email)
    company_subdomains = user.brands.map(&:company).map(&:subdomain)
    company_subdomains.include?(self.subdomain) && user
  end
end

顺便说一下:

companies = user.brands.map(&:company).map(&:subdomain)

等同于

companies = []
user.brands.each do |b|
  companies << b.company.subdomain
end

谢谢你分享.map($:)技巧,我将能够在很多地方使用它,你能解释一下吗?或者在这里放一个链接让我去了解一下..? - Rabbott
.map(&:method_name) 是 .map{|o| o.method_name } 的简写形式。我不确定这是 Ruby 还是 Rails 核心扩展的一部分。所以我不知道它是否适用于纯 Ruby。 - jigfox
太好了,谢谢! - 我会在赏金结束前接受您的解决方案,以便让其他人有机会回应,因为一旦我接受了,就无法更改。 - Rabbott
1
&符号表示“将其视为一个块”。由于符号不是一个块,因此它会尝试调用to_proc方法。在符号上调用to_proc将返回一个proc,该proc接受一个对象,并调用符号所代表的方法。http://pragdave.pragprog.com/pragdave/2005/11/symbolto_proc.html - Matt Briggs

0

在Rails 3中,您可以使用以下解决方法:

class UserSessionsController < ApplicationController
...
def create
    @company = <# YourMethodToGetIt #>
    session_hash = params[:user_session].dup
    session_hash[:username] = { :login => params[:user_session][:username], :company => @company }
    @user_session = UserSession.new(session_hash)

    if @user_session.save
        flash[:notice] = "Login successful!"
        redirect_back_or_default dashboard_url
    else
        @user_session.username = params[:user_session][:username]
        render :action => :new
    end

...
end

那么

class UserSession < Authlogic::Session::Base
    find_by_login_method :find_by_custom_login
end

并且

class User < ActiveRecord::Base
...
    def self.find_by_custom_login(hash)
        if hash.is_a? Hash
            return find_by_username_and_company_id(hash[:login], hash[:company].id) ||
              find_by_email_and_company_id(hash[:login], hash[:company].id)
        else
            raise Exception.new "Error. find_by_custom_login MUST be called with {:login => 'username', :company => <Company.object>}"
        end
    end
...
end

这很朴素和“正确”。我花了很多时间才找到,但它运行良好!


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接