Chuyển đến nội dung chính

レッスン 22: API ゲートウェイ、Nginx、マイクロサービス

Keycloakは、Nginx(lua-resty-openidc/nginx-oidc-moduleまたはOAuth2プロキシ)、Kongゲートウェイ(OIDCプラグイン)、Traefik(ForwardAuthミドルウェア)、マイクロサービス用のAPIゲートウェイパターン、サービスアカウント認証(クライアント資格情報の付与)、トークン交換、内部サービス間認証、および完全なDocker Composeスタックと統合します。

🔒 DevSecOps — レッスン 22 レッスン 22: API ゲートウェイ、Nginx、および マイクロサービス

基本から上級までの Keycloak

パート 6: 実際のアプリケーションの統合

xdev.asia

1. Keycloakを使用したAPIゲートウェイパターン

マイクロサービス アーキテクチャでは、API ゲートウェイはすべてのクライアント リクエストに対する単一のエントリ ポイントとして機能します。 Keycloakと組み合わせると、ゲートウェイは次の処理を実行します。集中認証そしてJWTの伝播ダウンストリームサービスに。

                    ┌──────────────────────────────────────┐
                    │           API Gateway                │
                    │    (Nginx / Kong / Traefik)          │
 ┌────────┐        │                                      │        ┌──────────────┐
 │ Client │────────▶│  1. Validate JWT (Keycloak JWKS)    │───────▶│ Service A    │
 │ (SPA)  │        │  2. Rate Limiting                    │        │ (User API)   │
 └────────┘        │  3. Route to service                 │        └──────────────┘
                    │  4. Propagate JWT header             │
 ┌────────┐        │                                      │        ┌──────────────┐
 │ Mobile │────────▶│                                      │───────▶│ Service B    │
 │  App   │        │                                      │        │ (Order API)  │
 └────────┘        └───────────────┬──────────────────────┘        └──────────────┘
                                    │                                      │
                    ┌───────────────▼──────────────────────┐               │
                    │           Keycloak                    │               │
                    │  - Token Validation (JWKS)           │        ┌──────▼───────┐
                    │  - Token Exchange                    │        │ Service C    │
                    │  - Service Account Tokens            │        │ (Payment)    │
                    └──────────────────────────────────────┘        └──────────────┘
ゲートウェイ認証方法アドバンテージ
Nginx + lua-resty-openidcLua OIDC モジュール軽量、高性能
Nginx + OAuth2 プロキシサイドカープロキシ簡単なセットアップ、Lua は不要
コングゲートウェイOIDC プラグインエンタープライズ機能、プラグイン エコシステム
トレイフィク転送認証クラウドネイティブな自動検出

2. Nginx + Keycloakの統合

2.1 アプローチ 1: lua-resty-openidc

lua-resty-openidcNginx/OpenResty 用の OpenID Connect モジュールで、JWT 検証、トークン イントロスペクション、OIDC ログイン フローを Nginx 層で直接サポートします。

2.1.1 OpenRestyのインストール

# Dockerfile.openresty
FROM openresty/openresty:1.25.3.1-jammy

# Cài đặt lua-resty-openidc và dependencies
RUN luarocks install lua-resty-openidc 1.7.6
RUN luarocks install lua-resty-session 4.0.5

COPY nginx.conf /usr/local/openresty/nginx/conf/nginx.conf
COPY default.conf /etc/nginx/conf.d/default.conf

EXPOSE 80

2.1.2 ベアラートークン検証を使用した Nginx 構成

# /etc/nginx/conf.d/default.conf

lua_package_path '/usr/local/openresty/lualib/?.lua;;';

# Shared dict cho OIDC session & discovery cache
lua_shared_dict discovery 1m;
lua_shared_dict jwks 1m;
lua_shared_dict introspection 10m;

# Resolver cho DNS (Docker internal DNS)
resolver 127.0.0.11 ipv6=off;

upstream user-service {
    server user-service:8081;
}

upstream order-service {
    server order-service:8082;
}

server {
    listen 80;
    server_name api.example.com;

    # ===== Public endpoints (không cần auth) =====
    location /api/public/ {
        proxy_pass http://user-service;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }

    # ===== Protected endpoints (JWT validation) =====
    location /api/users/ {
        access_by_lua_block {
            local opts = {
                discovery = "http://keycloak:8080/realms/my-realm/.well-known/openid-configuration",
                token_signing_alg_values_expected = { "RS256" },
                accept_none_alg = false,
                accept_unsupported_alg = false,
            }

            -- Verify Bearer token
            local res, err = require("resty.openidc").bearer_jwt_verify(opts)

            if err or not res then
                ngx.status = 401
                ngx.header["Content-Type"] = "application/json"
                ngx.say('{"error": "Unauthorized", "message": "' .. (err or "invalid token") .. '"}')
                return ngx.exit(ngx.HTTP_UNAUTHORIZED)
            end

            -- Propagate user info to upstream via headers
            ngx.req.set_header("X-User-ID", res.sub)
            ngx.req.set_header("X-User-Name", res.preferred_username or "")
            ngx.req.set_header("X-User-Email", res.email or "")

            -- Extract và propagate roles
            local realm_roles = ""
            if res.realm_access and res.realm_access.roles then
                realm_roles = table.concat(res.realm_access.roles, ",")
            end
            ngx.req.set_header("X-User-Roles", realm_roles)
        }

        proxy_pass http://user-service;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    location /api/orders/ {
        access_by_lua_block {
            local opts = {
                discovery = "http://keycloak:8080/realms/my-realm/.well-known/openid-configuration",
                token_signing_alg_values_expected = { "RS256" },
            }

            local res, err = require("resty.openidc").bearer_jwt_verify(opts)

            if err or not res then
                ngx.status = 401
                ngx.header["Content-Type"] = "application/json"
                ngx.say('{"error": "Unauthorized"}')
                return ngx.exit(ngx.HTTP_UNAUTHORIZED)
            end

            -- Kiểm tra role cần thiết
            local has_role = false
            if res.realm_access and res.realm_access.roles then
                for _, role in ipairs(res.realm_access.roles) do
                    if role == "USER" or role == "ADMIN" then
                        has_role = true
                        break
                    end
                end
            end

            if not has_role then
                ngx.status = 403
                ngx.header["Content-Type"] = "application/json"
                ngx.say('{"error": "Forbidden", "message": "Insufficient permissions"}')
                return ngx.exit(ngx.HTTP_FORBIDDEN)
            end

            ngx.req.set_header("X-User-ID", res.sub)
            ngx.req.set_header("X-User-Name", res.preferred_username or "")
        }

        proxy_pass http://order-service;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

2.2 アプローチ 2: OAuth2 プロキシ サイドカー

oauth2-プロキシは、OAuth2/OIDC プロバイダーを介して認証を提供するリバース プロキシです。このアプローチには Lua は必要なく、セットアップが簡単です。

┌────────┐     ┌──────────────┐     ┌───────┐     ┌─────────────┐
│ Client │────▶│ Nginx        │────▶│OAuth2 │────▶│ Backend     │
│        │     │ (Reverse     │     │Proxy  │     │ Service     │
│        │     │  Proxy)      │     │       │     │             │
└────────┘     └──────────────┘     └───┬───┘     └─────────────┘
                                        │
                                  ┌─────▼─────┐
                                  │ Keycloak  │
                                  │           │
                                  └───────────┘

2.2.1 OAuth2 プロキシ構成

# oauth2-proxy.cfg
provider = "keycloak-oidc"
provider_display_name = "Keycloak"

# Keycloak OIDC configuration
oidc_issuer_url = "http://keycloak:8080/realms/my-realm"
client_id = "oauth2-proxy-client"
client_secret = "your-client-secret"

# Cookie configuration
cookie_secret = "a-32-byte-base64-encoded-secret!"
cookie_secure = false  # true cho HTTPS
cookie_name = "_oauth2_proxy"

# Redirect URL
redirect_url = "http://localhost:4180/oauth2/callback"

# Upstream configuration
upstreams = [
  "http://user-service:8081"
]

# Email domain (cho phép tất cả)
email_domains = ["*"]

# Pass headers to upstream
set_xauthrequest = true
pass_access_token = true
pass_authorization_header = true

# Skip auth cho health endpoints
skip_auth_routes = [
  "^/api/public/",
  "^/health"
]

# Token refresh
cookie_refresh = "1m"

2.2.2 auth_request を使用した Nginx

# nginx.conf - sử dụng auth_request directive
server {
    listen 80;
    server_name api.example.com;

    # OAuth2 Proxy endpoints
    location /oauth2/ {
        proxy_pass http://oauth2-proxy:4180;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Auth-Request-Redirect $request_uri;
    }

    location = /oauth2/auth {
        proxy_pass http://oauth2-proxy:4180;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Content-Length "";
        proxy_pass_request_body off;
    }

    # Public endpoints
    location /api/public/ {
        proxy_pass http://user-service:8081;
    }

    # Protected endpoints
    location /api/ {
        auth_request /oauth2/auth;

        # Pass auth headers to upstream
        auth_request_set $user $upstream_http_x_auth_request_user;
        auth_request_set $email $upstream_http_x_auth_request_email;
        auth_request_set $access_token $upstream_http_x_auth_request_access_token;

        proxy_set_header X-User $user;
        proxy_set_header X-Email $email;
        proxy_set_header Authorization "Bearer $access_token";

        # Error handling
        error_page 401 = /oauth2/sign_in;

        proxy_pass http://user-service:8081;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

3. Kong ゲートウェイ + Keycloak

3.1 Kong OIDC プラグインの設定

Kong Gateway は、Keycloak からの JWT トークンを検証するための OIDC プラグイン (Kong Enterprise またはコミュニティ プラグイン) をサポートしています。

# kong.yml - Declarative configuration
_format_version: "3.0"

services:
  # ===== User Service =====
  - name: user-service
    url: http://user-service:8081
    routes:
      - name: user-api
        paths:
          - /api/users
        strip_path: false
    plugins:
      - name: openid-connect
        config:
          issuer: http://keycloak:8080/realms/my-realm
          client_id:
            - my-kong-client
          client_secret:
            - your-client-secret
          auth_methods:
            - bearer
          bearer_token_param_type:
            - header
          # Chỉ verify bearer token (Resource Server mode)
          bearer_only: "yes"
          # Cache introspection results
          cache_introspection: true
          cache_token_exchange: true
          # Propagate consumer headers
          upstream_headers_claims:
            - sub
            - preferred_username
            - email
          upstream_headers_names:
            - X-User-ID
            - X-User-Name
            - X-User-Email

  # ===== Order Service =====
  - name: order-service
    url: http://order-service:8082
    routes:
      - name: order-api
        paths:
          - /api/orders
        strip_path: false
    plugins:
      - name: openid-connect
        config:
          issuer: http://keycloak:8080/realms/my-realm
          client_id:
            - my-kong-client
          client_secret:
            - your-client-secret
          auth_methods:
            - bearer
          bearer_only: "yes"
          # Scope và role check
          scopes_required:
            - openid
          roles_required:
            - USER

  # ===== Public Service (no auth) =====
  - name: public-service
    url: http://user-service:8081
    routes:
      - name: public-api
        paths:
          - /api/public
        strip_path: false

# ===== Rate Limiting =====
plugins:
  - name: rate-limiting
    config:
      minute: 100
      policy: local

3.2 Kong 管理 API 構成

宣言的ではなく Kong Admin API 経由で設定可能:

# 1. Tạo service
curl -X POST http://localhost:8001/services \
  -d name=user-service \
  -d url=http://user-service:8081

# 2. Tạo route
curl -X POST http://localhost:8001/services/user-service/routes \
  -d 'name=user-api' \
  -d 'paths[]=/api/users' \
  -d 'strip_path=false'

# 3. Thêm OIDC plugin cho route
curl -X POST http://localhost:8001/routes/user-api/plugins \
  -d 'name=openid-connect' \
  -d 'config.issuer=http://keycloak:8080/realms/my-realm' \
  -d 'config.client_id=my-kong-client' \
  -d 'config.client_secret=your-client-secret' \
  -d 'config.auth_methods=bearer' \
  -d 'config.bearer_only=yes'

# 4. Thêm rate limiting
curl -X POST http://localhost:8001/routes/user-api/plugins \
  -d 'name=rate-limiting' \
  -d 'config.minute=60' \
  -d 'config.policy=local'

4. トレイフィク + キークローク

4.1 ForwardAuthミドルウェア

トラフィックが使用ForwardAuth ミドルウェア認証を外部サービス (OAuth2 プロキシ) に委任するには:

# traefik.yml - Static configuration
api:
  dashboard: true
  insecure: true

entryPoints:
  web:
    address: ":80"
  websecure:
    address: ":443"

providers:
  docker:
    exposedByDefault: false
  file:
    filename: /etc/traefik/dynamic.yml
# dynamic.yml - Dynamic configuration
http:
  middlewares:
    # ForwardAuth middleware - delegate auth to OAuth2 Proxy
    keycloak-auth:
      forwardAuth:
        address: "http://oauth2-proxy:4180/oauth2/auth"
        trustForwardHeader: true
        authResponseHeaders:
          - X-Auth-Request-User
          - X-Auth-Request-Email
          - X-Auth-Request-Access-Token
          - Authorization

    # Rate limiting
    rate-limit:
      rateLimit:
        average: 100
        burst: 50
        period: 1m

  routers:
    # Public routes (no auth)
    public-api:
      rule: "PathPrefix(`/api/public`)"
      service: user-service
      entryPoints:
        - web

    # Protected routes (with auth)
    user-api:
      rule: "PathPrefix(`/api/users`)"
      service: user-service
      entryPoints:
        - web
      middlewares:
        - keycloak-auth
        - rate-limit

    order-api:
      rule: "PathPrefix(`/api/orders`)"
      service: order-service
      entryPoints:
        - web
      middlewares:
        - keycloak-auth

    # OAuth2 Proxy routes
    oauth2-proxy:
      rule: "PathPrefix(`/oauth2`)"
      service: oauth2-proxy
      entryPoints:
        - web

  services:
    user-service:
      loadBalancer:
        servers:
          - url: "http://user-service:8081"

    order-service:
      loadBalancer:
        servers:
          - url: "http://order-service:8082"

    oauth2-proxy:
      loadBalancer:
        servers:
          - url: "http://oauth2-proxy:4180"

4.2 Docker ラベルを使用した Traefik

# docker-compose.yml snippet cho Traefik + Docker labels
services:
  user-service:
    image: user-service:latest
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.user-api.rule=PathPrefix(`/api/users`)"
      - "traefik.http.routers.user-api.entrypoints=web"
      - "traefik.http.routers.user-api.middlewares=keycloak-auth@file"
      - "traefik.http.services.user-service.loadbalancer.server.port=8081"

5. サービスアカウントの認証

5.1 クライアント資格情報の付与

サービス間通信 (ユーザー コンテキストなし) の場合は、次を使用します。クライアント資格情報の付与:

┌──────────────┐                    ┌──────────────┐
│ Service A    │                    │ Keycloak     │
│ (Order)      │                    │              │
│              │── 1. client_id ───▶│              │
│              │   + client_secret  │              │
│              │                    │              │
│              │◀── 2. Access ──────│              │
│              │    Token           │              │
└──────┬───────┘                    └──────────────┘
       │
       │ 3. Bearer token
       ▼
┌──────────────┐
│ Service B    │
│ (Payment)    │
│              │
└──────────────┘

5.1.1 Keycloakクライアントのセットアップ

Client Settings cho Service Account:
  Client ID:           order-service
  Client Protocol:     openid-connect
  Access Type:         confidential
  Service Accounts:    ON
  Standard Flow:       OFF (không cần user login)
  Direct Access:       OFF

  Service Account Roles:
    → Assign realm roles hoặc client roles cần thiết
    → Ví dụ: payment-read, payment-write

5.1.2 サービスアカウントトークンの取得

# Client Credentials Grant
curl -s -X POST \
  "http://localhost:8080/realms/my-realm/protocol/openid-connect/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials" \
  -d "client_id=order-service" \
  -d "client_secret=order-service-secret" \
  | jq .

# Response:
# {
#   "access_token": "eyJhbGci...",
#   "expires_in": 300,
#   "token_type": "Bearer",
#   "not-before-policy": 0,
#   "scope": "profile email"
# }

5.1.3 Spring Boot のサービス アカウント

package com.example.service;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;

import java.time.Instant;
import java.util.Map;

@Service
public class ServiceAccountTokenProvider {

    @Value("${keycloak.auth-server-url}")
    private String keycloakUrl;

    @Value("${keycloak.realm}")
    private String realm;

    @Value("${keycloak.service-account.client-id}")
    private String clientId;

    @Value("${keycloak.service-account.client-secret}")
    private String clientSecret;

    private final RestTemplate restTemplate = new RestTemplate();

    private String cachedToken;
    private Instant tokenExpiry;

    public synchronized String getServiceAccountToken() {
        // Return cached token nếu còn hạn
        if (cachedToken != null && Instant.now().isBefore(tokenExpiry)) {
            return cachedToken;
        }

        // Request new token
        String tokenUrl = String.format(
            "%s/realms/%s/protocol/openid-connect/token",
            keycloakUrl, realm
        );

        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);

        MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
        body.add("grant_type", "client_credentials");
        body.add("client_id", clientId);
        body.add("client_secret", clientSecret);

        HttpEntity<MultiValueMap<String, String>> request =
            new HttpEntity<>(body, headers);

        ResponseEntity<Map> response = restTemplate.postForEntity(
            tokenUrl, request, Map.class
        );

        Map<String, Object> tokenResponse = response.getBody();
        cachedToken = (String) tokenResponse.get("access_token");
        int expiresIn = (Integer) tokenResponse.get("expires_in");
        // Refresh 30 giây trước khi hết hạn
        tokenExpiry = Instant.now().plusSeconds(expiresIn - 30);

        return cachedToken;
    }
}
// Sử dụng service account token để gọi downstream service
@Service
public class PaymentServiceClient {

    private final RestTemplate restTemplate = new RestTemplate();
    private final ServiceAccountTokenProvider tokenProvider;

    public PaymentServiceClient(ServiceAccountTokenProvider tokenProvider) {
        this.tokenProvider = tokenProvider;
    }

    public PaymentResult processPayment(PaymentRequest request) {
        String token = tokenProvider.getServiceAccountToken();

        HttpHeaders headers = new HttpHeaders();
        headers.setBearerAuth(token);
        headers.setContentType(MediaType.APPLICATION_JSON);

        HttpEntity<PaymentRequest> entity = new HttpEntity<>(request, headers);

        ResponseEntity<PaymentResult> response = restTemplate.exchange(
            "http://payment-service:8083/api/payments",
            HttpMethod.POST,
            entity,
            PaymentResult.class
        );

        return response.getBody();
    }
}

5.2 トークン交換 (RFC 8693)

トークン交換を使用すると、サービスはユーザー トークンを異なるスコープ/対象ユーザーを持つ新しいトークンに交換したり、ダウンストリーム サービスを呼び出すときにユーザーになりすますことができます。

# Bật Token Exchange trong Keycloak
# Realm Settings → Token → Token Exchange: Enable

# Exchange user token thành service-specific token
curl -s -X POST \
  "http://localhost:8080/realms/my-realm/protocol/openid-connect/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" \
  -d "client_id=order-service" \
  -d "client_secret=order-service-secret" \
  -d "subject_token=$USER_ACCESS_TOKEN" \
  -d "subject_token_type=urn:ietf:params:oauth:token-type:access_token" \
  -d "audience=payment-service" \
  -d "requested_token_type=urn:ietf:params:oauth:token-type:access_token" \
  | jq .

# Response chứa token mới với audience = payment-service
# Token vẫn giữ user identity nhưng scoped cho payment-service
Luồng Token Exchange:

User ──▶ Order Service (user token)
              │
              ├── Exchange user token → Keycloak
              │   (audience = payment-service)
              │
              ◀── New token (user context, scoped for payment)
              │
              ├── Call Payment Service (exchanged token)
              │
              ◀── Payment result

5.3 内部サービスの認証パターン

パターン使用事例長所短所
JWTの伝播ユーザートークンをダウンストリームに転送するシンプルでユーザーコンテキストが保持されるトークンの有効期限の問題
クライアントの資格情報サービス間、ユーザーコンテキストなしユーザーセッションに依存しないユーザー ID がありません
トークン交換なりすまし、視聴者制限ユーザーコンテキスト + 限定されたアクセス追加の Keycloak 呼び出し
mTLSゼロトラストサービスメッシュ強力な ID、トークンは不要証明書の管理

6. 完全な Docker Compose スタック

Keycloak、PostgreSQL、Nginx ゲートウェイおよびバックエンド サービスを備えた Docker Compose スタック:

# docker-compose.yml
version: '3.9'

services:
  # ===== PostgreSQL Database =====
  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: keycloak
      POSTGRES_USER: keycloak
      POSTGRES_PASSWORD: keycloak_password
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U keycloak"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - backend

  # ===== Keycloak =====
  keycloak:
    image: quay.io/keycloak/keycloak:25.0
    command: start-dev --import-realm
    environment:
      KC_DB: postgres
      KC_DB_URL_HOST: postgres
      KC_DB_URL_DATABASE: keycloak
      KC_DB_USERNAME: keycloak
      KC_DB_PASSWORD: keycloak_password
      KC_HOSTNAME: localhost
      KC_HOSTNAME_PORT: 8080
      KC_HTTP_ENABLED: "true"
      KC_HEALTH_ENABLED: "true"
      KEYCLOAK_ADMIN: admin
      KEYCLOAK_ADMIN_PASSWORD: admin
    volumes:
      - ./keycloak/realm-export.json:/opt/keycloak/data/import/realm-export.json
    ports:
      - "8080:8080"
    depends_on:
      postgres:
        condition: service_healthy
    healthcheck:
      test: ["CMD-SHELL", "exec 3<>/dev/tcp/localhost/8080 && echo -e 'GET /health/ready HTTP/1.1\r\nHost: localhost\r\n\r\n' >&3 && cat <&3 | grep -q '200'"]
      interval: 30s
      timeout: 10s
      retries: 5
    networks:
      - backend

  # ===== API Gateway (Nginx + OAuth2 Proxy) =====
  oauth2-proxy:
    image: quay.io/oauth2-proxy/oauth2-proxy:v7.6.0
    command:
      - --provider=keycloak-oidc
      - --provider-display-name=Keycloak
      - --oidc-issuer-url=http://keycloak:8080/realms/my-realm
      - --client-id=oauth2-proxy-client
      - --client-secret=oauth2-proxy-secret
      - --cookie-secret=bXktMzItYnl0ZS1iYXNlNjQtZW5jb2RlZC1zZWNyZXQ=
      - --cookie-secure=false
      - --email-domain=*
      - --upstream=static://202
      - --http-address=0.0.0.0:4180
      - --set-xauthrequest=true
      - --pass-access-token=true
      - --pass-authorization-header=true
      - --skip-auth-route=^/api/public/
      - --skip-provider-button=true
    depends_on:
      keycloak:
        condition: service_healthy
    networks:
      - backend

  nginx:
    image: nginx:1.27-alpine
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./nginx/conf.d:/etc/nginx/conf.d:ro
    ports:
      - "80:80"
    depends_on:
      - oauth2-proxy
      - user-service
      - order-service
    networks:
      - backend

  # ===== Backend Services =====
  user-service:
    build:
      context: ./services/user-service
      dockerfile: Dockerfile
    environment:
      SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI: http://keycloak:8080/realms/my-realm
      SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_JWK_SET_URI: http://keycloak:8080/realms/my-realm/protocol/openid-connect/certs
      SERVER_PORT: 8081
    ports:
      - "8081:8081"
    depends_on:
      keycloak:
        condition: service_healthy
    networks:
      - backend

  order-service:
    build:
      context: ./services/order-service
      dockerfile: Dockerfile
    environment:
      SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI: http://keycloak:8080/realms/my-realm
      SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_JWK_SET_URI: http://keycloak:8080/realms/my-realm/protocol/openid-connect/certs
      SERVER_PORT: 8082
      # Service account cho gọi payment-service
      KEYCLOAK_SERVICE_ACCOUNT_CLIENT_ID: order-service
      KEYCLOAK_SERVICE_ACCOUNT_CLIENT_SECRET: order-service-secret
    ports:
      - "8082:8082"
    depends_on:
      keycloak:
        condition: service_healthy
    networks:
      - backend

volumes:
  postgres_data:

networks:
  backend:
    driver: bridge

6.1 Docker Compose の Nginx 構成

# nginx/conf.d/default.conf
upstream user-service {
    server user-service:8081;
}

upstream order-service {
    server order-service:8082;
}

upstream oauth2-proxy {
    server oauth2-proxy:4180;
}

server {
    listen 80;
    server_name localhost;

    # ===== OAuth2 Proxy endpoints =====
    location /oauth2/ {
        proxy_pass http://oauth2-proxy;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Auth-Request-Redirect $request_uri;
    }

    location = /oauth2/auth {
        proxy_pass http://oauth2-proxy;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Content-Length "";
        proxy_pass_request_body off;
    }

    # ===== Public APIs (no auth) =====
    location /api/public/ {
        proxy_pass http://user-service;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }

    # ===== Protected: User APIs =====
    location /api/users {
        auth_request /oauth2/auth;
        auth_request_set $user $upstream_http_x_auth_request_user;
        auth_request_set $email $upstream_http_x_auth_request_email;
        auth_request_set $token $upstream_http_x_auth_request_access_token;

        proxy_set_header X-User $user;
        proxy_set_header X-Email $email;
        proxy_set_header Authorization "Bearer $token";

        error_page 401 = /oauth2/sign_in;

        proxy_pass http://user-service;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }

    # ===== Protected: Order APIs =====
    location /api/orders {
        auth_request /oauth2/auth;
        auth_request_set $token $upstream_http_x_auth_request_access_token;

        proxy_set_header Authorization "Bearer $token";

        error_page 401 = /oauth2/sign_in;

        proxy_pass http://order-service;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }

    # ===== Health check =====
    location /health {
        return 200 '{"status":"UP"}';
        add_header Content-Type application/json;
    }
}

7. モニタリングとレート制限

7.1 サービスアカウントトークンの監視

# Kiểm tra active sessions cho service account
curl -s "http://localhost:8080/admin/realms/my-realm/clients/$CLIENT_UUID/service-account-user" \
  -H "Authorization: Bearer $ADMIN_TOKEN" | jq .

# Liệt kê sessions
curl -s "http://localhost:8080/admin/realms/my-realm/users/$SERVICE_ACCOUNT_USER_ID/sessions" \
  -H "Authorization: Bearer $ADMIN_TOKEN" | jq .

# Revoke tất cả sessions của service account
curl -X POST \
  "http://localhost:8080/admin/realms/my-realm/users/$SERVICE_ACCOUNT_USER_ID/logout" \
  -H "Authorization: Bearer $ADMIN_TOKEN"

7.2 サービスアカウンティングのベストプラクティス

#練習する説明する
1有効期間の短いトークンサービス アカウントのアクセス トークンの有効期間を短く設定します (5 分)
2最低限の特権必要最小限の役割のみを割り当てる
3シークレットをローテーションするclient_secret を定期的に変更する
4トークンのキャッシュクライアント側でトークンをキャッシュし、有効期限が切れる前に更新します
5個別のクライアント各サービスは独自のクライアントを使用し、資格情報を共有しません
6ネットワークポリシーサービス間のネットワークアクセスを制限する
7監査ログサービス アカウント トークン リクエストのログを記録する

7.3 ゲートウェイでのレート制限

# Nginx rate limiting
http {
    # Định nghĩa rate limit zones
    # $binary_remote_addr: limit per IP
    limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;

    # Limit per authenticated user (từ JWT sub claim)
    limit_req_zone $http_x_user_id zone=user_limit:10m rate=30r/s;

    server {
        # Apply rate limiting cho API endpoints
        location /api/ {
            limit_req zone=api_limit burst=20 nodelay;
            limit_req zone=user_limit burst=50 nodelay;

            limit_req_status 429;

            # Custom error response cho rate limit
            error_page 429 = @rate_limited;
            # ... proxy_pass ...
        }

        location @rate_limited {
            default_type application/json;
            return 429 '{"error": "Too Many Requests", "message": "Rate limit exceeded. Please retry after a moment."}';
        }
    }
}

8. まとめ

ゲートウェイ複雑パフォーマンス最適な用途
Nginx + lua-resty-openidc中くらい非常に高い高トラフィックのカスタム ロジック
Nginx + OAuth2 プロキシ低い高い素早いセットアップ、簡単な認証
コング中くらい高いエンタープライズ、プラグイン エコシステム
トレイフィク低い高いクラウドネイティブ、Kubernetes

API Gateway + Keycloak 導入チェックリスト:

#カテゴリ状態
1ゲートウェイでの JWT 検証 (JWKS キャッシュ)☐
2ユーザーヘッダーをダウンストリームサービスに伝播する☐
3内部通信用のサービス アカウント☐
4IP ごとおよびユーザーごとのレート制限☐
5ゲートウェイでの CORS 構成☐
6ヘルスチェックエンドポイント (認証をバイパス)☐
7ゲートウェイでの TLS 終端☐
8ロギングとモニタリング☐
9クライアント シークレットのローテーション戦略☐
10トークンの有効期限と更新の処理☐

次のシリーズでは、Keycloakのクラスタリング、本番環境のデプロイメント、パフォーマンスのチューニングなどの高度なトピックを掘り下げていきます。