GraphQL・REST API の堅牢な認可設計:RBAC・ABAC・OAuth 2.0 のベストプラクティス

2024/05/13に公開

はじめに

現代のWebアプリケーションにおいて、APIのセキュリティは非常に重要な課題です。特に、適切な認可(Authorization)の仕組みを導入しないと、意図しないデータ漏洩や不正アクセスが発生するリスクがあります。

本記事では、Role-Based Access Control (RBAC) や Attribute-Based Access Control (ABAC) の適用、GraphQL・REST API における認可設計、Rate Limiting の導入、API Gateway を活用したアクセス制御、監視のベストプラクティス など、API の安全性を高めるための手法を詳しく解説します。セキュリティ強化のための実践的なアプローチを学び、より堅牢な API 設計を目指しましょう。

本記事で紹介するAPI のアクセス制御強化手法

  • Role-Based Access Control (RBAC) の導入(管理者・一般ユーザーの区分)
  • Attribute-Based Access Control (ABAC) の適用
  • GraphQL の リゾルバレベルでの認可チェック の実装(graphql-shield)
  • REST API のスコープ制限(OAuth 2.0 の適用)
  • API Gateway の導入(AWS API Gateway / Kong)
  • ユーザーごとのデータアクセス制限(Multi-Tenancy 設計)
  • GraphQL の introspection を制限(本番環境では無効化)
  • 過剰なデータ取得を防ぐための Rate Limiting の導入
  • OpenAPI / GraphQL スキーマの適切なバージョン管理
  • API ログの監視(Datadog / Sentry を利用)

RBAC(ロールベースアクセス制御)の導入

RBAC(Role-Based Access Control)は、システムのアクセス制御をユーザーの「ロール(役割)」に基づいて管理する仕組みです。
これにより、以下のようなアクセス制御が可能になります。

  • 管理者のみが設定変更できる
  • 一般ユーザーは閲覧のみ可能
  • 特定のユーザーグループだけが特定の機能を実行できる

RBACは、セキュリティを向上させるだけでなく、管理コストを削減し、一貫性のあるアクセス制御を実現できます。

RBAC の基本的な概念

RBACは主に以下の3つの要素で構成されます。

  • ユーザー(User): システムを利用する個人(例: user1, admin1)
  • ロール(Role): ユーザーが持つ役割(例: admin, user, editor)
  • 権限(Permission): ロールごとに定義された操作可能なアクション(例: 記事の閲覧, ユーザー管理, 設定変更)

これらを組み合わせることで、柔軟なアクセス制御を行えます。

実装例

RBACをデータベースに適用する方法の一例として、以下のようなデータモデルを考えます。

データモデル(例: PostgreSQL)

CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    username VARCHAR(255) UNIQUE NOT NULL,
    role_id INTEGER NOT NULL REFERENCES roles(id)
);

CREATE TABLE roles (
    id SERIAL PRIMARY KEY,
    name VARCHAR(50) UNIQUE NOT NULL
);

CREATE TABLE permissions (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100) UNIQUE NOT NULL
);

CREATE TABLE role_permissions (
    role_id INT REFERENCES roles(id) ON DELETE CASCADE,
    permission_id INT REFERENCES permissions(id) ON DELETE CASCADE,
    PRIMARY KEY (role_id, permission_id)
);

CREATE TABLE user_roles (
    user_id INT REFERENCES users(id) ON DELETE CASCADE,
    role_id INT REFERENCES roles(id) ON DELETE CASCADE,
    PRIMARY KEY (user_id, role_id)
);

このデータモデルのポイント

  • users : role_id を設定し、どのロールに属するかを管理
  • roles : admin や user などの役割を定義
  • permissions : 記事の編集 などの具体的な権限を定義
  • role_permissions : どのロールがどの権限を持つかを管理
  • user_roles : ユーザーがどのロールを持つかを管理

RBAC の実装(Express + Middleware)

Node.js(Express)を使用して、RBACをミドルウェアとして実装する例を紹介します。

  1. 認証(JWT ベースのユーザー情報)
    RBAC を適用する前に、JWT を使ってユーザー情報を取得する必要があります。
const jwt = require('jsonwebtoken');

const authenticateUser = (req, res, next) => {
    const token = req.headers.authorization?.split(' ')[1];
    if (!token) {
        return res.status(401).json({ message: 'Unauthorized' });
    }

    try {
        const decoded = jwt.verify(token, process.env.JWT_SECRET);
        req.user = decoded;
        next();
    } catch (error) {
        res.status(403).json({ message: 'Invalid token' });
    }
};
  1. RBAC ミドルウェア
    特定のロールを持つユーザーのみアクセスできるようにする関数を定義します。
const authorizeRole = (requiredRole) => {
    return (req, res, next) => {
        if (!req.user || req.user.role !== requiredRole) {
            return res.status(403).json({ message: 'Forbidden' });
        }
        next();
    };
};
  1. ルートの制限
    例えば、管理者専用の API エンドポイントを作成する場合は、以下のように RBAC ミドルウェアを適用できます。
app.get('/admin', authenticateUser, authorizeRole('admin'), (req, res) => {
  res.json({ message: '管理者専用ページ' });
});

一般ユーザーがアクセスできるエンドポイントを追加する場合

app.get('/user', authenticateUser, authorizeRole('user'), (req, res) => {
  res.json({ message: '一般ユーザー専用ページ' });
});

ABAC(属性ベースアクセス制御)の適用

ABAC は、ユーザーの属性(例えば部署・役職・グループなど)を基に、動的にアクセス制御を行う方式です。RBAC(Role-Based Access Control、ロールベースアクセス制御)が「役割(ロール)」を基準にしているのに対し、ABAC はより柔軟なルール設定が可能です。

ABAC の基本的な構成

ABAC のアクセス制御では、次の4つの要素を考慮します。

要素 説明
ユーザー属性 ユーザーに関連する情報(役職、部署、グループ、年齢など)
リソース属性 対象データの種類や機密レベル(例:公開情報、機密情報)
アクション 許可する操作(例:読み取り、書き込み、削除)
環境コンテキスト アクセスの条件(例:IP アドレス、時間帯、デバイス)

ABAC のメリット

ABAC は、RBAC よりも 柔軟なアクセス制御 を実現できます。

  • ✅ 動的なアクセス制御:
    • ユーザーの役職、部門、時間帯などの属性を考慮し、動的にアクセス可否を判定できる。
    • 例えば「営業時間中のみアクセス可」「管理者権限があり、かつ VPN 経由のみ許可」といった制御が可能。
  • ✅ スケーラブルな管理:
    • RBAC の場合、新しい役職や部門が増えるたびにロールを増やす必要があるが、ABAC ならルールの変更で対応可能。
  • ✅ 細かな制御が可能:
    • ユーザーの属性 × リソースの属性 × アクション × 環境コンテキストを組み合わせてアクセス制御できる。

ABAC のルール設定

ABAC では、JSON 形式でポリシーを定義することが一般的です。
例えば、「エンジニア部門のユーザーが、レポートを読むことを許可する」ルールは以下のように表現できます。

{
    "rules": [
        {
            "attribute": "department",
            "value": "engineering",
            "action": "read",
            "resource": "reports"
        }
    ]
}

このルールでは、department が "engineering" のユーザーに対し、 reports の "read" を許可します。

Node.js での ABAC 実装

以下のシンプルな関数を使うと、ABAC ルールに基づいてアクセスを許可または拒否できます。

const rules = [
    {
        attribute: "department",
        value: "engineering",
        action: "read",
        resource: "reports"
    }
];

const checkAccess = (user, action, resource) => {
    return rules.some(rule =>
        user[rule.attribute] === rule.value &&
        rule.action === action &&
        rule.resource === resource
    );
};

// ユーザー情報
const user = { department: 'engineering' };

// アクセスの可否を判定
console.log(checkAccess(user, 'read', 'reports')); // true
console.log(checkAccess(user, 'write', 'reports')); // false

このコードでは:

  • rules にABACのルールを定義
  • checkAccess() 関数で、ユーザー情報とルールを照合
  • department: 'engineering' のユーザーは "reports" の "read" を許可される

ABAC のデメリット

  • 管理が複雑になる可能性
    • 柔軟な設定が可能な分、ルールが増えすぎると管理が煩雑になる。
    • そのため、ポリシーの一元管理ツール(例:AWS IAM Policy、OPA(Open Policy Agent))を活用すると良い。
  • パフォーマンスの問題
    • ユーザー属性や環境条件の評価を行うため、リアルタイムの処理負荷が高くなる可能性がある。
    • キャッシュや最適化を適用し、パフォーマンスを向上させる必要がある。

GraphQL のリゾルバレベルでの認可チェック(graphql-shield)

graphql-shield は、GraphQL の認可(Authorization)を簡単に管理できるライブラリです。リゾルバごとにルールを適用し、アクセス制御を実装できます。

https://the-guild.dev/graphql/shield

メリット

  • リゾルバロジックと認可ロジックの分離
    認可の処理をリゾルバ本体から切り離すことで、コードの可読性を向上できます。
  • 柔軟なルール設定
    ユーザーのロール(管理者、一般ユーザーなど)や、特定の条件に基づいた細かいアクセス制御が可能です。
  • エラーハンドリングの統一
    認可エラーを一貫した形式で返せるため、フロントエンドでのエラーハンドリングが容易になります。

graphql-shield のインストール

npm install graphql-shield

ルールの定義

const { rule, shield } = require('graphql-shield');

const isAdmin = rule()(async (parent, args, { user }) => {
    return user.role === 'admin';
});

export const permissions = shield({
    Query: {
        sensitiveData: isAdmin
    }
});

ポイント

  • rule() を使って認可ルール (isAdmin) を作成。
  • shield() を使って、特定のリゾルバにルールを適用。

Apollo Server への適用

GraphQL サーバーで graphql-shield を適用するには、applyMiddleware を使用します。

const { ApolloServer } = require('apollo-server');
const { applyMiddleware } = require('graphql-middleware');
const { makeExecutableSchema } = require('@graphql-tools/schema');
const typeDefs = require('./schema');
const resolvers = require('./resolvers');
const { permissions } = require('./permissions');

// GraphQL スキーマの作成
const schema = makeExecutableSchema({ typeDefs, resolvers });

// 認可ミドルウェアの適用
const schemaWithPermissions = applyMiddleware(schema, permissions);

// Apollo Server の設定
const server = new ApolloServer({
    schema: schemaWithPermissions,
    context: ({ req }) => {
        // 認証情報を取得(例:JWT トークンを解析)
        const user = getUserFromToken(req.headers.authorization);
        return { user };
    }
});

server.listen().then(({ url }) => {
    console.log(`🚀 Server ready at ${url}`);
});

ポイント

  • makeExecutableSchema() でスキーマを作成。
  • applyMiddleware() で graphql-shield のルールを適用。
  • context で認証情報(ユーザー情報)を取得し、リゾルバ内で利用できるようにする。

エラーハンドリング

デフォルトでは、認可エラーが発生すると graphql-shield は "Not Authorised!" というエラーメッセージを返します。しかし、カスタムエラーメッセージを設定することも可能です。

const permissions = shield(
    {
        Query: {
            sensitiveData: isAdmin
        }
    },
    {
        fallbackError: "アクセス権がありません"
    }
);

REST API のスコープ制限(OAuth 2.0 の適用)

OAuth 2.0 を使用して API のスコープを制限することで、適切な権限を持つユーザーのみが特定の API を呼び出せるようにします。これにより、不適切なアクセスを防ぎ、セキュリティを強化できます。

OAuth 2.0 のスコープとは?

OAuth 2.0 のスコープ(Scope)は、アクセストークンを持つクライアントが実行できる操作の範囲を制限するために使用されます。スコープを設定することで、API のアクセス権限を細かく制御できます。

例えば、次のようなスコープを設定することで、ユーザー情報の閲覧や編集を制限できます。

{
    "scopes": {
        "read:users": "ユーザー情報の閲覧",
        "write:users": "ユーザー情報の編集"
    }
}
  • read:users: ユーザー情報の閲覧権限
  • write:users: ユーザー情報の編集権限

このように、スコープを細かく定義することで、特定の API のエンドポイントに対するアクセス権限を明確にできます。

スコープを利用した API アクセスの流れ

  1. クライアント(フロントエンドなど)が OAuth 2.0 の認証サーバーからアクセストークンを取得する。
  2. アクセストークン(JWT)には、スコープ情報が含まれている。
  3. クライアントはアクセストークンを Authorization ヘッダーに付与して API にリクエストを送る。
  4. Express サーバーは JWT を検証し、スコープ情報をチェックする。
  5. 指定されたスコープがある場合のみ API へのアクセスを許可する。

Express におけるスコープ制限の適用

Express アプリケーションで OAuth 2.0 のスコープを適用する方法を紹介します。

  1. スコープチェックのミドルウェア
    以下の checkScope ミドルウェアを使って、リクエストのスコープを確認し、不足している場合は 403 Forbidden を返します。
const checkScope = (scope) => {
    return (req, res, next) => {
        if (!req.user.scopes.includes(scope)) {
            return res.status(403).json({ message: 'Insufficient scope' });
        }
        next();
    };
};
  • req.user.scopes に、ユーザーが持っているスコープのリストが含まれている前提です。
  • 指定した scope がリストに含まれていない場合、403 エラーを返します。
  1. API ルートへの適用
    この checkScope ミドルウェアを使用して、スコープが適切な場合のみ API にアクセスできるように設定します。
app.get('/users', checkScope('read:users'), (req, res) => {
    res.json({ users: [{ id: 1, name: 'Alice' }] });
});

app.post('/users', checkScope('write:users'), (req, res) => {
    res.status(201).json({ message: 'User created' });
});
  • /users の GET リクエストは read:users スコープを持つユーザーのみが実行可能。
  • /users の POST リクエストは write:users スコープを持つユーザーのみが実行可能。

JWT (JSON Web Token) を利用したスコープ管理

OAuth 2.0 では、アクセストークン(JWT)を使用してスコープを管理することが一般的です。JWT にスコープ情報を含めることで、リクエストごとにユーザーの権限をチェックできます。

  1. JWT のデコードとスコープの取得
    JWT をデコードし、スコープを取得する方法を示します。
const jwt = require('jsonwebtoken');

const authenticateJWT = (req, res, next) => {
    const authHeader = req.headers.authorization;

    if (authHeader) {
        const token = authHeader.split(' ')[1];

        jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
            if (err) {
                return res.sendStatus(403);
            }
            req.user = user; // ユーザー情報をリクエストに格納
            next();
        });
    } else {
        res.sendStatus(401);
    }
};

// すべてのルートで JWT 認証を適用
app.use(authenticateJWT);
  • Authorization ヘッダーから JWT を取得し、検証を行います。
  • JWT に含まれる scopes 情報を req.user にセットすることで、後続のミドルウェアで利用可能になります。

API Gateway の導入(AWS API Gateway)

API Gateway は、クライアントとバックエンド API の間に立ち、以下のような機能を提供する重要なコンポーネントです。主に下記の役割を担っています。

  • 認証・認可の統一管理
    API レイヤーで JWT 認証、OAuth 2.0、API キーなどを適用可能。
  • ルーティングとロードバランシング
    リクエストを適切なバックエンドサービスに転送し、スケーリングをサポート。
  • レート制限とモニタリング
    DDoS 攻撃対策や API の使用状況を監視可能。

AWS API Gateway での JWT 認証

AWS API Gateway は、Amazon Cognito や Lambda Authorizer と統合することで、JWT(JSON Web Token)による認証を実現できます。

  1. Cognito と統合した JWT 認証
    AWS API Gateway は Cognito User Pools を ID プロバイダーとして使用できます。

設定手順

  • Cognito User Pool の作成
    • ユーザーの管理・認証を行う Cognito User Pool を作成
    • アプリクライアント ID を取得
  • API Gateway に Cognito Authorizer を設定
    • API Gateway の 「Authorizers」 から Cognito Authorizer を追加
    • User Pool ID と アプリクライアント ID を設定
  • リクエスト時に JWT トークンを渡す クライアント側で Cognito からアクセストークンを取得し、API リクエストの Authorization ヘッダーに付与:
curl -X GET https://your-api-id.execute-api.region.amazonaws.com/prod/resource \
  -H "Authorization: Bearer YOUR_JWT_ACCESS_TOKEN"
  1. Lambda Authorizer を使用した JWT 認証
    Cognito 以外の ID プロバイダー(Auth0、Firebase など)を使う場合、Lambda Authorizer を利用できます。

設定手順

  • Lambda 関数を作成し、JWT を検証
    • JWT の署名検証
    • クレーム(claim)のチェック(例: iss, aud)
    • ユーザーの権限(role)に基づいたアクセス制御
import json
import jwt

def lambda_handler(event, context):
    token = event['headers']['Authorization'].split(" ")[1]
    try:
        decoded_token = jwt.decode(token, "YOUR_PUBLIC_KEY", algorithms=["RS256"])
        return {
            "principalId": decoded_token["sub"],
            "policyDocument": {
                "Version": "2012-10-17",
                "Statement": [{
                    "Action": "execute-api:Invoke",
                    "Effect": "Allow",
                    "Resource": event["methodArn"]
                }]
            }
        }
    except Exception as e:
        return {"message": "Unauthorized"}
  • API Gateway の Authorizer に Lambda 関数を設定
    • API Gateway の「Authorizers」から Lambda Authorizer を追加
    • 作成した Lambda 関数を設定
    • Authorization ヘッダーをトークンとして扱うよう設定

ユーザーごとのデータアクセス制限(Multi-Tenancy 設計)

マルチテナンシーとは、一つのアプリケーションを複数のユーザー(テナント)が利用できるようにする設計です。
企業向けSaaSなどでよく用いられ、各テナントのデータが他のテナントから適切に分離される必要があります。

  • シングルテナント(Single-Tenancy): 各顧客に専用のアプリ・DBを提供
  • マルチテナント(Multi-Tenancy): 一つのアプリを複数の顧客で共有しながらも、データは適切に分離

このマルチテナンシーを実現するためのデータ分離設計には主に3つのパターンがあります。

設計パターン

  1. データベース分離型(Database-per-Tenant)
    各テナントごとに独立したデータベースを作成する方法。
  • メリット

    • データ分離が完全 → 他のテナントのデータにアクセスするリスクがない
    • パフォーマンスの確保 → テナントごとにリソースを管理しやすい
  • デメリット

    • 運用コストが高い → テナントごとにDBを作成・管理するため、スケールすると運用負担が増大
    • マイグレーションが難しい → 新しいテーブル変更時、すべてのDBに適用する必要がある
  • 使用例

    • 企業ごとにデータを厳格に分離する必要がある場合
    • 金融や医療系のアプリ(データセキュリティが最優先)
  1. スキーマ分離型(Schema-per-Tenant)
    一つのデータベース内に、テナントごとに異なるスキーマを用意する方法。
  • メリット
    • データ分離を確保しつつ、管理コストを削減
    • パフォーマンスを確保(特定のテナントに対してスキーマ単位で最適化が可能)
    • マイグレーションが比較的簡単(スキーマごとに適用)
  • デメリット
    • スキーマの管理が必要(増えるとDBの管理が複雑になる)
    • DBの接続管理が必要(スキーマをテナントごとに切り替えるロジックが必要)
  • 使用例
    • SaaS型サービス(中規模以上のマルチテナントアプリ)
    • ある程度のデータ分離が求められつつ、スケーラビリティも必要な場合
  1. 行レベルセキュリティ型(Row-Level Security, RLS)
    一つのデータベース・スキーマを複数のテナントが共有し、データを行レベルで制御する方法。
    PostgreSQL の RLS(Row-Level Security)を活用することで実装可能。
  • メリット

    • 最もスケーラブル(全てのデータを単一のDBに格納するため、管理しやすい)
    • コスト削減(テナント数が増えても追加のDBやスキーマが不要)
    • マイグレーションが容易(DB全体に適用できる)
  • デメリット

    • セキュリティ設定を誤ると、他テナントのデータが漏洩するリスク
    • クエリのオーバーヘッドが増加(適切なインデックス設計が必須)
  • 使用例

    • 中小規模の SaaS(スケールしやすく、コストを抑えられる)
    • エンタープライズ向けアプリ(アクセス制御が求められるが、テナントごとの完全分離は不要)

RLS の実装例

PostgreSQL の RLS を利用して、各テナントのデータを行単位で制御する方法です。

  1. RLS ポリシーを作成
ALTER TABLE users ENABLE ROW LEVEL SECURITY;

CREATE POLICY tenant_isolation_policy
ON users
FOR SELECT
USING (tenant_id = current_setting('app.current_tenant')::uuid);

上記の設定により、PostgreSQL は app.current_tenant に設定された tenant_id と一致する行のみを返します。

  1. アプリケーション側の設定
    テナントごとに SET app.current_tenant を設定することで、SQL クエリ実行時に自動的に適切なデータがフィルタリングされます。
async function setTenantContext(tenantId: string) {
  await db.query("SET app.current_tenant = $1", [tenantId]);
}

この方式を使うと、開発者が個別に WHERE tenant_id = xxx のようなフィルタリングを行わなくても、DBが自動で適切なデータのみを返してくれます。

設計パターンの比較

選択のポイント

  • データ分離が最重要 → Database-per-Tenant
  • バランスを取りたい → Schema-per-Tenant
  • スケールしやすさとコスト重視 → RLS(Row-Level Security)
設計パターン データ分離の強度 スケーラビリティ 運用コスト 主な用途
DB分離型 低(テナント増加で負荷増) 高セキュリティが求められるSaaS(金融・医療)
スキーマ分離型 中規模のSaaS(エンタープライズ向け)
RLS(行単位管理) 小規模〜中規模のSaaS(スタートアップ向け)

GraphQL の introspection を制限(本番環境では無効化)

GraphQL の introspection(イントロスペクション) は、クライアントがスキーマの詳細を問い合わせるための仕組みです。
例えば、以下のようなクエリを実行すると、API がどのようなスキーマを持っているかを取得できます。

{
  __schema {
    types {
      name
    }
  }
}

この機能を利用することで、開発者はスキーマの構造を確認しながら開発できますが、本番環境で 無制限に introspection を許可すると、セキュリティリスクが発生 します。

本番環境で introspection を無効化すべき理由

  • API の構造が第三者に知られてしまう
    API のエンドポイントやスキーマを悪意のあるユーザーが把握し、攻撃対象を特定しやすくなる。

  • 攻撃者が API の脆弱性を探しやすくなる
    例えば、意図しないエンドポイントや過去の古いスキーマが露出していると、それを利用して脆弱性を狙われる可能性がある。

  • 不要なリソース消費
    introspection クエリが無駄に実行されることで、API サーバーに不要な負荷がかかる。

本番環境で introspection を無効化する方法

GraphQL サーバーの実装によって異なりますが、代表的な Apollo Server での無効化方法を紹介します。

  • Apollo Server の場合
    Apollo Server では introspection オプションを false に設定することで、本番環境で introspection を無効化できます。
import { ApolloServer } from "apollo-server";
import { ApolloServerPluginLandingPageDisabled } from "apollo-server-core";

const server = new ApolloServer({
  schema,
  plugins: [
    // GraphQL Playground の UI も無効化
    ApolloServerPluginLandingPageDisabled(),
  ],
  // 本番環境では introspection を無効化
  introspection: process.env.NODE_ENV !== "production",
});
  • Express(graphqlHTTP) の場合
    express-graphql を使っている場合も、graphiql: false に設定し、introspection を false にできます。
import express from "express";
import { graphqlHTTP } from "express-graphql";
import schema from "./schema";

const app = express();

app.use(
  "/graphql",
  graphqlHTTP({
    schema,
    // 本番環境では GraphiQL を無効化
    graphiql: process.env.NODE_ENV !== "production", 
    customFormatErrorFn: (err) => {
      // エラーの詳細を漏らさない
      return { message: "Internal Server Error" }; 
    },
    // 本番環境で introspection を無効化
    introspection: process.env.NODE_ENV !== "production", 
  })
);

app.listen(4000, () => {
  console.log("Server running on port 4000");
});

本番環境で introspection を無効化しなくても良いケース

必ずしも "完全無効化" すべきとは限らないケースもあります。

  • 社内ツールやクローズドな API の場合
    認証済みユーザーにだけ introspection を許可する ことで、内部開発者は利用可能。
  • 特定の IP アドレスや JWT トークンを持つユーザーにのみ許可
    context でリクエスト元を判定し、適切なユーザーにだけ許可する。

例)特定の IP アドレスからのみ introspection を許可

const allowedIPs = ["192.168.1.1", "203.0.113.5"];

const server = new ApolloServer({
  schema,
  introspection: ({ req }) => {
    const clientIP = req.ip || req.connection.remoteAddress;
    return allowedIPs.includes(clientIP);
  },
});

過剰なデータ取得を防ぐための Rate Limiting の導入

API の負荷を軽減し、不正アクセスや DDoS 攻撃を防ぐために Rate Limiting(リクエスト制限)は重要です。ここでは、Express + Redis を使用して Rate Limiting を実装する方法を詳しく解説します。

Rate Limiting とは

Rate Limiting は、一定時間内に許可される API リクエスト数を制限する仕組みです。
これにより、以下のような問題を防ぐことができます。

  • サーバー負荷の軽減:短時間に大量のリクエストが発生することで、サーバーが過負荷になるのを防ぐ。
  • DDoS 攻撃の抑制:悪意のあるリクエストがサーバーを圧迫するのを防ぐ。
  • 公平なリソース配分:特定のユーザーが過剰にリソースを消費するのを抑え、他のユーザーに公平なリソースを提供する。

実装方法(Express + Redis)

以下のライブラリを使用します。

ライブラリ名 説明
express-rate-limit Express 用の Rate Limiting ミドルウェア
rate-limit-redis Redis を Rate Limiting のストアとして使用するためのライブラリ
ioredis Redis クライアント(接続管理を行う)

インストールコマンド

npm install express-rate-limit rate-limit-redis ioredis

実装コード

import express from 'express';
import rateLimit from 'express-rate-limit';
import RedisStore from 'rate-limit-redis';
import Redis from 'ioredis';

const app = express();

// Redis クライアントの作成
const redisClient = new Redis({
  host: 'redis',
  port: 6379,
  enableOfflineQueue: false,
});

// Rate Limiter の設定
const limiter = rateLimit({
  store: new RedisStore({
    client: redisClient,
    expiry: 60, // 60秒間で制限
  }),
  windowMs: 1 * 60 * 1000, // 1分間の時間枠
  max: 100, // 1分間に最大 100 リクエストまで許可
  message: 'Too many requests, please try again later.',
  standardHeaders: true, // RateLimit ヘッダーを追加
  legacyHeaders: false, // `X-RateLimit-*` ヘッダーを無効化(新しい標準ヘッダーを使用)
});

// 特定の API ルートに Rate Limiting を適用
app.use('/api/', limiter);

app.get('/api/test', (req, res) => {
  res.send('API response');
});

// サーバー起動
app.listen(3000, () => {
  console.log('Server is running on port 3000');
});

Rate Limiting の仕組み

  • クライアントの IP アドレスごと にカウント。
  • Redis にリクエスト数を保存し、一定時間ごとにリセット。
  • 制限を超えると 429 Too Many Requests を返す。

GraphQL / OpenAPI スキーマの適切なバージョン管理

API のバージョン管理は、クライアントとの互換性を維持しつつ、新機能の追加や既存機能の改善を進めるために不可欠です。それぞれのアプローチ(GraphQL と OpenAPI)において、どのようにバージョン管理を行うべきか詳しく解説します。

API のバージョン管理が必要な理由

API を運用するうえで、以下のような理由から適切なバージョン管理が求められます。

  • 互換性の維持
    API を利用するクライアント(フロントエンドアプリ、モバイルアプリ、サードパーティー)に影響を与えずに、スキーマの変更を適用する必要がある。
  • 安全な移行
    新しい API への移行をスムーズに行い、古い API を段階的に廃止できるようにする。
  • 開発スピードの向上
    バージョンごとに変更を整理することで、開発チームが安心して新機能の追加・修正を行える。

GraphQL のバージョン管理

GraphQL では、REST API のようにエンドポイントをバージョンごとに分けるのではなく、スキーマの進化(Schema Evolution)を通じてバージョン管理を行います。

GraphQL のバージョン管理戦略

  1. フィールドの追加
    GraphQL は後方互換性を維持しやすく、既存のフィールドを変更せずに新しいフィールドを追加できる。
type Query {
  user: User
}
  1. フィールドの非推奨化(Deprecation)
    既存フィールドを削除せず、非推奨 (@deprecated) にして新しいフィールドへ移行を促す。
type Query {
  userV1: UserV1 @deprecated(reason: "Use userV2")
  userV2: UserV2
}
  1. 新しい型の導入
    UserV1 → UserV2 のように型をバージョンアップすることで、互換性を維持しつつ進化できる。
type UserV1 {
  id: ID
  name: String
}
type UserV2 {
  id: ID
  fullName: String
}

GraphQL で非推奨フィールドをクライアントに通知

GraphQL のクライアント(Apollo Client など)は、非推奨フィールドをリクエストすると警告を出すため、開発者に適切なバージョン移行を促すことができる。

OpenAPI のバージョン管理(Swagger)

OpenAPI(旧 Swagger)は、REST API のスキーマを定義する標準仕様であり、バージョン管理の方法はいくつか存在します。

OpenAPI のバージョン管理戦略

  1. URL にバージョン番号を含める(推奨)
    バージョンごとにエンドポイントを分ける方法。
    API の変更が大きい場合に有効。
openapi: 3.0.0
info:
  title: Example API
  version: 2.0.0
paths:
  /v1/users:
    get:
      summary: Get users (v1, deprecated)
      deprecated: true
  /v2/users:
    get:
      summary: Get users (v2)
  1. HTTP ヘッダーでバージョンを指定
    ヘッダーを利用してバージョンを切り替える方法。
    エンドポイント URL を変更せずにバージョン管理ができる。
GET /users
Headers:
  X-API-Version: 2
  1. Query パラメータでバージョン指定
    クエリパラメータに ?version=2 を含めてバージョン管理を行う方法。
GET /users?version=2

API ログの監視(Datadog / Sentry を利用)

監視の目的

API のログを監視することで、以下のような目的を達成できる。

  • パフォーマンスの最適化
    • レスポンスタイムの変動やスローダウンの原因を特定
    • ボトルネックの分析と最適化
  • エラーの早期検知と対応
    • 例外や障害発生時に即時アラートを受信
    • 問題の発生箇所や影響範囲を迅速に特定
  • セキュリティ監視
    • 不審なリクエストや攻撃の兆候を検知
    • API の異常な使用パターンを監視
  • システムの信頼性向上
    • 障害発生時の影響を最小限に抑え、SLA(サービスレベルアグリーメント)を維持
    • インシデント対応の迅速化

Datadog を利用した監視

Datadog は、リアルタイムのモニタリングと可視化に強みを持つツールです。

主要機能

  • APM(Application Performance Monitoring)
    API のリクエスト/レスポンスタイムを詳細に記録
    スパン(Span)を使ってリクエストの流れを可視化し、ボトルネックを特定
  • ログ管理
    API のリクエスト・レスポンスデータの収集・分析
    フィルタリングやカスタムメトリクスの設定
  • アラート設定
    API のレスポンス遅延やエラーレートが閾値を超えた際にアラートを送信
    Slack や PagerDuty などの通知システムと連携

導入手順(概要)

  • Datadog のアカウントを作成し、API Key を取得
  • アプリケーションに Datadog Agent をインストール
  • API のログを Datadog に送信する設定を行う(例: winston-datadog を使用)
  • ダッシュボードを作成し、リアルタイムで監視

Sentry を利用したエラーログの監視

Sentry は、アプリケーションのエラートラッキングに特化したツール。

主要機能

  • エラーログの自動収集
    API のエラーログをリアルタイムで記録し、スタックトレースを表示
    エラーの発生箇所や原因を特定しやすくする
  • ユーザー影響分析
    特定のエラーがどのユーザーに影響を与えたかを分析
    エラー発生頻度や影響範囲を可視化
  • リリース管理
    デプロイごとのエラーレポートを管理し、特定のバージョンで発生した問題を特定
  • アラート通知
    重要なエラーが発生した際に通知を送信
    Slack やメールと連携可能

導入手順(概要)

  • Sentry のアカウントを作成し、DSN(Data Source Name)を取得
  • API のエラーログを Sentry に送信するように設定(例: @sentry/node を使用)
  • ダッシュボードを作成し、エラーを監視・分析
  • アラートの設定を行い、重大なエラー発生時に通知を受け取る

Next.jsのプロジェクトにSentryを導入したブログ記事がありますのでご参考ください。

https://shinagawa-web.com/blogs/nextjs-sentry-tutorial

まとめ

APIの認可とセキュリティ対策は、一度導入すれば終わりではなく、継続的な改善が求められます。RBAC や ABAC を活用した細かなアクセス制御、Rate Limiting によるリソース保護、GraphQL introspection の制限、ログ監視の強化など、多層的なアプローチを組み合わせることで、より安全な API を構築できます。

本記事で紹介した手法を実践し、自社の環境に適したセキュリティ対策を整えていきましょう。今後も API の安全性を維持しながら、スケーラブルなシステムを構築していくための取り組みを続けていきましょう。

記事に関するお問い合わせ📝

記事の内容に関するご質問、ご意見などは、下記よりお気軽にお問い合わせください。
ご質問フォームへ

技術支援などお仕事に関するお問い合わせ📄

技術支援やお仕事のご依頼に関するお問い合わせは、下記よりお気軽にお問い合わせください。
お問い合わせフォームへ

関連する技術ブログ