PAYATHON 2026

如何使用 https 保护 Rails API 并强制客户端连接

技术老齐

结论

HTTPS 只能防止数据在传输过程中被窃听或篡改,不能代替客户端身份认证。对于只读的 server-to-server Rails API,通常需要同时采取以下措施:

  1. 只开放 HTTPS,并使用有效的 TLS 证书。
  2. 为每个客户端签发独立的 API key,避免多人共用同一个 key。
  3. 通过 Authorization 请求头传递凭证。
  4. 在服务端验证凭证,并设置权限控制、限流、审计、密钥轮换和撤销机制。
  5. 对安全要求更高的合作方,可以使用 mTLS。

你无法在客户端服务器上“强制它正确使用 HTTPS”。服务端能做的是拒绝明文 HTTP,只接受 TLS 连接。客户端则必须在证书验证失败时停止请求。

HTTPS 与 OAuth 解决的问题不同

HTTPS 保护的是传输通道,作用包括:

  • 加密请求和响应;
  • 验证客户端连接的是正确的 API 服务;
  • 检测传输中的数据是否被篡改。

API key、OAuth 2.0 和 mTLS 用于识别调用者。即使接口只提供读取操作,只要返回的数据有价值,就需要进行身份认证和授权。

如果调用方数量有限、权限模型简单,也不需要代表终端用户访问资源,独立 API key 通常比 OAuth 2.0 更容易实施。但这不意味着部署 HTTPS 后就可以省略客户端身份验证。

API key 是共享密钥或访问令牌,不宜称为 private key。真正的私钥不应该通过网络发送。

在 Rails 中要求 HTTPS

可以在生产环境中启用 Rails 的 SSL 强制配置:

# config/environments/production.rb
Rails.application.configure do
  config.force_ssl = true
end

config.force_ssl 的具体行为可能因 Rails 版本和应用配置而异,通常涉及 HTTP 重定向、HSTS 和安全 Cookie 设置。应查阅当前 Rails 版本的文档,并检查实际返回的响应头。

也可以在控制器层限制 SSL,但这种方式通常不如全局配置稳妥:

class ApplicationController < ActionController::API
  force_ssl
end

对于纯 API,尤其是携带凭证的请求,不应把 HTTP 到 HTTPS 的重定向作为主要安全措施。客户端发出 HTTP 请求时,API key 可能已经通过明文网络泄露。此时服务端再返回 301 或 308,已经无法挽回。

更可靠的做法包括:

  • 客户端从一开始就使用 https://api.example.com;
  • 在负载均衡器、反向代理或防火墙处关闭 HTTP 端口,或者只返回不包含业务内容的拒绝响应;
  • 阻止 HTTP 请求进入 Rails 的业务处理流程。

例如,可以在 Nginx 层拒绝明文 API 请求:

server {
    listen 80;
    server_name api.example.com;

    return 444;
}

server {
    listen 443 ssl;
    server_name api.example.com;

    ssl_certificate     /etc/ssl/certs/api.example.com.pem;
    ssl_certificate_key /etc/ssl/private/api.example.com.key;

    location / {
        proxy_pass http://rails_app;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Proto https;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

444 是 Nginx 的非标准行为。如果基础设施要求返回标准 HTTP 响应,可以改用 400 或 403。如果同一域名还承载浏览器页面,通常需要保留 HTTP 到 HTTPS 的重定向,不能直接关闭端口。

正确传递和验证 API key

不要把 API key 放在 URL 中:

https://api.example.com/data?api_key=secret

URL 可能被代理日志、访问日志、监控系统、浏览器历史或错误报告记录。应通过请求头传递 API key:

Authorization: Bearer client-specific-api-key

Rails 可以这样读取并验证凭证:

class ApplicationController < ActionController::API
  before_action :authenticate_client!

  private

  def authenticate_client!
    scheme, token = request.authorization.to_s.split(" ", 2)

    unless scheme == "Bearer" && valid_api_key?(token)
      head :unauthorized
    end
  end

  def valid_api_key?(token)
    return false if token.blank?

    digest = OpenSSL::Digest::SHA256.hexdigest(token)
    ApiClient.active.exists?(api_key_digest: digest)
  end
end

创建密钥时,应生成高强度随机值,只向客户端展示一次原始值,数据库中仅保存摘要:

raw_key = SecureRandom.hex(32)
digest = OpenSSL::Digest::SHA256.hexdigest(raw_key)

ApiClient.create!(
  name: "partner-a",
  api_key_digest: digest,
  active: true
)

puts raw_key

API key 通常具有较高的随机熵,因此使用 SHA-256 摘要保存是常见做法。不要用可预测的编号、短字符串或普通密码充当 API key。

每个合作方都应使用独立密钥,以便:

  • 单独撤销泄露的密钥;
  • 准确计费和审计;
  • 为不同客户设置权限和限额;
  • 在不影响其他客户的情况下轮换密钥。

比较敏感值时,应使用恒定时间比较,以降低时序攻击风险。如果通过数据库查询摘要,还要避免查询方式和错误响应泄露额外信息。另一种做法是先用非敏感的 key ID 定位客户端,再通过 ActiveSupport::SecurityUtils.secure_compare 比较摘要。

TLS 终止在反向代理时的注意事项

在生产环境中,HTTPS 经常终止于负载均衡器、CDN 或 Nginx,Rails 收到的内部请求可能是 HTTP。这种架构没有问题,但需要满足以下条件:

  • Rails 和代理之间位于可信的私有网络,或者也使用 TLS;
  • 只有可信代理可以写入 X-Forwarded-Proto 等转发头;
  • Rails 能正确识别原始请求使用的是 HTTPS;
  • 外部客户端无法绕过代理直接访问 Rails 端口。

否则,系统可能出现无限重定向、错误判断连接协议或转发头被伪造等问题。受信代理的具体配置取决于 Rails 版本和部署平台,不能依靠客户端自行发送 X-Forwarded-Proto: https。

客户端必须验证服务端证书

客户端不能只“启用 SSL”,还必须确认:

  • 证书由受信任的 CA 签发;
  • 证书域名与 API 域名一致;
  • 证书仍在有效期内;
  • 验证失败时立即终止连接。

Ruby 客户端示例:

require "net/http"
require "json"

uri = URI("https://api.example.com/v1/data")

request = Net::HTTP::Get.new(uri)
request["Authorization"] = "Bearer #{ENV.fetch("API_KEY")}"
request["Accept"] = "application/json"

response = Net::HTTP.start(
  uri.hostname,
  uri.port,
  use_ssl: true,
  verify_mode: OpenSSL::SSL::VERIFY_PEER
) do |http|
  http.request(request)
end

raise "API request failed: #{response.code}" unless response.is_a?(Net::HTTPSuccess)

data = JSON.parse(response.body)

不要使用 OpenSSL::SSL::VERIFY_NONE,也不要为了绕过证书错误而关闭校验。

服务端还应采取的措施

密钥管理

  • 使用环境变量或密钥管理服务保存服务端秘密;
  • 不在源码、配置仓库或日志中记录原始 API key;
  • 支持密钥过期、轮换和立即撤销;
  • 客户端发生泄露后能够迅速更换密钥;
  • 日志中只记录脱敏后的 key ID。

“客户按使用量付费”并不能消除密钥共享风险。密钥仍可能因代码仓库泄露、日志记录、服务器入侵或员工误操作而落入第三方手中。

限流与异常检测

请求通过认证后,仍要限制每个客户端的调用频率和数据量。可以使用反向代理、API Gateway 或 Rails 限流中间件进行控制。建议记录以下信息:

  • 客户端标识;
  • 请求时间;
  • 接口和响应状态;
  • 返回数据量;
  • 来源 IP;
  • 限流或认证失败事件。

不要记录完整的 Authorization 请求头。

最小权限

即使当前接口全部只读,也应限制每个 key 只能访问所需数据。通过身份认证不代表可以读取所有数据。在查询之前,还要检查客户端权限和允许访问的数据范围。

防止缓存泄露

如果响应中包含受保护的数据,应根据实际缓存策略设置响应头,例如:

response.set_header("Cache-Control", "private, no-store")

如果使用 CDN 或共享代理,还要确认经过认证的响应不会被错误缓存,并返回给其他客户端。

保持依赖和 TLS 配置更新

及时更新 Rails、Ruby、Web 服务器和 TLS 库,并禁用已知不安全的协议和密码套件。允许使用哪些 TLS 版本,应结合当前运行环境、合作方兼容性和组织安全基线确定。不要将某组密码套件永久写死后不再维护。

安全要求较高时使用 mTLS

如果需要确认只有获准的合作伙伴服务器能够调用 API,可以考虑双向 TLS,也就是 mTLS:

  • 服务端向客户端出示服务器证书;
  • 客户端也向服务端出示客户证书;
  • 服务端只接受由受信任 CA 签发的客户证书。

mTLS 可以将调用身份与合作方服务器绑定得更紧,但证书签发、更新、吊销和故障处理也更复杂。常见做法是在负载均衡器、API Gateway 或 Nginx 层验证客户证书,再将验证后的客户端身份传给 Rails。

对于简单的只读 API,可以先采用 TLS、独立 API key、权限控制、限流和审计。如果数据价值很高,或者合同、合规要求明确,再增加 mTLS、固定出口 IP 白名单或请求签名。IP 白名单只能作为附加防线,不能代替身份认证和 TLS。

备注:内容仅供参考。