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アドレスはリクエスト情報がセットされているrequestのremote_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)やってます。フォローしてもらえるとうれしいです!