İçeriğe geç

Kiracı Ayrımı — A Bankasının Müşterisi B Bankasının Verisini Görmesin

Orta 8 dk Sık karşılaşılır

Önce şunu oku: Dinamik Sorgu — Filtre Ekranı Bir Kapıya Dönüşmesin

30 saniyede özet

Birçok bankaya hizmet veren bir platformda bütün satırlar aynı tablolardadır. Elle yazılan kiracı koşulu bir gün unutulur; @TenantId Hibernate sorgularını korur ama native SQL'i değil. Row-level security ayrımı verinin kendisine koyar.

Bir apartmanın bütün mektupları tek bir odaya geliyor. Postacı her mektupta daire numarasına bakıp doğru kutuya koyuyor. Bir gün bakmayı unutursa, birinin mektubu başkasının eline geçer.

  1. Bayt: Cüzdan platformumuz üç bankaya hizmet veriyor. Hepsi aynı veritabanında, her satırda tenant_id var.

  2. Sen: Her sorguya where tenant_id = :tenant yazıyoruz. Bugüne kadar sorun olmadı.

  3. Bayt: Yeni hareket arama ekranında o koşul yok! A Bankası'nın müşterisi B'nin havalelerini görüyor.

  4. Bayt: Koşulu her seferinde hatırlamak yerine, kutulara kilit takmamız gerekiyor.

Tek tablo, birçok banka

çok kiracılılıkTek bir uygulamanın birden çok müşteri kurumuna (kiracıya) hizmet vermesi. Her kiracı yalnızca kendi verisini görmelidir.Sözlükte gör → bir platformda bütün bankaların verisi çoğu zaman aynı tablolardadır. Hangi satırın kime ait olduğunu bir tenant_id sütunu söyler.

Ayrımı sağlayan şey her sorgudaki o koşuldur. Yüz sorgunun doksan dokuzunda yazılı olması yetmez; birinde unutulursa başka bankanın verisi görünür.

Kafam karıştı, daha basit anlat

Bütün bankalar aynı tabloda. Onları ayıran tek şey bir koşul; unutulursa veri sızar.

Hızlı kontrolBaşlangıç

Birçok bankaya hizmet veren bir platformda tek veritabanı var ve her satırda tenant_id duruyor. En büyük risk nedir?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Her sorguya elle where tenant_id = :tenant yazmanın zayıf yanı nedir?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Koşulu kim ekler?

Hibernate'in @TenantId alanı açık. Aylık rapor native SQL ile yazılmış ve kiracı koşulu yok. Rapor kimin satırlarını getirir? Cevabı göster

Bütün bankaların. @TenantId koşulu yalnızca Hibernate’in ürettiği sorgulara ekler. Native SQL doğrudan veritabanına gider ve bu korumadan geçmez.

Kilitli kutu: anahtar yalnızca seninkini açar.
Adım adım oku
  1. Bütün mektuplar tek bir odada.
  2. Postacı her mektupta daireye bakıyor.
  3. Bir gün bakmayı unuttu.
  4. Kilitli kutu: anahtar yalnızca seninkini açar.

Hibernate’te bir alana @TenantId koymak, koşulu hatırlama işini geliştiriciden alır. Kaydederken kiracı alana yazılır, HQL ve JPQL sorgularına koşul kendiliğinden eklenir.

row-level securityVeritabanının, sorgu ne olursa olsun yalnızca bir politikaya uyan satırları göstermesi. PostgreSQL'de CREATE POLICY ile tanımlanır.Sözlükte gör → ise bir adım daha ileri gider. Kural veritabanının içindedir: sorgu JPQL de olsa native SQL de olsa, yalnızca o kiracının satırları görünür.

Kafam karıştı, daha basit anlat

@TenantId Hibernate sorgularını korur. Row-level security her sorguyu korur.

Hızlı kontrolOrta

Hibernate'in @TenantId alanı ne yapar?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

@TenantId kullanılıyor. Koşulsuz bir native SQL rapor sorgusu ne getirir?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Kendin gör

A Bankası’nın müşterisi dört ekran açıyor

Tohum 200360

Oturum: A Bankası · tek veritabanı, her satırda tenant_id

  1. Hesaplarım (JPQL) · koşul elle yazılmış
  2. Kartlarım (JPQL) · koşul elle yazılmış
  3. Hareket arama (JPQL) · koşul yazılmamış
  4. Aylık rapor (native SQL) · koşul yazılmamış

✕ başka bankanın verisini gösteren ekran: 0

Hız
Adım 0

Şu an ne oldu?

Her sorguya elle: where tenant_id = :tenant

Her ekran bir sorgu çalıştırıyor. Sonuçta yalnızca A Bankası’nın satırları olmalı.

Görevler0/3

  • Unutulan bir koşul başka bankanın verisini göstersinaçık

    İpucu

    Elle koşul.

  • Yalnızca native SQL sızdırsınaçık

    İpucu

    Hibernate @TenantId.

  • Hiçbir ekran sızdırmasınaçık

    İpucu

    Veritabanında.

Olay günlüğü (0)

Henüz olay yok. Oynat veya adımla.

  1. Varsayılanla oynat. Koşullar elle yazılmış: hareket arama ve aylık rapor B Bankası’nın satırlarını da getirdi.
  2. “Hibernate @TenantId” seç. Hareket arama korundu; yalnızca native SQL rapor sızdırdı.
  3. “Row-level security” seç. Dört ekranın dördü de yalnızca A Bankası’nın verisini gösterdi.
Hızlı kontrolOrta

PostgreSQL row-level security ne sağlar?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Kiracı bilgisi istekte nereden alınmalı?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Tuzaklar

Tablonun sahibiyle bağlanmak. PostgreSQL’de row-level security politikaları tablonun sahibine varsayılan olarak uygulanmaz. Uygulamayı ayrı bir kullanıcıyla bağla ya da FORCE ROW LEVEL SECURITY kullan.

Kiracıyı bağlantıya bir kez vermek. Bağlantı havuzundaki bir bağlantı sonra başka bir bankanın isteğine gider. Kiracıyı her işlemin başında SET LOCAL ile ver; işlem bitince kendiliğinden silinir.

Kiracıyı kullanıcıdan almak. Kiracı bilgisi URL’den ya da istek gövdesinden değil, doğrulanmış oturumdan ya da token’dan gelir.

Kafam karıştı, daha basit anlat

Sahip olmayan kullanıcıyla bağlan, kiracıyı her işlemde ver, kiracıyı token’dan al.

Hızlı kontrolOrta

RLS kullanılıyor ama uygulama tabloların sahibi olan kullanıcıyla bağlanıyor. Ne olur?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.
Derinleş · Cüzdan platformu: üç banka, tek veritabanı 5 dosya · ~87 satır · ilk okumada atlayabilirsin
Proje dosyaları

wallet/src/main/java/com/bank/wallet/account/ Account.java Entity: @TenantId alanı; Hibernate kaydederken doldurur, sorgulara koşul ekler.

wallet/src/main/java/com/bank/wallet/account/Account.java
@Entity
@Table(name = "accounts")
public class Account {
@Id
private UUID id;
// Hibernate sets it on insert and adds "tenant_id = ?" to every HQL/JPQL query.
@TenantId
@Column(name = "tenant_id", nullable = false, updatable = false)
private String tenantId;
@Column(nullable = false)
private String iban;
@Column(nullable = false, precision = 19, scale = 2)
private BigDecimal balance;
protected Account() {
}
}

wallet/src/main/java/com/bank/wallet/tenant/ TokenTenantResolver.java Kiracı çözücü: kiracı, doğrulanmış token'daki bank alanından gelir.

wallet/src/main/java/com/bank/wallet/tenant/TokenTenantResolver.java
@Component
class TokenTenantResolver implements CurrentTenantIdentifierResolver<String>, HibernatePropertiesCustomizer {
@Override
public String resolveCurrentTenantIdentifier() {
// The tenant comes from the verified token, never from the URL or the body.
if (SecurityContextHolder.getContext().getAuthentication() instanceof JwtAuthenticationToken jwt) {
return jwt.getToken().getClaimAsString("bank");
}
throw new IllegalStateException("No tenant: request is not authenticated");
}
@Override
public boolean validateExistingCurrentSessions() {
return true;
}
@Override
public void customize(Map<String, Object> properties) {
properties.put(AvailableSettings.MULTI_TENANT_IDENTIFIER_RESOLVER, this);
}
}

wallet/src/main/resources/db/migration/ V9__row_level_security.sql Row-level security: politika veritabanında; uygulama kullanıcısı tablonun sahibi değil.

wallet/src/main/resources/db/migration/V9__row_level_security.sql
ALTER TABLE accounts ENABLE ROW LEVEL SECURITY;
ALTER TABLE accounts FORCE ROW LEVEL SECURITY; -- applies to the owner too
-- Every query, JPQL or native, sees only the current bank's rows.
CREATE POLICY accounts_by_bank ON accounts
USING (tenant_id = current_setting('app.tenant'))
WITH CHECK (tenant_id = current_setting('app.tenant'));
-- The application connects as wallet_app, which does not own the table.
GRANT SELECT, INSERT, UPDATE ON accounts TO wallet_app;

wallet/src/main/java/com/bank/wallet/tenant/ TenantTransactionListener.java Her işlemde kiracı: SET LOCAL, işlem bitince silinir.

wallet/src/main/java/com/bank/wallet/tenant/TenantTransactionListener.java
@Component
class TenantTransactionListener {
private final JdbcTemplate jdbc;
private final TokenTenantResolver tenants;
TenantTransactionListener(JdbcTemplate jdbc, TokenTenantResolver tenants) {
this.jdbc = jdbc;
this.tenants = tenants;
}
/** Called at the start of every transaction; SET LOCAL ends with it. */
void bindTenant() {
jdbc.queryForObject("SELECT set_config('app.tenant', ?, true)", String.class,
tenants.resolveCurrentTenantIdentifier());
}
}

wallet/src/test/java/com/bank/wallet/ TenantIsolationTest.java Test: iki bankanın verisi yüklenir, A'nın oturumuyla B'nin hiçbir satırı görünmemeli.

wallet/src/test/java/com/bank/wallet/TenantIsolationTest.java
@SpringBootTest
@Testcontainers
class TenantIsolationTest {
@Autowired
TransactionSearch search;
@Test
@WithBank("A")
void a_user_of_bank_a_never_sees_bank_b() {
TestData.loadTwoBanks("A", "B");
assertThat(search.find("havale"))
.isNotEmpty()
.allSatisfy(tx -> assertThat(tx.tenantId()).isEqualTo("A"));
}
}

Kendini sına

Şimşek turu1/4

Elle yazılan kiracı koşulu bir gün bir sorguda unutulabilir.

Soru 1/3İleri

Bağlantı havuzunda RLS için kiracı bilgisi bağlantıya nasıl verilir?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Aklında kalacak üç şey

  1. 1 Paylaşılan tablolarda kiracıları ayıran şey bir koşuldur. Yüz sorgudan birinde unutulması bir sızıntı için yeter.
  2. 2 Hibernate'in @TenantId alanı, Hibernate'in ürettiği sorgulara koşulu kendisi ekler. Native SQL ise bu korumadan geçmez.
  3. 3 Row-level security ayrımı veritabanına koyar: sorgu ne olursa olsun yalnızca o kiracının satırları görünür.
0/4 kart bu dersten toplandı