Shaxsiy ma’lumotlarga kirish jurnallarini xavfsiz qayd etish

Shaxsiy ma’lumotlarga kirish jurnallarini xavfsiz qayd etish

BanJangNote backendini ishlab chiqish davomida bir nechta yo‘nalishda ishladim, ammo shaxsiy ma’lumotlarga oid audit jurnali tizimi haqida ko‘p o‘yladim.

Avvaliga ishga yollash va ish izlash kabi asosiy biznes funksiyalarini yaratish bilan shu qadar band edimki, boshqa narsalar haqida o‘ylashga deyarli vaqtim bo‘lmadi. Ammo xizmat funksiyalarini ishlab chiqish yakuniga yetib borar ekan, ortga surib kelgan vazifamni bajarish — Shaxsga doir ma’lumotlarni himoya qilish to‘g‘risidagi qonunga rioya etish uchun tegishli audit jurnali tizimini joriy qilish vaqti keldi. Xususan, BanJangNote ishga qabul qilish jarayonida katta hajmdagi shaxsiy ma’lumotlar almashiladigan platforma, shu sababli jurnal fayllariga bir nechta qator yozib qo‘yishning o‘zi yetarli emas edi.

Ishga kirishganimdan keyin qonuniy talablar qat’iy ekanini angladim. Tizimda 300 dan ortiq API bor edi, ularning har biriga jurnal yozuvlarini qo‘shishni eslab qolishni odamlarga topshirish esa tizim rivojlanishda davom etar ekan, muqarrar ravishda ayrim yozuvlar tushib qolishiga olib kelardi. Shuning uchun yuqori trafik paytida API javob vaqtini saqlab qolgan holda audit jurnallaridagi yozuvlar tushib qolishining oldini oladigan jarayonni qanday yaratganim bilan bo‘lishmoqchiman.

Qonuniy qoidalarni kod talablariga aylantirish

Shaxsga doir ma’lumotlarni himoya qilish to‘g‘risidagi qonunning 29-moddasi tashkilotlardan tizimga kirish yozuvlarini saqlash va ularning o‘zgartirilishi yoki soxtalashtirilishining oldini olish uchun xavfsizlik choralarini ko‘rishni talab qiladi. Batafsil talablar Shaxsga doir ma’lumotlar xavfsizligini ta’minlash standartlari to‘g‘risidagi bildirishnomaning 8-moddasida keltirilgan.

Qonunga ko‘ra, shaxsiy ma’lumotlarga kirish jurnallarida qayd etilishi kerak bo‘lgan ma’lumotlar quyidagilardan iborat.

Birinchidan, kirish jurnalida doimo bo‘lishi shart bo‘lgan beshta ma’lumot bor.

  • Tizimga kim kirganini ko‘rsatuvchi identifikator (ID yoki hisob qaydnomasi)

  • Kirish sanasi va vaqti

  • Tizimga kirilgan IP manzil

  • Tizimda bajarilgan amalni ko‘rsatuvchi vazifa, masalan, ko‘rish, o‘zgartirish, o‘chirish yoki yuklab olish

  • Ma’lumotlariga kirilgan ma’lumotlar subyektining identifikatori

Bundan tashqari, yana bir nechta talab bor.

  • 50 000 yoki undan ortiq kishiga oid ma’lumotlar yoxud noyob identifikatsiya ma’lumotlari qayta ishlanganda, yozuvlar kamida ikki yil saqlanishi kerak.

  • Yozuv saqlanganidan so‘ng, hech kim uni o‘zboshimchalik bilan o‘zgartira olmasligi kerak.

  • Shaxsiy ma’lumotlar Excel jadvali yoki boshqa fayl sifatida yuklab olinganda, yuklab olish sababi qayd etilishi shart.

  • Ommaviy so‘rovlar yoki noodatiy kirish holatlarini muntazam kuzatib borish imkoniyati bo‘lishi kerak.

Talablarning o‘zi aniq edi, ammo ularni kodda qanday joriy etish haqida o‘ylaganimda, bir nechta to‘siqqa duch keldim.

Tizimni yaratishda duch kelgan muammolarim

Spring muhitida audit jurnali uchun odatda birinchi bo‘lib interceptorlar va AOP xayolga keladi. Loyihalashning dastlabki bosqichida so‘rovlarni interceptor yordamida tizimning kirish qismida tutib, qayd etishni ko‘rib chiqdim. Ammo loyihani batafsil ishlab chiqishni boshlaganimda, bu ish o‘ylaganchalik oddiy emasligi ma’lum bo‘ldi.

1. Yuzlab endpoint bor. Ularning barchasini kim ham kuzatib bora oladi?

BanJangNote’da 300 dan ortiq API bor edi. Ular brigadirlar, guruh rahbarlari, administratorlar va boshqa rollarga ajratilgan edi. Dasturchilar har safar yangi API yaratganida «Bu shaxsiy ma’lumotlar bilan ishlaydi, demak jurnalga yozishim kerak», deb eslab qolishiga umid qilish haqiqatga to‘g‘ri kelmaydi. Ishlab chiqish bilan band bo‘lgan paytda nimanidir o‘tkazib yuborish oson, kodni ko‘rib chiqishning o‘zi esa bunday kamchiliklarni aniqlashda aniq cheklovlarga ega. Buning teskari muammosi ham bor edi: mavjud API o‘chirilganda yoki uning yo‘li o‘zgartirilganda, eski sozlamalari qolib ketishi mumkin edi.

2. Interceptor kimning ma’lumotlariga kirilganini bilmaydi

Interceptor mijoz yuborgan URL va so‘rov sarlavhalarini, shuningdek tizimga kirgan foydalanuvchining tokenini osongina aniqlay oladi. Ammo qonunda talab qilingan «ma’lumotlar subyekti identifikatori»ni, boshqacha aytganda, «bu odam kimning shaxsiy ma’lumotlarini ko‘rdi?» degan savolga javobni aniqlay olmaydi. Masalan, API muayyan ish joyidagi brigadirlar ro‘yxatini oladigan bo‘lsa, natijadagi 10 nafar xodimning a’zo ID’lari faqat xizmat mantiqi ma’lumotlar bazasidan so‘rov bajarganidan keyin ma’lum bo‘ladi. Aslida, ma’lumotlar veb qatlamidagi interceptor bilan biznes mantiqi qatlami o‘rtasida yo‘qolib qolayotgan edi.

3. Har safar ma’lumot ko‘rilganda bajariladigan INSERT’ni ma’lumotlar bazasi ko‘tara oladimi?

Qurilish maydonlarining xususiyatidan kelib chiqib, ertalab ishga kelish vaqtida so‘rovlar keskin ko‘payishini kutgandik. Shaxsiy ma’lumotlar bor ro‘yxat so‘ralgan har safar audit jurnali jadvaliga sinxron INSERT bajarsak, bu ma’lumotlar bazasi ulanishlar puliga darhol bosim o‘tkazishi mumkin edi. Natijada, faqat audit jurnallarini yozib borayotganimiz sababli muhim biznes API’lar sekinlashib qolishi ehtimoli bor edi. Shuningdek, audit jurnali yozilayotganda ma’lumotlar bazasidagi vaqtinchalik bloklanish yoki kechikish sababli foydalanuvchilarning ish e’lonlarini ko‘rish yoki ish soatlarini tekshirish kabi amallari bekor bo‘lib qolmasligini ta’minlashimiz kerak edi.

4. Jurnallarning o‘zi sabab bo‘ladigan ikkilamchi sizib chiqishlar

Auditni puxta yuritish maqsadida butun javob obyektlarini serializatsiya qilib, jurnal jadvaliga yozsak, jadvalning o‘zi ochiq matn ko‘rinishidagi shaxsiy identifikatsiya raqamlari va bank hisob raqamlari saqlanadigan xavfli omborga aylanadi. Ma’lumotlarning o‘zini yashirib, faqat ismlar, telefon raqamlari yoki hisob ma’lumotlari kabi qaysi turdagi maydonlarga kirilganini bildiruvchi metama’lumotlarni qayd etishimiz kerak edi.

Muammolarni hal qilish

1. Audit yozuvlari tushib qolishining oldini olish: har bir endpointni test kodi orqali ro‘yxatdan o‘tkazishni talab qilish

Odamlarning xotirasiga tayanmaslikning yagona yo‘li endpoint ro‘yxatdan o‘tkazilmagan har qanday holatda yig‘ish jarayonining o‘zi muvaffaqiyatsiz tugashini ta’minlash, degan qarorga keldim.

Avval ro‘yxatga olish klassini yaratdim va tizimdagi har bir endpointni shaxsiy ma’lumotlar bilan ishlaydigan yoki ishlamaydigan sifatida ro‘yxatdan o‘tkazishni talab qildim. Shaxsiy ma’lumotlar bilan ishlamaydigan endpointlarni ham tasniflamasdan qoldirib bo‘lmasdi: ular sog‘liqni tekshirish yoki foydalanuvchining o‘z ma’lumotlarini yangilash kabi sabab kodlari bilan ro‘yxatdan o‘tkazilishi kerak edi.

// 대상 엔드포인트 등록
AccessLogTargetRegistry.register(
    "/feature/cm/find-somebodys-detail/fetch",
    true,
    AccessTaskType.Read,
    List.of(
        PersonalDataItem.Name,
        PersonalDataItem.Phone,
        PersonalDataItem.BirthDate,
        PersonalDataItem.Certificate
    )
);

// 비대상 엔드포인트도 사유와 함께 명시
AccessLogNonTargetDeclarations.registerNonTarget(
    "/actuator/health", 
    NonTargetReason.HealthCheck
);

Keyin Spring’ning metadata reader’idan foydalanib, loyihadagi har bir @RestController’ning URL yo‘llarini to‘playdigan test yozdim.

class AccessLogTargetRegistryArchitectureTest {

    @Test
    @DisplayName("모든 Resource 엔드포인트가 레지스트리에 등록되어 있어야 한다")
    void allRoleResourceEndpointsMustBeRegistered() throws Exception {
        Set<String> discoveredEndpoints = scanAllRoleResourceEndpoints();

        // 레지스트리에 대상이든 비대상이든 등록되지 않은 엔드포인트를 찾는다
        List<String> missing = discoveredEndpoints.stream()
                .filter(ep -> !AccessLogTargetRegistry.contains(ep))
                .toList();

        // 하나라도 빠져 있으면 테스트 실패
        assertTrue(missing.isEmpty(),
                "AccessLogTargetRegistry에 등록되지 않은 엔드포인트가 있습니다: " + missing);
    }

    @Test
    @DisplayName("코드에서 삭제된 엔드포인트가 레지스트리에 남아있으면 안 된다")
    void registryMustNotContainStaleEndpoints() throws Exception {
        Set<String> realEndpoints = scanAllRoleResourceEndpoints();
        List<String> stale = AccessLogTargetRegistry.getAll().keySet().stream()
                .filter(ep -> !realEndpoints.contains(ep))
                .sorted()
                .toList();
        assertTrue(stale.isEmpty(), "실제 코드에 없는 엔드포인트가 레지스트리에 남아있습니다: " + stale);
    }
}

Shunday qilib, dasturchi yangi API yaratib, uni ro‘yxatdan o‘tkazmasa, regressiya testlari va CI jarayoni audit yozuvi yetishmayotganini aniqlaydi. Ro‘yxatda o‘chirilgan API qolib ketgan bo‘lsa ham, bu aniqlanadi. Shu yo‘l bilan dasturchilardan endpointlarni alohida kuzatib borishni talab qilmasdan, ularning ro‘yxati doimo 100% dolzarb bo‘lishini ta’minladik.

2. Biznes kontekstini ThreadLocal orqali uzatish

Interceptor va biznes mantiqi o‘rtasidagi uzilishni ThreadLocal asosidagi kontekst (AccessLogContext) yaratish orqali bartaraf etdim.

So‘rov kelganida interceptor preHandle vaqtida AccessLogContext’ga so‘rov yuboruvchisi, mijozning IP manzili va so‘rov vaqti kabi ma’lumotlarni o‘rnatadi.

Keyin so‘rov controllerdan o‘tib, xizmat mantiğiga yetgach, xizmat ma’lumotlar bazasidan tegishli ma’lumotlarni oladi va kontekstga ma’lumotlar subyekti hamda kirilgan shaxsiy ma’lumotlarni qayd etuvchi metodni chaqiradi.

@Service
@Transactional
public class WorkLdrFlow {
    //
    public WorkCrewMemberDetailRdo findMemberDetail(FindWorkCrewMemberDetailUdo udo) {
        //
        // 조회 대상이 된 정보주체 식별자를 컨텍스트(AccessLogContext)에 전달
        AccessLogContext.reportTargetSubject(udo.getMemberId(), AccessLogSubjectKind.Member);

        // 실제 업무 로직 수행
        return workCrewTask.findMemberDetail(udo.getMemberId());
    }
}

Bu biznes mantiqiga bir qator kod qo‘shish orqali kontekstga ma’lumot yozish imkonini beradi va murakkab jurnal yuritish mantiqining xizmat qatlamiga aralashib ketishiga yo‘l qo‘ymaydi.

So‘rov yakunlanganda esa interceptor ichidagi afterCompletion ushbu ma’lumotlarni birlashtirib, yakuniy jurnal obyektini yaratadi.

3. Asinxron paketlash orqali javob tezligini saqlash

Biznes API’larning javob vaqtini saqlab qolish uchun interceptor so‘rovlarni ma’lumotlar bazasiga bevosita yubormaydi. Buning o‘rniga, obyektlarni xotiradagi buferga (AccessLogBatchAccumulator) joylab, darhol javob qaytaradi.

@Component("accessLogBatchAccumulator")
@RequiredArgsConstructor
public class AccessLogBatchAccumulator {

    private final AccessLogBulkInsertStore bulkInsertStore;
    private final List<AccessLogCdo> buffer = new ArrayList<>();
    private final Object bufferLock = new Object();

    private int batchSize = 200; // 200건 기준
    private long flushIntervalMs = 1000; // 1초 주기
    private volatile long lastFlushTime = System.currentTimeMillis();

    public void add(AccessLogCdo cdo) {
        if (cdo == null) return;
        boolean shouldFlush = false;

        // 버퍼에 담을 때만 빠르게 락을 잡고 빠진다
        synchronized (bufferLock) {
            buffer.add(cdo);
            if (buffer.size() >= batchSize) {
                shouldFlush = true;
            }
        }

        // 실제 무거운 DB 작업은 락 바깥에서 수행
        if (shouldFlush) {
            drainAndInsert();
        }
    }

    @Scheduled(fixedDelayString = "1000")
    public void scheduledFlush() {
        // 200건이 안 차더라도 1초가 지나면 남아있는 로그를 비워준다
        boolean hasItems;
        long elapsed;
        synchronized (bufferLock) {
            hasItems = !buffer.isEmpty();
            elapsed = System.currentTimeMillis() - lastFlushTime;
        }
        if (hasItems && elapsed >= flushIntervalMs) {
            drainAndInsert();
        }
    }

    private void drainAndInsert() {
        List<AccessLogCdo> toInsert;
        synchronized (bufferLock) {
            if (buffer.isEmpty()) return;
            toInsert = new ArrayList<>(buffer);
            buffer.clear();
            lastFlushTime = System.currentTimeMillis();
        }

        // 별도 트랜잭션으로 JDBC Batch Insert 실행
        bulkInsertStore.insertAllInNewTransaction(toInsert);
    }

    @PreDestroy
    public void onShutdown() {
        // 서버가 내려갈 때 버퍼에 남은 로그를 비우고 종료
        drainAndInsert();
    }
}

Xotiradagi bufer JPA saveAll o‘rniga native batchUpdate’dan ham foydalanadi; ma’lumotlar bazasi va tarmoqqa tushadigan yukni kamaytirish uchun yozuvlarni 200 tadan guruhlab kiritadi.

Shu bilan birga, yozuvlarni buferga qo‘shayotgandagina qisqa muddatli blokirovkani oladi, ma’lumotlar bazasiga katta hajmdagi kiritish-chiqarish amallarini esa blokirovkadan tashqarida bajaradi. Bu oqimlarning bir-birini kutishi tufayli yuzaga keladigan tiqilinchlarning oldini oladi. Server qayta joylashtirilganda yoki o‘chirilganda xotirada kutib turgan jurnallar yo‘qolmasligi uchun @PreDestroy server yopilishidan oldin qolgan barcha ma’lumotlarni saqlaydi.

Shaxsiy ma’lumotlar auditi jurnallari — xizmatning qora qutisi

Xizmat ishlab chiqilayotganda xavfsizlik va qonunlarga rioya qilish bilan bog‘liq ishlar osonlikcha keyinga surilishi mumkin. Bu foydalanuvchilar darhol ko‘radigan funksiya emas, ustiga-ustak, yodda tutish kerak bo‘lgan qoidalar shunchalik ko‘pki, rostini aytsam, bu ish ko‘pincha zerikarli yumushdek tuyuladi.

Ammo biz qayta ishlayotgan ma’lumotlar shunchaki ma’lumotlar bazasida saqlanadigan matn emas. U onlayn olamda biror kishining timsoli; real dunyoda yashayotgan haqiqiy insonni aniqlashga yordam beradigan belgidir. Bu ma’lumotlar ehtiyotsizlik bilan boshqarilib, sizib chiqsa, oqibatini foydalanuvchilar ko‘taradi. Foydalanuvchilar shaxsiy ma’lumotlarini xizmatimizga ishonib topshiradi. Men esa qurayotgan tizimlarimiz orqali bu ishonchni oqlash dasturchining mas’uliyati ekanini angladim.

Audit jurnallari nafaqat foydalanuvchilar, balki xizmatimiz va dasturchilarning o‘zi uchun ham himoya vositasidir. Tashqaridan shubhali kirish bo‘lsa yoki ma’lumotlar sizib chiqqani gumon qilinsa, kim qaysi ma’lumotlarga qachon va qayerdan kirganini ko‘rsatuvchi aniq yozuvlarsiz dasturchilar o‘zini himoya qila olmaydi. Kirish jurnallarini puxta yuritganimizda, «tizimimiz tegishli tartiblarga amal qilgan, ta’sirlangan hisob qaydnomasi esa mana shu hajmdagi ma’lumotlargagina kirgan», deb aniq tushuntira olamiz.

Men qonunlarga rioya qilish va xavfsizlik choralarini xizmatni ishlab chiqishni sekinlashtiradigan ishlar deb emas, balki keyinchalik ancha tez ishlayotganimizda baxtsiz hodisalarning oldini olishga tayyorgarlik deb bilaman. Ishonchli qora quti o‘rnatilgach, xizmat o‘sib, rivojlanishda davom etar ekan, yangi funksiyalarni ishonch bilan ishlab chiqa olamiz.

informalife

Site footer