用户登录前使用 Net::HTTP 请求 API
结论
不要把 Net::HTTP 请求直接写进 UserSession model。建议按下面的方式处理:
- 把第三方 API 调用封装在独立的服务对象中。
- 创建登录会话前调用该服务。
- 只有远端客户状态明确允许登录时,才创建 session。
- 分别处理超时、非成功 HTTP 状态、无效响应和 SSL 错误。
网络通信、响应解析和登录逻辑由此分开,测试和维护也会更方便。
封装客户状态查询
创建 app/services/cheddar_getter/customer_status.rb:
# app/services/cheddar_getter/customer_status.rb
require "net/http"
require "json"
module CheddarGetter
class CustomerStatus
Result = Struct.new(:state, :error, keyword_init: true) do
def active?
state == "active"
end
def pending?
state == "pending"
end
def cancelled?
state == "cancelled"
end
def request_failed?
!error.nil?
end
end
def initialize(customer_code:)
@customer_code = customer_code
end
def call
uri = URI("https://yoursite.chargevault.com/status")
uri.query = URI.encode_www_form(
key: api_key,
code: @customer_code
)
request = Net::HTTP::Get.new(uri)
request["Accept"] = "application/json"
response = Net::HTTP.start(
uri.host,
uri.port,
use_ssl: true,
open_timeout: 3,
read_timeout: 5
) do |http|
http.request(request)
end
return Result.new(error: "HTTP #{response.code}") unless response.is_a?(Net::HTTPSuccess)
parse_response(response.body)
rescue JSON::ParserError
Result.new(error: "Invalid JSON response")
rescue Net::OpenTimeout, Net::ReadTimeout
Result.new(error: "Request timed out")
rescue SocketError, OpenSSL::SSL::SSLError, IOError => e
Result.new(error: e.class.name)
end
private
def api_key
Rails.application.credentials.dig(:cheddar_getter, :api_key)
end
def parse_response(body)
data = JSON.parse(body)
# 根据接口实际返回的字段调整这里。
state = data.fetch("status").to_s.downcase
Result.new(state: state)
end
end
end
这段代码假设接口返回 JSON,例如:
{
"status": "active"
}
如果实际响应是 XML 或纯文本,或者状态字段名是 customerStatus,就需要修改 parse_response。确认接口的响应格式前,不要假设 active? 是 API 返回对象自带的方法。
如果响应只是纯文本,可以这样处理:
def parse_response(body)
Result.new(state: body.to_s.strip.downcase)
end
在创建登录会话前检查
以普通的 sessions controller 为例。先验证用户名和密码,再检查订阅状态,确认状态允许登录后才写入 session:
class SessionsController < ApplicationController
def create
user = User.find_by(email: params[:email])
unless user&.authenticate(params[:password])
return render :new, status: :unprocessable_entity,
alert: "邮箱或密码错误"
end
result = CheddarGetter::CustomerStatus.new(
customer_code: user.cheddar_getter_customer_code
).call
if result.active?
session[:user_id] = user.id
redirect_to dashboard_path
elsif result.pending?
redirect_to billing_path, alert: "订阅尚未生效,请检查付款状态"
elsif result.cancelled?
redirect_to billing_path, alert: "订阅已取消,请重新订阅"
elsif result.request_failed?
Rails.logger.error(
"Customer status check failed for user_id=#{user.id}: #{result.error}"
)
render :new,
status: :service_unavailable,
alert: "暂时无法验证订阅状态,请稍后重试"
else
redirect_to billing_path, alert: "当前订阅状态不允许登录"
end
end
end
使用 Authlogic 时,可以在自定义登录流程中先检查状态,再调用 UserSession.create。使用 Devise 时,通常可以通过自定义 SessionsController#create 或认证策略接入。无论使用哪种方案,远程请求都应该放在登录流程的边界,而不是藏在 UserSession 的持久化回调中。
为什么不建议放进 model
把 HTTP 请求写进 UserSession model,尤其是放在 before_create、validate 之类的回调中,会产生这些问题:
- 一次普通的 model 保存操作可能突然访问外部网络。
- API 超时会阻塞数据库相关流程。
- 验证可能执行多次,导致请求重复发送。
- 网络异常可能表现为含糊的 model 验证失败。
- 单元测试需要处理真实或模拟的外部请求。
model 可以保存 customer_code、本地订阅状态和最后同步时间,远程通信则更适合交给 service object。
SSL 验证不能关闭
原代码中的这一行有安全风险:
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
这行代码会关闭服务器证书验证,让 HTTPS 连接失去必要的身份校验。正常情况下应将其删除,使用 Ruby 默认的证书验证机制。如果遇到证书错误,应修复运行环境中的 CA 证书配置,不能直接关闭验证。
还需要注意的事项
不要把 API key 硬编码在源码中。可以将它保存在 Rails credentials 里:
cheddar_getter:
api_key: your-secret-key
读取方式如下:
Rails.application.credentials.dig(:cheddar_getter, :api_key)
还要明确 API 不可用时采用哪种登录策略。涉及付费权限时,通常会采用“验证失败则暂不登录”的 fail-closed 策略,但第三方服务一旦发生故障,所有用户的登录都可能受到影响。
另一种做法是把订阅状态同步到本地数据库,通过 webhook 或定时任务更新。登录时直接读取本地状态,这样外部服务的短暂故障就不会阻断认证。
如果必须实时查询,至少要设置较短的连接超时和读取超时。日志中也不要记录 API key、完整请求 URL 或客户敏感数据。
备注:内容仅供参考。
