JWT ile Çoklu Servis Kimlik Doğrulama: Mikroservis Mimarisi
Mikroservis mimarisine geçtiğinizde ilk kafanızı ağrıtan şeylerden biri kimlik doğrulamadır. Monolitik uygulamada session tabanlı auth yeterliydi, bir veritabanına bakıyordunuz, kullanıcı giriş yapmış mı değil mi anlıyordunuz. Ama artık elimizde onlarca servis var: kullanıcı servisi, sipariş servisi, ödeme servisi, bildirim servisi… Her biri ayrı bir container’da çalışıyor, bazıları farklı dillerde yazılmış. Bu servislerin birbiriyle ve dış dünyayla güvenli iletişim kurması için JWT tam anlamıyla kurtarıcı oluyor.
Bu yazıda JWT’yi sadece teorik olarak değil, gerçek dünya mikroservis senaryolarında nasıl kullanacağınızı, servisler arası iletişimi nasıl güvenli hale getireceğinizi ve production’da başınıza gelebilecek sorunları nasıl önleyeceğinizi anlatacağım.
JWT Nedir, Neden Mikroservisler İçin İdeal?
JWT (JSON Web Token), üç parçadan oluşan, imzalanmış bir token formatıdır: header, payload ve signature. Base64URL ile encode edilmiş bu üç parça nokta ile ayrılır. Önemli olan nokta şu: JWT stateless’tır. Yani token’ı doğrulamak için merkezi bir veritabanına sorgu atmak zorunda değilsiniz.
Mikroservis mimarisinde bu inanılmaz değerlidir. Sipariş servisi, kullanıcının kimliğini doğrulamak için kullanıcı servisine HTTP isteği atmak yerine elindeki public key ile token’ı kendi başına doğrulayabilir. Bu hem gecikmeyi azaltır hem de servisler arası bağımlılığı minimuma indirir.
Payload içinde istediğiniz claim’leri taşıyabilirsiniz:
# JWT'yi decode etmek için basit bir one-liner
echo "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyXzEyMyIsInJvbGUiOiJhZG1pbiIsInNlcnZpY2VzIjpbIm9yZGVyIiwicGF5bWVudCJdLCJpYXQiOjE3MDAwMDAwMDAsImV4cCI6MTcwMDAwMzYwMH0.signature" |
cut -d'.' -f2 |
base64 -d 2>/dev/null |
python3 -m json.tool
Temel Mimari: Auth Servisi Merkezi, Doğrulama Dağıtık
Klasik yaklaşım şöyle çalışır: Merkezi bir Auth servisi token üretir, diğer tüm servisler bu token’ları bağımsız olarak doğrular.
[Client] --> [API Gateway] --> [Auth Service] (token üretir)
|
v
[Order Service] (token doğrular)
[Payment Service] (token doğrular)
[Notification Service] (token doğrular)
Bu yaklaşım için asimetrik şifreleme (RS256 veya ES256) kullanmak zorundasınız. Simetrik HMAC256 kullanırsanız tüm servislerinizin secret key’i bilmesi gerekir, bu hem güvenlik açığı hem de key rotation kabusudur.
Auth Servisini Ayağa Kaldırmak
Auth servisinizi Node.js ile yazıyorsanız basit bir örnek:
# Gerekli paketleri yükle
npm install jsonwebtoken express bcrypt dotenv
# RSA key pair oluştur
mkdir -p /opt/auth-service/keys
cd /opt/auth-service/keys
# Private key (sadece auth serviste olacak)
openssl genrsa -out private.pem 2048
# Public key (tüm servislere dağıtılacak)
openssl rsa -in private.pem -pubout -out public.pem
# İzinleri sıkılaştır
chmod 600 private.pem
chmod 644 public.pem
# auth-service/src/token.js için temel token üretme mantığı
cat > /opt/auth-service/src/token.js << 'EOF'
const jwt = require('jsonwebtoken');
const fs = require('fs');
const privateKey = fs.readFileSync('/opt/auth-service/keys/private.pem');
const publicKey = fs.readFileSync('/opt/auth-service/keys/public.pem');
function generateToken(userId, roles, allowedServices) {
const payload = {
sub: userId,
roles: roles,
services: allowedServices, // Hangi servislere erişebileceği
iss: 'auth.mycompany.internal',
aud: allowedServices,
iat: Math.floor(Date.now() / 1000),
jti: require('crypto').randomUUID() // Unique token ID
};
return jwt.sign(payload, privateKey, {
algorithm: 'RS256',
expiresIn: '1h'
});
}
function verifyToken(token, audience) {
return jwt.verify(token, publicKey, {
algorithms: ['RS256'],
issuer: 'auth.mycompany.internal',
audience: audience
});
}
module.exports = { generateToken, verifyToken };
EOF
Servisler Arası İletişimde JWT
Burada iki farklı senaryo var ve ikisini karıştırmamak lazım:
User-to-Service: Kullanıcı bir istek atıyor, bu istek birden fazla servise ulaşıyor.
Service-to-Service: Bir servis başka bir servisi tetikliyor, ortada kullanıcı yok.
User-to-Service Senaryosu
API Gateway’iniz token’ı alır, doğrular ve downstream servislere forward eder. Servislerin de kendi içlerinde doğrulama yapması gerekir, çünkü API Gateway’e güvenmek yetmez. Defense in depth prensibi.
# Python'da servis tarafı middleware örneği (FastAPI)
cat > /opt/order-service/middleware/auth.py << 'EOF'
import jwt
from fastapi import HTTPException, Security
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
PUBLIC_KEY_PATH = "/opt/shared-keys/public.pem"
with open(PUBLIC_KEY_PATH, 'r') as f:
PUBLIC_KEY = f.read()
security = HTTPBearer()
def verify_jwt(credentials: HTTPAuthorizationCredentials = Security(security)):
token = credentials.credentials
try:
payload = jwt.decode(
token,
PUBLIC_KEY,
algorithms=["RS256"],
issuer="auth.mycompany.internal",
audience="order" # Bu servisin audience değeri
)
# Role kontrolü
if "order:read" not in payload.get("roles", []):
raise HTTPException(status_code=403, detail="Yetersiz yetki")
return payload
except jwt.ExpiredSignatureError:
raise HTTPException(status_code=401, detail="Token süresi dolmuş")
except jwt.InvalidAudienceError:
raise HTTPException(status_code=403, detail="Bu servise erişim yetkisi yok")
except jwt.JWTError as e:
raise HTTPException(status_code=401, detail=f"Geçersiz token: {str(e)}")
EOF
Service-to-Service Senaryosu
Sipariş servisi ödeme servisini çağırdığında, kullanıcının token’ını forward etmek kötü bir pratiktir. Bunun yerine her servis kendi “service account” token’ını kullanmalı.
# Servis hesapları için özel token üretme scripti
cat > /opt/scripts/generate-service-token.sh << 'EOF'
#!/bin/bash
SERVICE_NAME=$1
ALLOWED_SERVICES=$2
if [ -z "$SERVICE_NAME" ]; then
echo "Kullanim: $0 <service-name> <allowed-services-csv>"
exit 1
fi
# Python ile service token üret
python3 << PYEOF
import jwt
import time
import json
import uuid
with open('/opt/auth-service/keys/private.pem', 'r') as f:
private_key = f.read()
payload = {
"sub": f"service:${SERVICE_NAME}",
"type": "service_account",
"roles": ["internal-service"],
"services": "${ALLOWED_SERVICES}".split(",") if "${ALLOWED_SERVICES}" else [],
"iss": "auth.mycompany.internal",
"aud": "${ALLOWED_SERVICES}".split(",") if "${ALLOWED_SERVICES}" else [],
"iat": int(time.time()),
"exp": int(time.time()) + 3600,
"jti": str(uuid.uuid4())
}
token = jwt.encode(payload, private_key, algorithm='RS256')
print(token)
PYEOF
EOF
chmod +x /opt/scripts/generate-service-token.sh
# Örnek kullanım:
# ./generate-service-token.sh order-service payment,notification
Token Dağıtımı: Public Key’leri Servislere Nasıl Ulaştırırsınız?
Bu kısmı es geçen çok mühendis gördüm, sonra production’da başları yanıyor. Public key’leri servislere dağıtmanın birkaç yolu var:
Yöntem 1: JWKS Endpoint
En temiz yöntem bu. Auth servisiniz bir JWKS (JSON Web Key Set) endpoint’i sunar, diğer servisler public key’i bu endpoint’ten çeker.
# Nginx ile basit JWKS endpoint
cat > /etc/nginx/conf.d/jwks.conf << 'EOF'
server {
listen 8080;
server_name auth.mycompany.internal;
location /.well-known/jwks.json {
alias /opt/auth-service/keys/jwks.json;
add_header Content-Type application/json;
add_header Cache-Control "public, max-age=3600";
}
}
EOF
# JWKS dosyasını oluştur
cat > /opt/scripts/generate-jwks.py << 'EOF'
#!/usr/bin/env python3
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.backends import default_backend
import base64
import json
import struct
with open('/opt/auth-service/keys/public.pem', 'rb') as f:
public_key = serialization.load_pem_public_key(f.read(), backend=default_backend())
public_numbers = public_key.public_key().public_numbers() if hasattr(public_key, 'public_key') else public_key.public_numbers()
def int_to_base64url(n):
byte_length = (n.bit_length() + 7) // 8
n_bytes = n.to_bytes(byte_length, byteorder='big')
return base64.urlsafe_b64encode(n_bytes).rstrip(b'=').decode('ascii')
jwks = {
"keys": [{
"kty": "RSA",
"use": "sig",
"kid": "key-v1-2024",
"alg": "RS256",
"n": int_to_base64url(public_numbers.n),
"e": int_to_base64url(public_numbers.e)
}]
}
with open('/opt/auth-service/keys/jwks.json', 'w') as f:
json.dump(jwks, f, indent=2)
print("JWKS dosyasi olusturuldu")
EOF
python3 /opt/scripts/generate-jwks.py
Yöntem 2: Kubernetes Secret ile Dağıtım
K8s ortamındaysanız public key’i bir Secret olarak saklayıp her servise mount edebilirsiniz:
# Public key'i K8s secret olarak kaydet
kubectl create secret generic jwt-public-key
--from-file=public.pem=/opt/auth-service/keys/public.pem
-n production
# Deployment'a mount et
cat > /tmp/order-service-patch.yaml << 'EOF'
spec:
template:
spec:
containers:
- name: order-service
volumeMounts:
- name: jwt-public-key
mountPath: /opt/keys
readOnly: true
volumes:
- name: jwt-public-key
secret:
secretName: jwt-public-key
EOF
kubectl patch deployment order-service
-n production
--patch-file /tmp/order-service-patch.yaml
Token Yenileme ve Revoke Stratejisi
JWT’nin stateless doğası bir paradoks yaratır: token’ı veritabanına sormadan doğrulayabilirsiniz ama token’ı expire etmeden önce geçersiz de kılamazsınız. Kullanıcı şifresini değiştirdi, hesabı askıya alındı, ama elindeki token hala bir saat geçerli.
Bunu çözmenin birkaç yolu var:
Access Token + Refresh Token kombinasyonu: Access token’ı kısa tutun (5-15 dakika), refresh token’ı uzun tutun ve bunu veritabanında saklayın.
Token blacklist: Redis’te geçersiz token ID’lerini saklayın.
# Redis tabanlı token blacklist
cat > /opt/auth-service/src/blacklist.sh << 'EOF'
#!/bin/bash
# Token revoke scripti - manuel müdahale için
REDIS_HOST=${REDIS_HOST:-"redis.mycompany.internal"}
TOKEN_JTI=$1
EXPIRE_SECONDS=${2:-3600}
if [ -z "$TOKEN_JTI" ]; then
echo "Kullanim: $0 <token-jti> [expire-seconds]"
exit 1
fi
redis-cli -h $REDIS_HOST SET "blacklist:$TOKEN_JTI" "revoked" EX $EXPIRE_SECONDS
if [ $? -eq 0 ]; then
echo "Token $TOKEN_JTI basariyla blacklist'e alindi (TTL: ${EXPIRE_SECONDS}s)"
else
echo "HATA: Token blacklist'e alinamadi!"
exit 1
fi
EOF
chmod +x /opt/auth-service/src/blacklist.sh
# Redis'te blacklist kontrolü için Python middleware eki
cat >> /opt/order-service/middleware/auth.py << 'EOF'
import redis
redis_client = redis.Redis(host='redis.mycompany.internal', port=6379, decode_responses=True)
def check_blacklist(jti: str) -> bool:
"""True dönerse token blacklist'te, erişimi reddet"""
return redis_client.exists(f"blacklist:{jti}") > 0
EOF
Production’da Başınıza Gelebilecek Sorunlar
Teorik bilgi yeterli değil, production’da gördüğüm gerçek problemleri paylaşayım.
Saat Senkronizasyonu
JWT doğrulaması zaman damgasına dayanır. Servislerinizin saatleri birbirinden farklıysa geçerli token’lar “süresi dolmuş” görünebilir.
# Tüm node'larda NTP durumunu kontrol et
for host in auth-service order-service payment-service; do
echo "=== $host ==="
ssh $host "chronyc tracking | grep -E 'System time|RMS offset'"
done
# Maksimum kabul edilebilir kayma 500ms, fazlası alarm vermeli
# Monitoring için Prometheus alert örneği:
cat > /opt/monitoring/alerts/ntp-drift.yaml << 'EOF'
groups:
- name: jwt-prerequisites
rules:
- alert: NTPDriftHigh
expr: abs(node_timex_offset_seconds) > 0.5
for: 2m
labels:
severity: warning
annotations:
summary: "NTP kayması yüksek - JWT doğrulaması etkilenebilir"
description: "{{ $labels.instance }} üzerinde NTP kayması {{ $value }}s"
EOF
Key Rotation
6 ayda bir veya güvenlik ihlali durumunda private key’inizi değiştirmeniz gerekir. Bunu yaparken downtime yaşamamalısınız.
#!/bin/bash
# /opt/scripts/rotate-jwt-keys.sh
# Graceful key rotation scripti
KEY_DIR="/opt/auth-service/keys"
DATE=$(date +%Y%m%d)
echo "JWT Key Rotation Basliyor - $DATE"
# 1. Yeni key pair olustur
openssl genrsa -out $KEY_DIR/private-${DATE}.pem 2048
openssl rsa -in $KEY_DIR/private-${DATE}.pem -pubout -out $KEY_DIR/public-${DATE}.pem
# 2. JWKS dosyasini IKISI de icine alacak sekilde guncelle
# (Gecis suresi boyunca eski token'lar da gecerli olmali)
python3 /opt/scripts/generate-jwks.py --include-old-key
# 3. Auth servisi yeni key'i kullanmaya baslasın
ln -sf $KEY_DIR/private-${DATE}.pem $KEY_DIR/private.pem
ln -sf $KEY_DIR/public-${DATE}.pem $KEY_DIR/public.pem
# 4. Auth servisi restart (graceful)
systemctl reload auth-service || kubectl rollout restart deployment/auth-service -n production
# 5. 2 saat bekle (eski token'larin expire olmasi icin)
echo "Eski token'larin expire olmasi bekleniyor (2 saat)..."
sleep 7200
# 6. JWKS'ten eski key'i kaldir
python3 /opt/scripts/generate-jwks.py --remove-old-key
echo "Key rotation tamamlandi"
Token Boyutu
Payload’a aşırı veri doldurmak her HTTP isteğini şişirir. Her request header’ında bu token taşınıyor, CDN ve proxy loglarına düşüyor.
# Token boyutunu kontrol et
TOKEN=$(cat /tmp/test-token.txt)
TOKEN_SIZE=$(echo -n $TOKEN | wc -c)
echo "Token boyutu: ${TOKEN_SIZE} bytes"
# 1KB'ı geçen token'lar için uyarı
if [ $TOKEN_SIZE -gt 1024 ]; then
echo "UYARI: Token boyutu ${TOKEN_SIZE} bytes, optimize edilmeli!"
echo "Payload icerigi:"
echo $TOKEN | cut -d'.' -f2 | base64 -d 2>/dev/null | python3 -m json.tool
fi
Monitoring ve Debugging
# JWT ile ilgili hataları merkezi olarak toplamak için
# Fluentd/Filebeat filter örneği - önemli alanları çıkar
cat > /opt/logging/jwt-filter.conf << 'EOF'
<filter microservices.**>
@type record_transformer
enable_ruby true
<record>
jwt_error ${record.dig("error", "type")}
jwt_subject ${
begin
require 'base64'
require 'json'
token = record["headers"]["authorization"].to_s.split(" ").last
payload = JSON.parse(Base64.decode64(token.split(".")[1] + "==")) rescue {}
payload["sub"]
rescue
"unknown"
end
}
service_name ${tag_parts[1]}
</record>
</filter>
EOF
# Auth hata metriklerini Grafana'da izlemek için query örneği:
# sum by (service_name, jwt_error) (
# rate(http_requests_total{status=~"401|403"}[5m])
# )
Sıkça Yapılan Hatalar
Pratikte gördüğüm ve sizi production’da uğraştıracak hatalar:
- Algorithm confusion saldırısı: Doğrulama tarafında
algorithms=["RS256"]olarak sabitleyin, aslaalgorithms=["*"]veya library’nin default değerine güvenmeyin. Bazı eski kütüphaneler “none” algoritmasını kabul ediyor.
- Token’ı cookie’de saklamak vs Authorization header: Mobile client’lar için header daha uygun, web için HttpOnly secure cookie daha güvenli. İkisini karıştırmayın.
- Tüm servislere aynı audience izni vermek: Her serviste
audiencekontrolü yapın. Ödeme servisine gidecek token farklı, sipariş servisine gidecek token farklı olmalı.
- Secret key’i environment variable olarak koyup loglamak: Private key’i asla process argümanı olarak geçirmeyin, dosyadan okuyun.
- Refresh token’ı JWT yapmak: Refresh token’ı opaque (rastgele string) yapın ve veritabanında saklayın. JWT olursa revoke edemezsiniz.
Sonuç
JWT tabanlı mikroservis kimlik doğrulaması doğru uygulandığında hem güvenli hem de performanslıdır. Asimetrik şifreleme kullanarak auth servisinizi doğrulama sürecinden izole edin, her servise sadece ihtiyacı olan audience iznini verin ve key rotation stratejinizi deployment’tan önce test edin.
En önemli pratik tavsiyem: token boyutunu küçük tutun, saat senkronizasyonunu ihmal etmeyin ve her servisin kendi doğrulamasını yapmasını sağlayın. API Gateway’e kör güven, savunma mimarisinin en büyük düşmanıdır.
Production’a çıkmadan önce şunları mutlaka test edin: süresi dolmuş token davranışı, yanlış audience ile istek, revoke edilmiş token, key rotation sırasında geçiş dönemi. Bu senaryolar test ortamında can sıkıcı, production’da ise çok daha pahalıdır.
