PAYATHON 2026

用户登录前使用 Net::HTTP 请求 API

数据阿明

结论

不要把 Net::HTTP 请求直接写进 UserSession model。建议按下面的方式处理:

  1. 把第三方 API 调用封装在独立的服务对象中。
  2. 创建登录会话前调用该服务。
  3. 只有远端客户状态明确允许登录时,才创建 session。
  4. 分别处理超时、非成功 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 或客户敏感数据。

备注:内容仅供参考。