跳至内容 跳至搜索

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 散列以该 的身份进行身份验证,但不会有用户的密码,无法在其他网站上尝试使用。

在极少数情况下,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)

返回对使用解码后的 凭据 和预期 密码urihttp_method 请求的预期响应。 可选参数 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 不匹配,或者时间戳值不够新,则拒绝请求。 通过这种方式,服务器可以限制 nonce 的有效时间。 包含 ETag 可以防止对资源更新版本的重放请求。(注意:将客户端的 IP 地址包含在 nonce 中似乎可以让服务器限制 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