專案

一般

個人檔案

動作

替代(自定義)身份驗證方法

簡介

此頁面說明如何讓 Redmine 對不同的資料庫驗證使用者。也許您正在執行(甚至編寫)一個已經儲存帳戶記錄的應用程式,並且您希望 Redmine 使用這些記錄。假設該應用程式不支援 OpenID,否則您將會設定它。

讓 Redmine 將身份驗證延遲到您的其他應用程式對使用者很有幫助——他們只需要記住一組密碼,並且帳戶不會不同步。如果他們已經在您的主要應用程式中註冊,則可以將 Redmine 設定為在他們第一次登入時自動將其新增到自己的資料表中(不儲存任何密碼資訊)。

Redmine 對替代身份驗證的支援

Redmine 對替代/自定義身份驗證有特定的支援,這使得實作它非常容易。
  • auth_sources 資料表
    • 您將在此處新增一條與您的自定義身份驗證相關的記錄。
  • AuthSource 類別
    • 您將建立此類別的自定義子類別,並實作 authenticate() 方法。
Redmine 的身份驗證過程大致如下(假設 LDAP 和 OpenID 已停用)
  1. 首先,嘗試使用 Redmine 的內部資料表 (users) 驗證 登入密碼
  2. 如果失敗,請嘗試 auth_sources 資料表中註冊的每個替代身份驗證來源,並在其中一個來源成功時停止。
  3. 如果失敗,則拒絕登入嘗試。

注意:Redmine 會記錄哪個來源成功驗證了特定使用者。下次該使用者嘗試登入時,將會首先嘗試使用該來源。管理員可以透過 管理 -> 使用者 -> {使用者} -> 身份驗證模式,逐一設定/覆寫使用者的設定。

實作替代身份驗證來源

本文假設替代身份驗證涉及查詢其他資料庫中的一個或多個資料表。但是,這種方法可以推廣到對幾乎任何其他事物進行身份驗證(一些秘密檔案、一些網路服務,或者您遇到的任何有趣的情況);您可能想要檢查 app/models/auth_source_ldap.rb 中的 Redmine 程式碼,以獲得非資料庫替代身份驗證的範例。

插入合理的 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    |                |
+-------------------+--------------+------+-----+---------+----------------+

這個 schema 相對穩定,儘管舊版的 Redmine 可能缺少最後 2 個欄位(例如 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」 此替代驗證來源的名稱。將顯示在管理介面頁面中。
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 驗證來源使用它們將 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.rb` 和 `auth_source_ldap.rb` 的命名方案。
  • 在這裡,我們將使用 `app/models/auth_source_my_custom_app.rb`

實作該類別,以便呼叫其 `authenticate()` 方法將連接到其他資料庫並使用那裡的資料表來檢查提供的登入憑證。您在上面 `auth_sources` 資料表記錄中的值可透過實例變數(例如 `self.host`、`self.base_dn`、`self.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_sources 中用於驗證使用者的 idNULL 表示「使用內部 Redmine 驗證」。管理員也可以透過我上面提到的 Authentication mode UI 小工具手動設定這個值。
  • 使用備用來源驗證的使用者將無法使用 Redmine 變更他們的密碼(核心程式碼的出色檢查),並且如果他們嘗試這樣做,將會看到錯誤訊息。

後續步驟

如果您只想使用您的備用驗證來源進行 Redmine 登入,請移除「註冊」按鈕。我們透過移除 lib/redmine.rb 中的 menu.push :register 行來做到這一點。並且我們透過 管理 -> 設定 -> 驗證 -> 忘記密碼 關閉了「忘記密碼」功能。

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 的使用者資料表中沒有姓氏,因此我們添加了一個 Html 標籤以在 Redmine 中顯示圖示。

請記得變更資料庫前綴(第 84 行)和姓氏自訂(第 98 行)。

錯誤

  • 在 1.0.1 中,如果您在登入時遇到類似「undefined method `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 個修訂