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) Link
成功时返回 true,否则返回 false。
authentication_header(controller, realm) Link
# 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) Link
# 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) Link
# 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) Link
encode_credentials(http_method, credentials, password, password_is_ha1) Link
# 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) Link
返回用于 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) Link
nonce(secret_key, time = Time.now) Link
使用基于时间的 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 哈希组成。确保时间不能被客户端修改。
opaque(secret_key) Link
基于密钥哈希的隐藏值
secret_token(request) Link
validate_digest_response(request, realm, &password_procedure) Link
当请求凭据响应值与预期值不匹配时返回 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) Link
可能需要较短的超时时间,具体取决于请求是 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