跳至内容 跳至搜索

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(username:realm:password) 比存储纯密码要好。如果密码文件或数据库被泄露,攻击者将能够使用 ha1 哈希在此 realm 下以用户身份进行身份验证,但无法获得用户的密码以尝试在其他站点使用。

在极少数情况下,Web 服务器或前端代理会在授权标头到达应用程序之前将其剥离。您可以通过记录所有环境变量来调试这种情况,并检查 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)

返回用于 http_method 请求到 uri 的预期响应,使用解码的 credentials 和预期的 password。可选参数 password_is_ha1 默认设置为 true,因为最佳实践是存储 ha1 摘要而不是纯文本密码。

# 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 可能由以下内容的 base 64 编码构成:

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

其中 time-stamp 是服务器生成的或任何非重复值,ETag 是所请求实体的 HTTP ETag 标头的值,private-key 是服务器才知道的数据。通过这种形式的 nonce,服务器将在收到客户端身份验证标头后重新计算哈希部分,如果哈希不匹配标头中的 nonce 或 time-stamp 值不够新,则拒绝请求。这样服务器就可以限制 nonce 的有效时间。包含 ETag 可以防止对资源的更新版本进行重放请求。(注意:在 nonce 中包含客户端的 IP 地址似乎可以使服务器将 nonce 的重用限制在最初获取它的客户端。但是,这会破坏代理集群,其中单个用户的请求经常会通过集群中的不同代理。而且,IP 地址欺骗并不难。)

为了防止重放攻击,实现可以选择不接受先前使用的 nonce 或先前使用的摘要。或者,实现可以选择为 POST、PUT 或 PATCH 请求使用一次性 nonce 或摘要,为 GET 请求使用时间戳。有关涉及的更多详细信息,请参阅本文档的第 4 节。

Nonce 对客户端是隐藏的。由 Time 和创建项目时生成的 Rails 会话密钥的 Time 哈希组成。确保时间不能被客户端修改。

# 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,以及客户端是浏览器还是 Web 服务。如果实现了 Stale 指令,可以大大缩短时间。这将允许用户使用新的 nonce 而无需再次提示用户输入用户名和密码。

# 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