项目

常规

个人资料

操作

替代(自定义)认证 HowTo

简介

此页面解释了如何让 Redmine 验证用户对不同的数据库。也许您正在运行(甚至编写)一个已经存储账户记录的应用程序,并希望 Redmine 使用这些记录。可能该应用程序不支持 OpenID,否则您会配置它。

让 Redmine 将认证委托给您的其他应用程序对用户很有帮助——他们只需要记住一组密码,账户就不会出现不一致的情况。如果他们在主要应用程序中注册,Redmine 可以配置为在它们首次登录时自动将其添加到其自己的表中(不存储任何密码信息)。

Redmine 对替代认证的支持

Redmine 对替代/自定义认证有特定的支持,这使得实现它非常简单。
  • auth_sources
    • 您将在这里添加一个特定于您自定义认证的记录。
  • AuthSource
    • 您将创建自己的子类,并实现 authenticate() 方法。
Redmine 的认证过程大致如下(假设禁用了 LDAP & OpenID)
  1. 首先,尝试使用 login & password 对 Redmine 内部表(users)进行认证。
  2. 如果失败,尝试 auth_sources 表中注册的每个替代认证源,直到其中一个成功为止。
  3. 如果都失败,拒绝登录尝试。

注意:Redmine 会记录哪个源成功认证了特定用户。下次该用户尝试登录时,将首先尝试该源。管理员可以通过 管理 -> 用户 -> {用户} -> 认证模式 手动设置/覆盖此设置。

实现替代认证源

本文假定替代认证涉及查询另一个数据库中的表(或多个表)。但是,此方法可以推广到几乎任何其他内容(某些秘密文件、某些网络服务、您有任何有趣的场景);您可能需要查看 Redmine 代码中的 app/models/auth_source_ldap.rb,以获取非数据库替代认证的示例。

插入合理的 auth_sources 记录

首先,我们应该决定我们的 auth_sources 记录将是什么样子。Redmine 核心和我们的 AuthSource 子类代码将使用这些信息,因此最好提前弄清楚。

仅供参考,以下是在 Redmine 2.4.1 中 auth_sources 表的内容

+-------------------+--------------+------+-----+---------+----------------+
| Field             | Type         | Null | Key | Default | Extra          |
+-------------------+--------------+------+-----+---------+----------------+
| id                | int(11)      | NO   | PRI | NULL    | auto_increment |
| type              | varchar(30)  | NO   |     |         |                |
| name              | varchar(60)  | NO   |     |         |                |
| host              | varchar(60)  | YES  |     | NULL    |                |
| port              | int(11)      | YES  |     | NULL    |                |
| account           | varchar(255) | YES  |     | NULL    |                |
| account_password  | varchar(255) | YES  |     |         |                |
| base_dn           | varchar(255) | YES  |     | NULL    |                |
| attr_login        | varchar(30)  | YES  |     | NULL    |                |
| attr_firstname    | varchar(30)  | YES  |     | NULL    |                |
| attr_lastname     | varchar(30)  | YES  |     | NULL    |                |
| attr_mail         | varchar(30)  | YES  |     | NULL    |                |
| onthefly_register | tinyint(1)   | NO   |     | 0       |                |
| tls               | tinyint(1)   | NO   |     | 0       |                |
| filter            | varchar(255) | YES  |     | NULL    |                |
| timeout           | int(11)      | YES  |     | NULL    |                |
+-------------------+--------------+------+-----+---------+----------------+

此模式相对稳定,尽管较旧的Redmine版本可能缺少最后两列(例如,Redmine 0.9没有这两列)。

我们的AuthSource子类代码如何使用这些字段基本上取决于我们,但有两个来自Redmine的关键限制
  1. type必须是你的AuthSource子类的名称
    • 当尝试使用你的自定义源进行身份验证时,Redmine将使用此字段来实例化你的类并调用其authenticate()方法
    • 此类名应以AuthSource开头。
    • 我们将放置 "AuthSourceMyCustomApp"
  2. onthefly_register有1或0
    • Redmine将使用此字段来决定是否可以在Redmine中使用此身份验证源注册未知用户(Redmine尚未了解的登录)。否则,如果您在这里输入“0”,则管理员必须首先手动注册用户(并可能设置其身份验证模式)--Redmine不会自动将其添加。

以下是我们将如何使用这些字段(替换为你的值)

字段 我们的值 注释
id NULL 让数据库引擎提供id。
type "AuthSourceMyCustomApp" 你的AuthSource子类的名称
name "MyCustomApp" 此替代身份验证源的名称。将在管理UI页面中显示。
host "myApp.other.host.edu" 其他数据库所在的主机名。本文不假设它与你的Redmine数据库所在的主机相同。
port 3306 其他主机上的数据库端口。
account "myDbUser" 访问其他数据库的账户名称。
account_password "myDbPass" 访问其他数据库的账户密码。
base_dn "mysql:myApp" 这个字段听起来非常像LDAP。抱歉。我们将将其解释为“基本数据库名称数据”并存储在类似“{dbAdapterName}:{dbName}”的字符串中。
attr_login "name" 你的其他数据库表中的哪个字段包含登录信息?
attr_firstname "firstName" 你的其他数据库表中的哪个字段包含用户的首名?
attr_lastname "lastName" 你的其他数据库表中的哪个字段包含用户的姓氏?
attr_mail "email" 你的其他数据库表中的哪个字段包含用户的电子邮件?
onthefly_register 1 是的,如果此源验证了用户,那么Redmine应该为他们创建一个内部记录(没有密码信息)。
tls 0 不知道。0表示“否”。
filter NULL 不知道。NULL是默认值。
timeout NULL 不知道。等待替代验证器时的超时?NULL是默认值。

注意:attr_*字段不一定总是需要的。它们用于将LDAP属性映射到Redmine属性。然而,我建议使用它们,因为它们使authentication()代码更通用(你需要在特定情况下使用代码时进行的更改更少)。

因此,我们将使用如下SQL语句将记录插入我们的Redmine(v2.4.1)的auth_sources表中

INSERT INTO auth_sources VALUES
  (NULL, 'AuthSourceMyCustomApp', 'MyCustomApp', 'myApp.other.host.edu', 3306,
  'myDbUser', 'myDbPass', 'mysql:myApp', 'name', 'firstName', 'lastName', 'email',
  1, 0, null, null)

实现你的AuthSource子类

app/models/中为你的AuthSource子类创建一个新文件,遵循现有auth_source.rbauth_source_ldap.rb的命名方案。
  • 这里我们将使用app/models/auth_source_my_custom_app.rb

实现类,以便调用其authenticate()方法将联系其他数据库并使用该表来检查提供的登录凭证。上面的auth_sources表记录中的值可通过实例变量获取(例如,self.hostself.base_dnself.attr_firstname)。

以下是我们的注释类

# Redmine MyApp Authentication Source
#
# Copyright (C) 2010 Andrew R Jackson
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

# Let's have a new class for our ActiveRecord-based connection
# to our alternative authentication database. Remember that we're
# not assuming that the alternative authentication database is on
# the same host (and/or port) as Redmine's database. So its current
# database connection may be of no use to us. ActiveRecord uses class
# variables to store state (yay) like current connections and such; thus,
# dedicated class...
class MyAppCustomDB_ActiveRecord < ActiveRecord::Base
  PAUSE_RETRIES = 5
  MAX_RETRIES = 50
end

# Subclass AuthSource
class AuthSourceMyCustomApp < AuthSource

  # authentication() implementation
  # - Redmine will call this method, passing the login and password entered
  #   on the Sign In form.
  #
  # +login+ : what user entered for their login
  # +password+ : what user entered for their password
  def authenticate(login, password)
    retVal = nil
    unless(login.blank? or password.blank?)
      # Get a connection to the authenticating database.
      # - Don't use ActiveRecord::Base when using establish_connection() to get at
      #   your alternative database (leave Redmine's current connection alone).
      #   Use class you prepped above.
      # - Recall that the values stored in the fields of your auth_sources
      #   record are available as self.fieldName

      # First, get the DB Adapter name and database to use for connecting:
      adapter, dbName = self.base_dn.split(':')

      # Second, try to get a connection, safely dealing with the MySQL<->ActiveRecord
      # failed connection bug that can still arise to this day (regardless of 
      # reconnect, oddly).
      retryCount = 0
      begin
        connPool = MyAppCustomDB_ActiveRecord.establish_connection(
          :adapter  => adapter,
          :host     => self.host,
          :port     => self.port,
          :username => self.account,
          :password => self.account_password,
          :database => dbName,
          :reconnect => true
        )
        db = connPool.checkout()
      rescue => err # for me, always due to dead connection; must retry bunch-o-times to get a good one if this happens
        if(retryCount < MyAppCustomDB_ActiveRecord::MAX_RETRIES)
          sleep(1) if(retryCount < MyAppCustomDB_ActiveRecord::PAUSE_RETRIES)
          retryCount += 1
          connPool.disconnect!
          retry # start again at begin
        else # too many retries, serious, reraise error and let it fall through as it normally would in Rails.
          raise
        end
      end

      # Third, query the alternative authentication database for needed info. SQL
      # sufficient, obvious, and doesn't require other setup/LoC. Even more the
      # case if we have our database engine compute our digests (here, the whole
      # username is a salt). SQL also nice if your alt auth database doesn't have
      # AR classes and is not part of a Rails app, etc.
      resultRow = db.select_one(
        "SELECT #{self.attr_login}, #{self.attr_firstname}, #{self.attr_lastname}, #{self.attr_mail} " +
        "FROM genboreeuser " +
        "WHERE SHA1(CONCAT(#{self.attr_login}, password)) = SHA1(CONCAT('#{db.quote_string(login)}', '#{db.quote_string(password)}'))" 
      )

      unless(resultRow.nil? or resultRow.empty?)
        user = resultRow[self.attr_login]
        unless(user.nil? or user.empty?)
          # Found a record whose login & password digest matches that computed
          # from Sign Inform parameters. If allowing Redmine to automatically
          # register such accounts in its internal table, return account
          # information to Redmine based on record found.
          retVal =
          {
            :firstname => resultRow[self.attr_firstname],
            :lastname => resultRow[self.attr_lastname],
            :mail => resultRow[self.attr_mail],
            :auth_source_id => self.id
          } if(onthefly_register?)
        end
      end
    end
    # Check connection back into pool.
    connPool.checkin(db)
    return retVal
  end

  def auth_method_name
    "MyCustomApp" 
  end
end


部署 & 测试

将你的新类保存在app/model/中并重新启动Redmine。

  • 您应该能够尝试使用存在于您的备用数据库中但不存在于您的Redmine实例中的用户登录。
  • 现有的Redmine账户(和密码)应继续有效。
  • 如果您检查Redmine的users表,您应该会在每次使用备用数据库成功登录后看到记录出现。
    • 这些记录的hashed_password将为空。
    • auth_source_id将包含用于验证用户的auth_sourcesidNULL表示“使用内部Redmine认证”。管理员还可以通过我上面提到的Authentication mode UI小部件手动设置此值。
  • 使用备用源进行身份验证的用户将无法使用Redmine更改他们的密码(核心代码的一个很好的检查),如果他们尝试这样做,将看到错误消息。

后续

如果您只想为Redmine登录使用备用身份验证源,请删除“注册”按钮。我们通过从lib/redmine.rb中删除menu.push :register行来实现这一点。我们还通过Administration -> Settings -> Authentication -> Lost password关闭了“忘记密码”功能。

This was all pretty fast and simple to set up, thanks to how Redmine is organized and that some thought about permitting this kind of thing had been made. Quality. I hope I didn't get anything too wrong.

Joomla

我们已定制代码以将Redmine与Joomla集成。请检查附件以查看Joomla 2.5和Redmine 2.0.3的实现。

由于Joomla的用户表中没有姓氏,我们在Redmine中添加了Html标签以显示图标。

请记住更改数据库前缀(第84行)和姓氏定制(第98行)。

错误

  • 在1.0.1中,如果您在登录时遇到类似“在登录时找不到方法`stringify_keys!' for #<Array:...>”的错误,请查看#6196
    # Lines 97 from previous script
              retVal =
              {
                :firstname => resultRow[self.attr_firstname],
                :lastname => resultRow[self.attr_lastname],
                :mail => resultRow[self.attr_mail],
                :auth_source_id => self.id
              } if(onthefly_register?)
    # ...
    
  • 在2.0.3中,对于#6196的错误代码修复仍然需要,我们已经修复了原始代码。

Jean-Philippe Lang更新,大约10年前 · 11次修订