Panduan Pasukan Lucee MY

Log audit dalam CFML: rekod siapa buat apa dan bila

Membina log audit dalam CFML: skema jadual, penulisan selamat dengan parameter terikat, medan IP dan JSON, carian mengikut pelaku, indeks dan dasar simpan data.

Log audit dalam CFML: rekod siapa buat apa dan bila
0
Kandungan

Log audit dalam CFML: rekod siapa buat apa dan bila

Penulis: Thalhah · Tarikh: 16 Oktober 2026

Log audit ialah rekod kekal tentang siapa yang melakukan apa, ke atas objek mana, dan pada bila. Ia menjawab soalan akauntabiliti seperti "siapa yang menukar harga produk ini dan bila", bukan soalan teknikal seperti "di mana ralat berlaku". Dalam sistem yang perlu mematuhi kehendak pematuhan dan penyiasatan penipuan, log audit ialah bukti yang boleh dipertanggungjawabkan, bukan sekadar alat penyahpepijatan.

Artikel ini membina satu jadual log_audit dalam MySQL, fungsi penulisan yang selamat, cara mengambil alamat IP klien dengan betul di belakang proksi, carian mengikut pelaku dan masa, serta dasar penyimpanan 12 bulan. Semua kod menggunakan queryExecute() dengan parameter terikat dan diuji pada MySQL 8.

Log audit lawan log aplikasi

Kedua-duanya dipanggil log, tetapi tujuannya berbeza dan tidak boleh dicampur dalam satu jadual. Log aplikasi yang ditulis melalui cflog atau writeLog bersifat teknikal dan sementara: ia menangkap ralat, amaran dan jejak tindanan untuk pembangun. Log audit pula bersifat perniagaan dan kekal: ia merekodkan tindakan yang perlu dikesan semula untuk pematuhan, penyiasatan dan pertikaian. Panduan lengkap tentang log aplikasi ada dalam Log dan Pemantauan Aplikasi CFML.

AspekLog aplikasiLog audit
Soalan yang dijawabApa yang rosak dan di manaSiapa buat apa dan bila
PenulisEnjin, pembangunAliran perniagaan
KandunganRalat, amaran, jejak tindananPelaku, tindakan, sasaran, butiran
Jangka hayatHari ke minggu, diputarBulan ke tahun, diwajibkan disimpan
KonsumenPembangun, pentadbirJuruaudit, penyiasat, pematuhan
Contoh"Timeout pada pertanyaan stok""thalhah menukar harga produk 12"

Kerana log audit ialah bukti, ia perlu disimpan dalam pangkalan data dengan medan berstruktur, bukan rentetan bebas dalam fail teks. Medan berstruktur membolehkan carian dan laporan, dan menjadikan log itu boleh dirujuk semula semasa audit dalaman atau siasatan.

Skema jadual log_audit

Setiap entri audit perlu menjawab lima soalan asas: siapa, apa, ke atas apa, dari mana, dan bila. Medan actor merekodkan pelaku, action merekodkan tindakan, target merekodkan objek yang disentuh, manakala ip, user_agent dan detail menambah konteks forensik. Butiran yang berubah-ubah disimpan sebagai JSON dalam medan detail, supaya anda tidak perlu menambah lajur baharu setiap kali jenis tindakan baharu muncul.

MedanJenisTujuan
idBIGINT UNSIGNED AUTO_INCREMENTKunci utama, tidak pernah dikitar semula
actorVARCHAR(100)Siapa: id atau nama pengguna, atau "sistem"
actionVARCHAR(50)Apa: CREATE, UPDATE, DELETE, LOGIN, LOGOUT
targetVARCHAR(255)Objek: gabungan jadual dan kunci, atau laluan URL
ipVARCHAR(45)Alamat IP klien, sokong IPv4 dan IPv6
user_agentVARCHAR(255)Pelayar atau klien yang digunakan
detailJSONButiran tambahan seperti nilai sebelum dan selepas
created_atDATETIMEBila tindakan berlaku, lalai masa semasa

Berikut ialah arahan DDL yang dijalankan melalui queryExecute(). Klausa IF NOT EXISTS menjadikannya selamat dijalankan berulang kali tanpa ralat:

<cfscript>
queryExecute(
    sql = "CREATE TABLE IF NOT EXISTS log_audit (
        id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
        actor VARCHAR(100) NOT NULL,
        action VARCHAR(50) NOT NULL,
        target VARCHAR(255) NOT NULL,
        ip VARCHAR(45) NULL,
        user_agent VARCHAR(255) NULL,
        detail JSON NULL,
        created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
        PRIMARY KEY (id),
        INDEX idx_audit_actor (actor),
        INDEX idx_audit_action (action),
        INDEX idx_audit_created (created_at)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
    options = { datasource = "mydb" }
);
writeOutput("Jadual log_audit sedia.");
</cfscript>

Fungsi tulis audit

Fungsi penulisan ialah satu-satunya pintu masuk ke jadual audit. Semua nilai diikat sebagai parameter supaya input pengguna tidak pernah dicantum ke dalam rentetan SQL. Fungsi ini menerima pelaku, tindakan, sasaran dan konteks pilihan, lalu menulis satu baris:

<cfscript>
function tulisAudit(
    required string actor,
    required string action,
    required string target,
    string ip = "",
    string userAgent = "",
    struct detail = {}
) {
    try {
        queryExecute(
            sql = "INSERT INTO log_audit
                   (actor, action, target, ip, user_agent, detail)
                   VALUES (:actor, :action, :target, :ip, :userAgent, CAST(:detail AS JSON))",
            params = {
                actor     = { value = arguments.actor, cfsqltype = "cf_sql_varchar" },
                action    = { value = arguments.action, cfsqltype = "cf_sql_varchar" },
                target    = { value = arguments.target, cfsqltype = "cf_sql_varchar" },
                ip        = { value = arguments.ip, cfsqltype = "cf_sql_varchar" },
                userAgent = { value = arguments.userAgent, cfsqltype = "cf_sql_varchar" },
                detail    = { value = serializeJSON(arguments.detail), cfsqltype = "cf_sql_varchar" }
            },
            options = { datasource = "mydb" }
        );
    } catch (any e) {
        cflog(text = "Audit gagal ditulis: " & e.message, type = "error", file = "audit");
    }
}

tulisAudit(
    actor = "thalhah",
    action = "LOGIN",
    target = "users/7",
    ip = cgi.remote_addr,
    userAgent = cgi.http_user_agent,
    detail = { "kaedah" = "kata_laluan" }
);
writeOutput("Audit ditulis.");
</cfscript>

Dua perkara penting dalam fungsi ini. Pertama, CAST(:detail AS JSON) memaksa MySQL mengesahkan JSON sebelum disimpan, supaya nilai yang rosak ditolak awal dan bukan disimpan sebagai teks yang tidak boleh dibaca semula. Kedua, seluruh penulisan dibalut try dan catch yang menulis ke cflog tanpa melempar semula ralat tersebut. Ini kunci kepada peraturan audit yang paling penting: kegagalan menulis audit tidak boleh merosakkan urus niaga perniagaan.

Medan wajib dan butiran JSON

Medan actor tidak semestinya nama pengguna sahaja. Untuk tindakan automatik seperti tugas berjadual, gunakan nilai khas seperti sistem supaya anda boleh membezakan tindakan manusia daripada mesin. Nilai actor biasanya diambil dari skop sesi selepas pengguna log masuk; panduan pengesahan pengguna dengan 2FA dan pengurusan sesi dalam CFML menerangkan cara mendapatkan identiti pengguna yang boleh dipercayai.

Medan action sebaiknya diseragamkan kepada satu set nilai kecil seperti CREATE, UPDATE, DELETE, LOGIN dan LOGOUT, kerana carian dan laporan bergantung pada nilai yang konsisten. Medan target membawa gabungan jenis objek dan pengecamnya, contohnya produk/12 atau users/7, supaya anda tahu jadual dan baris mana yang disentuh.

Medan detail JSON menyimpan nilai sebelum dan selepas sesuatu perubahan. Dengan menyimpan kedua-dua nilai, anda boleh menjawab "apakah harga sebelum dan selepas ditukar" tanpa membaca jadual asal, yang mungkin sudah berubah lagi sejak itu. Contoh struktur detail untuk perubahan harga ialah { "sebelum": { "harga": 19.90 }, "selepas": { "harga": 21.50 }, "sebab": "kemas kini harga pembekal" }.

IP klien dan proksi yang dipercayai

Apabila Lucee berjalan di belakang proksi songsang seperti Nginx atau Traefik, nilai cgi.remote_addr mengandungi alamat IP proksi, bukan IP klien sebenar. IP klien tiba dalam header X-Forwarded-For, tetapi header itu boleh dipalsukan oleh sesiapa sahaja yang menyambung terus ke pelayan. Oleh itu header proksi hanya boleh dipercayai apabila permintaan itu benar-benar melalui proksi anda.

<cfscript>
function ipKlien(required struct headers, boolean belakangProksi = false) {
    if (arguments.belakangProksi && structKeyExists(arguments.headers, "x-forwarded-for")) {
        return trim(listFirst(arguments.headers["x-forwarded-for"], ","));
    }
    return cgi.remote_addr;
}

headers = getHTTPRequestData().headers;
writeOutput("IP sambungan terus: " & ipKlien(headers, false));

contoh = { "x-forwarded-for" = "203.0.113.7, 10.0.0.1" };
writeOutput("IP belakang proksi: " & ipKlien(contoh, true));
</cfscript>

listFirst() mengambil nilai pertama dalam X-Forwarded-For kerana proksi menambah IP miliknya ke kanan senarai, jadi nilai paling kiri ialah klien asal. Bendera belakangProksi mesti benar hanya apabila aplikasi berada di belakang proksi anda dan tidak boleh dicapai terus dari internet. Jika anda membiarkannya benar tanpa proksi, penyerang boleh menghantar header palsu dan menulis IP orang lain ke dalam log anda, sekali gus merosakkan nilai forensik log tersebut. Amalan ini sebahagian daripada senarai semak dalam ringkasan keselamatan CFML setiap suku.

Audit tidak boleh menggagalkan urus niaga

Kegagalan yang paling mahal ialah apabila penulisan audit berkongsi urus niaga perniagaan dan melempar ralat, lalu menggulingkan semula pembayaran atau kemas kini yang sebenarnya berjaya. Dua peraturan mengelakkannya: tulis audit selepas urus niaga komited, dan balut penulisan itu dalam cubaan yang menelan ralat.

<cfscript>
// Urus niaga perniagaan komited dahulu.
transaction {
    queryExecute(
        "UPDATE users SET name = :nama WHERE id = :id",
        {
            nama = { value = "Nama Baru", cfsqltype = "cf_sql_varchar" },
            id = { value = 99999, cfsqltype = "cf_sql_integer" }
        },
        { datasource = "mydb" }
    );
}

// Audit ditulis selepas komit, dalam cubaan berasingan yang tidak melempar.
try {
    queryExecute(
        "INSERT INTO log_audit (actor, action, target, ip, detail)
         VALUES (:actor, :action, :target, :ip, CAST(:detail AS JSON))",
        {
            actor = { value = "thalhah", cfsqltype = "cf_sql_varchar" },
            action = { value = "UPDATE", cfsqltype = "cf_sql_varchar" },
            target = { value = "users/7", cfsqltype = "cf_sql_varchar" },
            ip = { value = cgi.remote_addr, cfsqltype = "cf_sql_varchar" },
            detail = { value = serializeJSON({ "medan" = "name" }), cfsqltype = "cf_sql_varchar" }
        },
        { datasource = "mydb" }
    );
} catch (any e) {
    cflog(text = "Audit gagal: " & e.message, type = "error", file = "audit");
}
writeOutput("Urus niaga komited, audit ditulis berasingan.");
</cfscript>

Fungsi tulisAudit yang ditulis sebelum ini meletakkan fungsi ini di satu tempat supaya setiap pemanggil tidak perlu mengulang corak try dan catch. Fungsi itu biasanya diletakkan dalam komponen yang dimuatkan oleh Application.cfc, supaya ia tersedia di seluruh aplikasi.

Carian mengikut pelaku dan julat masa

Sebab utama medan audit berstruktur ialah carian. Pertanyaan paling biasa ialah menyenaraikan semua tindakan seorang pelaku dalam sesuatu julat masa, contohnya untuk menjawab "apa yang dilakukan thalhah minggu ini".

<cfscript>
q = queryExecute(
    sql = "SELECT id, actor, action, target, created_at
           FROM log_audit
           WHERE actor = :actor
             AND created_at BETWEEN :mula AND :tamat
           ORDER BY created_at DESC",
    params = {
        actor = { value = "thalhah", cfsqltype = "cf_sql_varchar" },
        mula  = { value = dateAdd("d", -7, now()), cfsqltype = "cf_sql_timestamp" },
        tamat = { value = now(), cfsqltype = "cf_sql_timestamp" }
    },
    options = { datasource = "mydb" }
);
writeOutput("Rekod audit dijumpai: " & q.recordCount);
</cfscript>

Julat masa dibina dengan dateAdd() dan now(), jadi pertanyaan ini sentiasa merujuk kepada tujuh hari terkini dan tidak perlu menulis tarikh secara manual. Untuk mencari mengikut tindakan atau sasaran, tambah sahaja keadaan AND action = :action atau AND target = :target dengan parameter berasingan.

Indeks yang perlu

Indeks menentukan sama ada carian audit kekal pantas apabila jadual membesar kepada jutaan baris. Jadual log_audit di atas sudah membawa tiga indeks: idx_audit_actor untuk penapisan kesamaan pada actor, idx_audit_action untuk action, dan idx_audit_created untuk julat masa pada created_at. Indeks pada actor dan created_at ialah yang paling penting kerana kedua-duanya muncul dalam pertanyaan carian utama.

Apabila kebanyakan carian menggabungkan pelaku dan masa, indeks komposit (actor, created_at) melayan kedua-dua keadaan dalam satu indeks dan lebih cekap daripada dua indeks berasingan. Tambahkannya apabila jadual sudah besar dan carian pelaku mengikut masa menjadi perlahan:

CREATE INDEX idx_audit_actor_created ON log_audit (actor, created_at);

Jangan tambah indeks pada setiap medan secara membuta tuli. Setiap indeks memperlahankan penulisan dan memakan ruang cakera, dan penulisan audit berlaku pada setiap tindakan. Indeks hanya pada medan yang benar-benar ditanya dalam klausa WHERE.

Dasar simpan 12 bulan dan pemadaman berkelompok

Log audit perlu disimpan cukup lama untuk memenuhi kehendak pematuhan, tetapi tidak selama-lamanya kerana jadual yang membesar akan melambatkan carian dan memakan storan. Dasar 12 bulan ialah titik mula yang munasabah: simpan rekod satu tahun penuh, kemudian padam yang lebih lama. Sebelum memadam, sahkan dasar simpanan anda tidak bercanggah dengan kehendak undang-undang atau kontrak; panduan sandaran dan pemulihan CFML menerangkan cara menyimpan salinan sebelum apa-apa pemadaman kekal.

Pemadaman dilakukan secara berkelompok dengan LIMIT, supaya satu arahan tidak mengunci jadual terlalu lama:

<cfscript>
had = dateAdd("yyyy", -1, now());
queryExecute(
    sql = "DELETE FROM log_audit WHERE created_at < :had LIMIT 1000",
    params = { had = { value = had, cfsqltype = "cf_sql_timestamp" } },
    options = { datasource = "mydb" }
);
writeOutput("Rekod audit melebihi 12 bulan dipadamkan.");
</cfscript>

Klausa LIMIT 1000 memastikan setiap arahan memadam paling banyak seribu baris. Untuk membersihkan jadual yang besar, jalankan arahan ini berulang kali dalam satu gelung sehingga tiada baris lagi yang sepadan, dan jadualkan ia melalui tugas berjadual pada waktu trafik rendah supaya ia tidak bersaing dengan pengguna sebenar.

Soalan Lazim

Apakah perbezaan antara log audit dan log aplikasi?

Log aplikasi menangkap ralat, amaran dan jejak tindanan untuk membantu pembangun menyahpepijat, dan ia diputar dalam tempoh singkat. Log audit merekodkan siapa melakukan apa dan bila untuk akauntabiliti dan pematuhan, dan ia disimpan berbulan-bulan atau bertahun-tahun.

Adakah saya perlu menyimpan alamat IP klien dalam log audit?

Ya. Alamat IP ialah bukti forensik yang membantu mengesahkan siapa sebenarnya melakukan tindakan, terutamanya semasa menyiasat penipuan atau akses tidak sah. Pastikan ia diambil dari header proksi yang dipercayai dan bukan cgi.remote_addr secara membuta tuli apabila aplikasi berada di belakang proksi.

Bagaimana jika penulisan audit gagal di tengah urus niaga?

Jangan biarkan kegagalan audit menggagalkan urus niaga perniagaan. Tulis audit selepas urus niaga komited, dan balut penulisan dalam try dan catch yang mencatat ralat ke cflog tanpa melempar semula.

Berapa lama rekod audit perlu disimpan?

Dua belas bulan ialah titik mula yang munasabah, tetapi tempoh sebenar bergantung pada kehendak undang-undang, kontrak dan piawaian pematuhan yang mengikat perniagaan anda. Semak kehendak tersebut sebelum menetapkan dasar pemadaman automatik.

Bolehkah saya menggunakan satu jadual audit untuk semua jenis tindakan?

Boleh, dan itulah pendekatan yang ditunjukkan di sini. Medan action dan target membezakan jenis tindakan, manakala medan detail JSON menyimpan butiran khusus setiap tindakan tanpa menambah lajur baharu. Untuk jadual yang sangat besar, anda boleh membahagikan mengikut tarikh kemudian hari.

0 komen

0 komen

Log masuk untuk menjawab atau memberi komen.

Log masuk