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。
来源:显示 | 在 GitHub 上
# 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) 链接
来源:显示 | 在 GitHub 上
# 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) 链接
来源:显示 | 在 GitHub 上
# 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) 链接
来源:显示 | 在 GitHub 上
# 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) 链接
来源:显示 | 在 GitHub 上
# 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) 链接
来源:显示 | 在 GitHub 上
# 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) 链接
返回对使用解码后的 凭据
和预期 密码
的 uri
的 http_method
请求的预期响应。 可选参数 password_is_ha1
默认设置为 true
,因为最佳实践是存储 ha1 散列而不是明文密码。
来源:显示 | 在 GitHub 上
# 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) 链接
来源:显示 | 在 GitHub 上
# 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
与密钥的哈希值。 确保时间无法由客户端修改。
来源:显示 | 在 GitHub 上
# 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) 链接
基于密钥散列的不透明值
来源:显示 | 在 GitHub 上
# 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) 链接
来源:显示 | 在 GitHub 上
# 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 散列密码。 如果失败,则尝试将其作为明文密码。
来源:显示 | 在 GitHub 上
# 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 而不必再次提示用户输入用户名和密码。
来源:显示 | 在 GitHub 上
# 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