如何使用 https 保护 Rails API 并强制客户端连接
结论
HTTPS 只能防止数据在传输过程中被窃听或篡改,不能代替客户端身份认证。对于只读的 server-to-server Rails API,通常需要同时采取以下措施:
- 只开放 HTTPS,并使用有效的 TLS 证书。
- 为每个客户端签发独立的 API key,避免多人共用同一个 key。
- 通过
Authorization请求头传递凭证。 - 在服务端验证凭证,并设置权限控制、限流、审计、密钥轮换和撤销机制。
- 对安全要求更高的合作方,可以使用 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。
备注:内容仅供参考。
