跳到內容 跳到搜尋

HTTP Digest 驗證

簡單 Digest 範例

require "openssl"
class PostsController < ApplicationController
  REALM = "SuperSecret"
  USERS = {"dhh" => "secret", #plain text password
           "dap" => OpenSSL::Digest::MD5.hexdigest(["dap",REALM,"secret"].join(":"))}  #ha1 digest password

  before_action :authenticate, except: [:index]

  def index
    render plain: "Everyone can see me!"
  end

  def edit
    render plain: "I'm only accessible if you know the password"
  end

  private
    def authenticate
      authenticate_or_request_with_http_digest(REALM) do |username|
        USERS[username]
      end
    end
end

附註

authenticate_or_request_with_http_digest 區塊必須傳回應使用者的密碼或 ha1 摘要雜湊,以便架構可以適當地雜湊以檢查使用者的憑證。傳回 nil 會導致驗證失敗。

儲存 ha1 雜湊 (MD5(使用者名稱:領域:密碼)) 比儲存純文字密碼更好。如果密碼檔案或資料庫遭到入侵,攻擊者將能夠使用 ha1 雜湊來驗證使用此 domain 的身分,但不會有使用者的密碼可在其他網站上嘗試使用。

在罕見情況下,網際網路伺服器或前端代理會在授權標頭到達您的應用程式之前移除它們。您可以透過記錄所有環境變數並從中查看 HTTP_AUTHORIZATION 來偵錯這個狀況。

命名空間
方法
A
D
E
H
N
O
S
V

執行個體公開方法

authenticate(request, realm, &password_procedure)

對於有效回應傳回 true,否則傳回 false。

# File actionpack/lib/action_controller/metal/http_authentication.rb, line 215
def authenticate(request, realm, &password_procedure)
  request.authorization && validate_digest_response(request, realm, &password_procedure)
end

authentication_header(controller, realm)

# File actionpack/lib/action_controller/metal/http_authentication.rb, line 274
def authentication_header(controller, realm)
  secret_key = secret_token(controller.request)
  nonce = self.nonce(secret_key)
  opaque = opaque(secret_key)
  controller.headers["WWW-Authenticate"] = %(Digest realm="#{realm}", qop="auth", algorithm=MD5, nonce="#{nonce}", opaque="#{opaque}")
end

authentication_request(controller, realm, message = nil)

# File actionpack/lib/action_controller/metal/http_authentication.rb, line 281
def authentication_request(controller, realm, message = nil)
  message ||= "HTTP Digest: Access denied.\n"
  authentication_header(controller, realm)
  controller.status = 401
  controller.response_body = message
end

decode_credentials(header)

# File actionpack/lib/action_controller/metal/http_authentication.rb, line 267
def decode_credentials(header)
  ActiveSupport::HashWithIndifferentAccess[header.to_s.gsub(/^Digest\s+/, "").split(",").map do |pair|
    key, value = pair.split("=", 2)
    [key.strip, value.to_s.gsub(/^"|"$/, "").delete("'")]
  end]
end

decode_credentials_header(request)

# File actionpack/lib/action_controller/metal/http_authentication.rb, line 263
def decode_credentials_header(request)
  decode_credentials(request.authorization)
end

encode_credentials(http_method, credentials, password, password_is_ha1)

# File actionpack/lib/action_controller/metal/http_authentication.rb, line 258
def encode_credentials(http_method, credentials, password, password_is_ha1)
  credentials[:response] = expected_response(http_method, credentials[:uri], credentials, password, password_is_ha1)
  "Digest " + credentials.sort_by { |x| x[0].to_s }.map { |v| "#{v[0]}='#{v[1]}'" }.join(", ")
end

expected_response(http_method, uri, credentials, password, password_is_ha1 = true)

傳回對使用已解碼的 credentials 以及預期的 passworduri 進行的 http_method 要求的預期回應。因為建議慣例是儲存 ha1 消化碼而非純文字密碼,所以選用參數 password_is_ha1 預設設定為 true

# File actionpack/lib/action_controller/metal/http_authentication.rb, line 248
def expected_response(http_method, uri, credentials, password, password_is_ha1 = true)
  ha1 = password_is_ha1 ? password : ha1(credentials, password)
  ha2 = OpenSSL::Digest::MD5.hexdigest([http_method.to_s.upcase, uri].join(":"))
  OpenSSL::Digest::MD5.hexdigest([ha1, credentials[:nonce], credentials[:nc], credentials[:cnonce], credentials[:qop], ha2].join(":"))
end

ha1(credentials, password)

# File actionpack/lib/action_controller/metal/http_authentication.rb, line 254
def ha1(credentials, password)
  OpenSSL::Digest::MD5.hexdigest([credentials[:username], credentials[:realm], password].join(":"))
end

nonce(secret_key, time = Time.now)

使用基於時間的 MD5 消化碼來產生僅能使用一次的值。

由伺服器指定,每次進行 401 回應時都應唯一產生的資料字串。建議這個字串是 Base64 或十六進位資料。特別是,由於字串是在標頭列中作為一個引號字串傳遞,因此不允許使用雙引號字元。

nonce 的內容取決於實作。實作的品質取決於良好的選擇。例如,nonce 可能建構為

time-stamp H(time-stamp ":" ETag ":" private-key)

的時間戳記的 Base64 編碼,其中時間戳記是伺服器產生的時間或其他不會重複的值,ETag 是與所要求實體相關的 HTTP ETag 標頭值,而私密金鑰是僅伺服器知道的資料。透過這個形式的 nonce,伺服器會在收到客戶端驗證標題後重新計算雜湊部分,並且如果它與該標題中的 nonce 不相符或時間戳記值不新近,即拒絕要求。透過這種方式,伺服器可以限制 nonce 有效的時間。包含 ETag 可防止對資源已更新版本的重播要求。(注意:在 nonce 中包含客戶端的 IP 位址似乎可讓伺服器有能力將 nonce 的重複使用限制給最初取得它的同一個客戶端。然而,這樣會破壞代理伺服器農場,其中來自單一使用者的要求通常會經過農場中的不同代理伺服器。此外,IP 位址變造也沒那麼難。)

實作可能會選擇不接受先前使用過的 nonce 或先前使用過的摘要,以防止重播攻擊。或者,實作可能會選擇對 POST、PUT 或 PATCH 要求使用一次性 nonce 或摘要,並對 GET 要求使用時間戳記。有關所涉問題的更多詳細資訊,請參閱此文件第 4 節。

用戶端無法理解隨機數。由TimeTime 與專案創建後產生的Rails會話密鑰進行雜湊組成。確保時間無法由用戶端修改。

# File actionpack/lib/action_controller/metal/http_authentication.rb, line 330
def nonce(secret_key, time = Time.now)
  t = time.to_i
  hashed = [t, secret_key]
  digest = OpenSSL::Digest::MD5.hexdigest(hashed.join(":"))
  ::Base64.strict_encode64("#{t}:#{digest}")
end

opaque(secret_key)

基於密鑰摘要的不透明

# File actionpack/lib/action_controller/metal/http_authentication.rb, line 348
def opaque(secret_key)
  OpenSSL::Digest::MD5.hexdigest(secret_key)
end

secret_token(request)

# File actionpack/lib/action_controller/metal/http_authentication.rb, line 288
def secret_token(request)
  key_generator  = request.key_generator
  http_auth_salt = request.http_auth_salt
  key_generator.generate_key(http_auth_salt)
end

validate_digest_response(request, realm, &password_procedure)

要在請求憑證回應值與預期值相符的情況下,才傳回 false。首先嘗試將密碼設為 ha1 摘要密碼。如果失敗,再嘗試將其設為純文字密碼。

# File actionpack/lib/action_controller/metal/http_authentication.rb, line 222
def validate_digest_response(request, realm, &password_procedure)
  secret_key  = secret_token(request)
  credentials = decode_credentials_header(request)
  valid_nonce = validate_nonce(secret_key, request, credentials[:nonce])

  if valid_nonce && realm == credentials[:realm] && opaque(secret_key) == credentials[:opaque]
    password = password_procedure.call(credentials[:username])
    return false unless password

    method = request.get_header("rack.methodoverride.original_method") || request.get_header("REQUEST_METHOD")
    uri    = credentials[:uri]

    [true, false].any? do |trailing_question_mark|
      [true, false].any? do |password_is_ha1|
        _uri = trailing_question_mark ? uri + "?" : uri
        expected = expected_response(method, _uri, credentials, password, password_is_ha1)
        expected == credentials[:response]
      end
    end
  end
end

validate_nonce(secret_key, request, value, seconds_to_timeout = 5 * 60)

可能會想要更短暫的逾時,這取決於請求是 PATCH、PUT 或 POST,以及用戶端是瀏覽器還是網路服務。如果實作過時指令,可能會更短暫。這將允許使用者使用新的隨機數,而不用使用者再次輸入使用者名稱和密碼。

# File actionpack/lib/action_controller/metal/http_authentication.rb, line 341
def validate_nonce(secret_key, request, value, seconds_to_timeout = 5 * 60)
  return false if value.nil?
  t = ::Base64.decode64(value).split(":").first.to_i
  nonce(secret_key, t) == value && (t - Time.now.to_i).abs <= seconds_to_timeout
end