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-openidc | Lua 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 | 内部通信用のサービス アカウント | ☐ |
| 4 | IP ごとおよびユーザーごとのレート制限 | ☐ |
| 5 | ゲートウェイでの CORS 構成 | ☐ |
| 6 | ヘルスチェックエンドポイント (認証をバイパス) | ☐ |
| 7 | ゲートウェイでの TLS 終端 | ☐ |
| 8 | ロギングとモニタリング | ☐ |
| 9 | クライアント シークレットのローテーション戦略 | ☐ |
| 10 | トークンの有効期限と更新の処理 | ☐ |
次のシリーズでは、Keycloakのクラスタリング、本番環境のデプロイメント、パフォーマンスのチューニングなどの高度なトピックを掘り下げていきます。