在 Rails Application 使用 Http Authentication

Aaron Kuo
Akatsuki Taiwan Technology
7 min readMar 29, 2019

by Aaron Kuo

當你的 web application 是非公開的服務、必須保護不被陌生的使用者存取的時候,會需要建立某種身份驗証的機制 — 當然你可以選擇直接掛個 devise 或設計你的使用者管理系統 — 但若你的服務不是那麼龐大,或不想花太多額外的成本來處理身份驗証,那麼 HTTP level 的身份驗証或許就是一個簡單又快速的選擇。

什麼是 HTTP Authentication

HTTP Authentication 是 HTTP 定義的一種簡單的驗証機制;啟用 HTTP Authentication 的 uri 會在瀏覽器訪問該位置時,若 request 中未帶有經過驗証的憑証時,server 會回傳 401 status 並附帶一個 challenge-response 的資訊,

瀏覽器接收到這樣的 response 時,會直接跳出原生的對話視窗,要求輸入使用者名稱及密碼,在用戶輸入正確的身份識別後,http server 會產生一組 credentials 回傳給 browser,而 browser 會在而後發送的 requests 之中重複利用這組 credentials 來通過檢查

常見的 HTTP Authentication Scheme

而 HTTP Authentication Framework 中定義多種不同的 Scheme,它提供了不同的處理方式和驗証強度,有些 Scheme 則可能被不同的 client 或軟體所支援、用在不同的情境,例如數位簽章或 Oauth 驗証。而瀏覽器上最常見的 Scheme 即為 Basic 與 Digest Authentication

Basic Authentication

如名就是最基本的處理方式,browser 會將使用者輸入的用戶名稱和密碼用「:」拼接後以 base64 編碼,放在 Header 中送出,HTTP server 收到後再 decode 後進行檢驗。

因為是用 base64 編碼所以相當不安全,在 HTTP 明文傳送下,只要被欄截下來取得 header 資料,再以 base64 decode,就能清楚知道原文。它的目的充其量只是為了轉換字元而非安全;因此建議只在 https 下以及私有環境中使用。

Digest Authentication

相較 Basic 就安全許多,Digest Authentication 是利用MD5 雜湊演算進行非對稱加密, 傳輸過程中 Header 放的是加密後的值,基本上是較難以破解的。加上 Digest Authentication 驗証時引入了 nonce 值,來阻擋用戶端利用重放攻擊的方式去猜測密碼的情況

如何啟用 HTTP Authentication

既然是 HTTP Protocol 的協議,那最直接的方式就是從 HTTP Server 進行配置啦,以 nginx 為例,在它的網站上都有清楚的說明

但如果你只是開發者,並不熟悉 Http Server 的管理或者是在無法更動 Http Server 配置的情況下,該怎麼辦呢?

如前所述,HTTP Authentication 的機制就是以 response 的 header 來運作,所以我們當然還是可以透過 Application 層來達成,更棒的是,ruby 的 Rack app 本身就已經有實作 Http Authentication 的 Middleware 供使用的

所以不管是 Rails 還是 Sinatra,想當然而我們可以簡單地將它加在 Middleware 中

以 Rails 為例,只要修改專案目錄中的 confige.ru:

USERS = {
'username' => 'password'
}
use(DigestAuth, {
realm: 'MyApp',
opaque: 'secret'
}) do |user|
USERS[user]
end
run Rails.application

(這裡僅以 Digest Auth 為例,Rack 同樣也提供有 Basic Auth,但它相對簡單又不安全,在這兒就不管它了)

  • realm: 是讓用戶端視別現在與在登入的對象,同時也是加密原文的一部分。
  • opaque: 是伺服器定義好的固定字串,客戶端在回覆時必須以原值回返,用於識別正確來源的請求。
  • 這裡的 USERS 常數中即定義好的用戶密碼對照,可自行選擇實作方式

如此就能為你的網站加上 digest authentication 了

加密

當然你會發現這樣子對 config.ru 的修改,不就會把密碼 commit 進程式原始碼了嗎?解決辦法就是設置 passwords_hashed 這個參數,告知 DigestAuth 回傳值是已加密過的,這樣定義的密碼表就可以改成 MD5 加密過後的資料,例如:

USERS = {
'username' => '42e47ee89fbd5a7fd1a1e0e36751da07'
}
use(DigestAuth, {
realm: 'MyApp',
opaque: 'secret',
passwords_hashed: true
}) do |user|
USERS[user]
end

根據 Digest Authentication 的驗証值是以 username:realm:password 組成
所以定義的加密值即需以此規則進行編碼,例如:

Digest::MD5.hexdigest('username:MyApp:12345678')

HTTP Server or Rack Application?

如果能在 HTTP Server 就進行配置,讓 Request 不用再進到 web application 層才做處理,當然會是比較高效的作法啦。

不過在 Application layer 來處理的話,則又多了一些 Programatic 的操作性,例如組織政策上除了要在安全機制加上 HTTP Authentication 外,可能還會希望它的密碼會是定期變換的,耶~這時我們就可以在實作上加入一些操作。

USERS = {
'username' => {
Date.new(2019, 1, 1) => '...',
Date.new(2019, 2, 1) => '...',
Date.new(2019, 3, 1) => '...',
Date.new(2019, 4, 1) => '...',
Date.new(2019, 5, 1) => '...',
}
use(DigestAuth, {
realm: 'MyApp',
opaque: 'secret',
passwords_hashed: true
}) do |user|
USERS[user][Time.zone.now.to_date.beginning_of_month]
end

嘿,這樣每個月就會變成採用不同的密碼啦

不過 Nginx (or 其他 http server)我想應該也能有辦法實現啦,有待 nginx 能手釋疑;但能用 ruby 完成它,應該是簡便又更容易維護吧(笑)

結語

HTTP Authentication 當然不是絕對安全的,但至少是一種提高破解難度的手段;又或可以輕便地在私有環境內再加上一層保護機制,畢竟若只是要殺雞就別用牛刀啦。

--

--