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.

Kandungan
- Log audit dalam CFML: rekod siapa buat apa dan bila
- Log audit lawan log aplikasi
- Skema jadual log_audit
- Fungsi tulis audit
- Medan wajib dan butiran JSON
- IP klien dan proksi yang dipercayai
- Audit tidak boleh menggagalkan urus niaga
- Carian mengikut pelaku dan julat masa
- Indeks yang perlu
- Dasar simpan 12 bulan dan pemadaman berkelompok
- Soalan Lazim
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.
| Aspek | Log aplikasi | Log audit |
|---|---|---|
| Soalan yang dijawab | Apa yang rosak dan di mana | Siapa buat apa dan bila |
| Penulis | Enjin, pembangun | Aliran perniagaan |
| Kandungan | Ralat, amaran, jejak tindanan | Pelaku, tindakan, sasaran, butiran |
| Jangka hayat | Hari ke minggu, diputar | Bulan ke tahun, diwajibkan disimpan |
| Konsumen | Pembangun, pentadbir | Juruaudit, 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.
| Medan | Jenis | Tujuan |
|---|---|---|
| id | BIGINT UNSIGNED AUTO_INCREMENT | Kunci utama, tidak pernah dikitar semula |
| actor | VARCHAR(100) | Siapa: id atau nama pengguna, atau "sistem" |
| action | VARCHAR(50) | Apa: CREATE, UPDATE, DELETE, LOGIN, LOGOUT |
| target | VARCHAR(255) | Objek: gabungan jadual dan kunci, atau laluan URL |
| ip | VARCHAR(45) | Alamat IP klien, sokong IPv4 dan IPv6 |
| user_agent | VARCHAR(255) | Pelayar atau klien yang digunakan |
| detail | JSON | Butiran tambahan seperti nilai sebelum dan selepas |
| created_at | DATETIME | Bila 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.