【Rails】APIリクエストにIPアドレスの制約をつける方法

Ruby

API開発において特定のIPアドレスからのアクセスのみ許可したいというケースがあります。
今回はプライベートIPアドレスからのリクエストのみ許可する非公開APIの開発を例にとり、APIリクエストにIPアドレスの制約をつける方法について紹介します。

IPアドレス制約のついたAPI実装手順

許可するIPアドレスの範囲を定義する

まずは許可するIPアドレスの範囲を指定します。
今回は例としてローカルIP(192.168.16.1/16)からのリクエストのみ許可する設定にします。

IPアドレスの値の管理は任意の方法で問題ありません。例えばconfig_forを利用して環境変数で管理する場合は以下のような設定ファイルを作成します。

config/ip_address.yml

default: &default
  allow_api_access: 192.168.16.1/16

test:
  <<: *default

development:
  <<: *default

production:
  <<: *default

以下のように値が参照できればOKです。

$ rails c

> Rails.application.config_for(:ip_address)["allow_api_access"]
=> "192.168.16.1/16"

config_forの詳細解説はRailsの環境変数の作成方法3パターン(config_for/global/config)比較で紹介しています。

リクエスト元のIPアドレスを参照し、IPアドレスによってAPIの挙動を変える

リクエスト元のIPアドレスが許可されたIPアドレスに含まれるかチェックします。

IPアドレスの文字列はIPAddrクラスを利用することでIPアドレスとして扱えます。
リクエスト元のIPアドレスはリクエスト情報がセットされているrequestremote_ipで確認できます。

上記をふまえると、controllerは以下のようになります。
今回は「ローカルIPからのリクエストに対してはarticlesプロパティをJSON形式で返す、ローカルIP以外からのリクエストに対しては403エラーをメッセージ付きで返す」という仕様のarticles_controller.rbを例として作成しています。なお、ルーティングの追加・モデル作成・マイグレーション実行の手順は省略します。

app/controllers/articles_controller.rb

class ArticlesController < ApplicationController
  before_action :validate_ip_adress

  def index
    @articles = Article.all
    render json: { articles: @articles }
  end

  private

  def validate_ip_adress
    render json: { message: "Invalid Ip Address" }, status: :forbidden unless allow_ip_addresses.include?(request.remote_ip)
  end

  def allow_ip_addresses
    IPAddr.new(Rails.application.config_for(:ip_address)["allow_api_access"])
  end
end

参考: IPアドレスのチェックをモジュール化する方法

IPアドレスをチェックする実装をモジュールに切り出すことで、ロジックを共通利用できます。

app/controllers/concerns/ip_restrictable.rb

module IpRestrictable
  class IpAddressInvalidError < StandardError; end

  extend ActiveSupport::Concern

  included do
    before_action :validate_ip_adress
  end

  private

  def validate_ip_adress
    render json: { message: "Invalid Ip Address" }, status: :forbidden unless allow_ip_addresses.include?(request.remote_ip)
  end

  def allow_ip_addresses
    IPAddr.new(Rails.application.config_for(:ip_address)["allow_api_access"])
  end
end

以下のようにincludeを追加することで上記のconcernsをcontrollerで利用できます。

app/controllers/articles_controller.rb

class ArticlesController < ApplicationController

  include IpRestrictable

  def index
    @articles = Article.all
    render json: { articles: @articles }
  end
end

参考: カスタム例外を利用してAPIのレスポンスを制御する方法

例えばモジュールで作成したカスタム例外を利用してIPアドレスの制御をする実装は以下のようになります。

app/controllers/concerns/ip_restrictable.rb

module IpRestrictable
  class IpAddressInvalidError < StandardError; end

  extend ActiveSupport::Concern

  included do
    before_action :validate_ip_adress
  end

  private

  def validate_ip_adress
    raise IpRestrictable::IpAddressInvalidError unless allow_ip_addresses.include?(request.remote_ip)
  end

  def allow_ip_addresses
    IPAddr.new(Rails.application.config_for(:ip_address)["allow_api_access"])
  end
end

app/controllers/application_controller.rb

class ApplicationController < ActionController::API
  rescue_from IpRestrictable::IpAddressInvalidError, with: :render403

  private

  def render403(exception = nil)
    render json: { message: "Invalid Ip Address" }, status: :forbidden
  end
end

カスタム例外の詳細解説は【Ruby】カスタム例外の作成と利用に関する基礎知識で紹介しています。

IPアドレス制約に関するRSpecの実装方法

RSpecで実行されるAPIリクエストのリクエスト元IPアドレスはActionDispatch::Requestクラスのremote_ipメソッドをモック化することで変更できます。

今回のサンプルコードのRSpecは以下のようになります。なお、RSpecとFactoryBotの初期設定の手順は省略します。

spec/requests/articles_spec.rb

require 'rails_helper'

RSpec.describe "Articles", type: :request do
  let(:json)  { JSON.parse(response.body).deep_symbolize_keys }

  context '許可したIPからのリクエストの場合' do
    before do
      create(:article, title: 'サンプル')
      allow_any_instance_of(ActionDispatch::Request).to receive(:remote_ip).and_return("192.168.0.0")
    end

    it 'レスポンスを返すこと' do
      get articles_path
      expect(response).to have_http_status(:ok)
      expect(json[:articles][0][:title]).to eq('サンプル')
    end
  end

  context '許可していないIPからのリクエストの場合' do
    before do
      allow_any_instance_of(ActionDispatch::Request).to receive(:remote_ip).and_return("172.24.0.1")
    end

    it '403エラーを返すこと' do
      get articles_path
      expect(response).to have_http_status(:forbidden)
      expect(json[:message]).to eq('Invalid Ip Address')
    end
  end
end

Web APIのRSpec、つまりRequest Specの書き方の詳細解説はRSpecでAPIのレスポンスボディのプロパティを検証する方法で紹介しています。

さいごに

Twitter(@nishina555)やってます。フォローしてもらえるとうれしいです!