Rate limiting dalam CFML: had kadar dengan cache, kunci dan respons 429
Bina had kadar dalam CFML dengan cache: tetingkap tetap lawan bergerak, kunci untuk perlumbaan, dan respons 429 dengan Retry-After yang diuji di rig Lucee.

Kandungan
- Rate limiting dalam CFML: had kadar dengan cache, kunci dan respons 429
- Kenapa had kadar perlu dalam aplikasi, bukan hanya di proksi
- Bagaimana tetingkap tetap berfungsi dengan cache Lucee?
- Kenapa tetingkap tetap membenarkan dua kali had pada sempadan?
- Adakah tetingkap bergerak menyelesaikan masalah itu?
- Kenapa had 10 menjadi 40 apabila permintaan tiba serentak?
- Jadual ringkasan ukuran di rig
- Bagaimana membalas 429 dengan Retry-After yang betul?
- Berapa kos semakan, dan stor mana dipilih?
- Had kadar yang laman ini sendiri pakai
- Kesilapan yang paling kerap
- Soalan Lazim
- Sumber
Rate limiting dalam CFML: had kadar dengan cache, kunci dan respons 429
Jawapan pantas: Had kadar dalam CFML dibina dengan menyimpan kiraan permintaan bagi setiap kunci, biasanya alamat IP, di dalam cache, kemudian menolak permintaan apabila kiraan melepasi had. Dua perkara menentukan sama ada ia berkesan: bentuk tetingkap masa, dan kunci di sekeliling bacaan serta tulisan kiraan itu. Diukur pada Lucee 7.1.1.17, tanpa kunci had 10 menjadi 40 permintaan yang dibenarkan.
Kenapa had kadar perlu dalam aplikasi, bukan hanya di proksi
Proksi di hadapan Lucee menyekat trafik dengan baik, tetapi ia tidak tahu apa-apa tentang pengguna. Ia melihat satu alamat IP untuk seluruh rangkaian pejabat, dan ia tidak tahu sama ada permintaan itu cubaan log masuk atau bacaan artikel. Had kadar aplikasi boleh mengira mengikut perkara yang proksi tidak nampak: cubaan log masuk bagi satu nama pengguna, atau panggilan API untuk satu token. Jadi kedua-duanya dipasang: proksi menapis banjir trafik, aplikasi mengira kuota yang bermakna.
Bagaimana tetingkap tetap berfungsi dengan cache Lucee?
Masa dibahagi kepada blok, contohnya setiap 60 saat, dan setiap blok mempunyai kiraan sendiri. Kunci cache dibina daripada nama baldi, kunci pelanggan dan nombor blok, jadi kiraan bermula semula secara automatik apabila blok bertukar.
<cfscript>
function hadKadarTetap(string bucket, string kunci, numeric had, numeric saat) {
var id = arguments.bucket & "|" & arguments.kunci & "|" & int(getTickCount() / 1000 / arguments.saat);
var bil = cacheGet(local.id);
if (isNull(local.bil)) local.bil = 0;
local.bil = local.bil + 1;
cachePut(local.id, local.bil, createTimeSpan(0, 0, 0, arguments.saat), createTimeSpan(0, 0, 0, arguments.saat));
var baki = arguments.had - local.bil;
return { benar: local.bil <= arguments.had, baki: (baki > 0 ? baki : 0) };
}
kunciUjian = "ip-" & createUUID();
lulus = 0;
tolak = 0;
for (i = 1; i <= 12; i++) {
if (hadKadarTetap("artikel", kunciUjian, 10, 60).benar) lulus = lulus + 1;
else tolak = tolak + 1;
}
writeOutput("lulus=" & lulus & " tolak=" & tolak);
</cfscript>
Dua belas permintaan dengan had 10 setiap 60 saat menghasilkan lulus=10 tolak=2. Dua perkara dalam kod itu senang tersilap. local. digunakan untuk setiap pemboleh ubah, kerana Lucee menyelesaikan nama tanpa skop kepada arguments dahulu di dalam fungsi, jadi var rekod diikuti rekod boleh membaling The key [REKOD] doesn't exist in the arguments scope. Dan masa diambil daripada getTickCount(), yang mengira milisaat sejak pelayan dimulakan, jadi ia tidak terjejas oleh perubahan waktu.
Kenapa tetingkap tetap membenarkan dua kali had pada sempadan?
Kerana tetingkap tetap tidak melihat ke belakang. Apabila tetingkap baharu bermula, kiraan kembali sifar, jadi pelanggan yang menghabiskan kuota di hujung satu tetingkap boleh menghabiskan kuota kedua sejurus selepas sempadan: satu letusan besar.
<cfscript>
function cuba(string kunci, numeric had, numeric saat) {
var id = arguments.kunci & "|" & int(getTickCount() / 1000 / arguments.saat);
var bil = cacheGet(local.id);
if (isNull(local.bil)) local.bil = 0;
local.bil = local.bil + 1;
cachePut(local.id, local.bil, createTimeSpan(0, 0, 0, arguments.saat), createTimeSpan(0, 0, 0, arguments.saat));
return local.bil <= arguments.had;
}
kunci = "sempadan-" & createUUID();
mula = getTickCount();
lulus = 0;
for (i = 1; i <= 5; i++) if (cuba(kunci, 5, 2)) lulus = lulus + 1;
semasa = int(getTickCount() / 1000 / 2);
while (int(getTickCount() / 1000 / 2) == semasa) sleep(20);
for (i = 1; i <= 5; i++) if (cuba(kunci, 5, 2)) lulus = lulus + 1;
writeOutput("lulus=" & lulus & " ms=" & (getTickCount() - mula));
</cfscript>
Dua larian pada rig memberi lulus=10 ms=426 dan lulus=10 ms=1017. Kiraannya sama, jarak masanya berbeza kerana permulaan ujian jatuh di tempat berlainan dalam tetingkap. Dalam kedua-dua larian, 10 permintaan dibenarkan sedangkan hadnya 5 setiap 2 saat. Untuk laluan log masuk dengan had 5 cubaan seminit, lebihan itu bermakna.
Adakah tetingkap bergerak menyelesaikan masalah itu?
Tetingkap bergerak menyimpan cap masa setiap permintaan yang dibenarkan dan membuang yang lebih tua daripada tempoh tetingkap pada setiap semakan. Keputusan berdasarkan apa yang berlaku dalam 2 saat terakhir, bukan blok masa yang ditetapkan.
<cfscript>
function benarkan(string kunci, numeric had, numeric tetingkapMs) {
var kini = getTickCount();
var rekod = cacheGet(arguments.kunci);
if (isNull(local.rekod)) local.rekod = [];
var baru = [];
for (var t in local.rekod) {
if (t > kini - arguments.tetingkapMs) local.baru.append(t);
}
local.baru.append(kini);
cachePut(arguments.kunci, local.baru, createTimeSpan(0, 0, 0, 10), createTimeSpan(0, 0, 0, 10));
return arrayLen(local.baru) <= arguments.had;
}
kunci = "bergerak-" & createUUID();
mula = getTickCount();
lulus = 0;
for (i = 1; i <= 5; i++) if (benarkan(kunci, 5, 2000)) lulus = lulus + 1;
sleep(1000);
for (i = 1; i <= 5; i++) if (benarkan(kunci, 5, 2000)) lulus = lulus + 1;
writeOutput("lulus=" & lulus & " ms=" & (getTickCount() - mula));
</cfscript>
Ujian yang sama bentuknya memberi lulus=5 ms=1001: lima permintaan sahaja, bukan sepuluh. Kosnya, senarai cap masa membesar mengikut bilangan permintaan yang dibenarkan, jadi ia perlu pembersihan dan had saiz. Untuk had besar seperti 1,000 permintaan sejam, tetingkap bergerak yang menyimpan setiap cap masa menjadi mahal.
Kenapa had 10 menjadi 40 apabila permintaan tiba serentak?
Kerana cacheGet dan cachePut ialah dua panggilan berasingan, bukan satu operasi atom. Empat puluh utas boleh membaca 0 pada masa yang sama dan menulis 1, masing-masing menyangka ia permintaan pertama.
<cfscript>
application.dHad = 10;
application.dHasil = [];
application.dKunci = "serentak-" & createUUID();
kumpulan = "";
for (i = 1; i <= 40; i++) {
nama = "ta" & i;
thread action="run" name=nama {
nilai = cacheGet(application.dKunci);
if (isNull(nilai)) nilai = 0;
sleep(randRange(1, 4));
nilai = nilai + 1;
cachePut(application.dKunci, nilai, createTimeSpan(0, 0, 0, 60), createTimeSpan(0, 0, 0, 60));
lock name="senarai-a" type="exclusive" timeout="5" {
arrayAppend(application.dHasil, nilai <= application.dHad);
}
};
kumpulan = listAppend(kumpulan, nama);
}
thread action="join" name=kumpulan timeout="30000";
tanpa = 0;
for (b in application.dHasil) if (b) tanpa = tanpa + 1;
writeOutput("tanpaKunci=" & tanpa);
</cfscript>
Blok itu membenarkan tanpaKunci=40 daripada 40 permintaan. Apabila bacaan dan tulisan yang sama dibalut lock name="kira-kadar" type="exclusive", hasilnya denganKunci=10, dan kesemua 40 permintaan selesai dalam 16 hingga 31 milisaat. Penalti itu kecil kerana kerjanya hanya dua panggilan cache. Tanpa kunci, had kadar hanyalah hiasan.
Jadual ringkasan ukuran di rig
| Ujian | Had yang diisytihar | Hasil sebenar |
|---|---|---|
| Tetingkap tetap, 12 permintaan | 10 setiap 60 saat | 10 lulus, 2 tolak |
| Tetingkap tetap merentas sempadan | 5 setiap 2 saat | 10 lulus, dalam 426 dan 1017 ms |
| Tetingkap bergerak | 5 setiap 2 saat | 5 lulus, dalam 1001 ms |
| 40 permintaan serentak tanpa kunci | 10 | 40 lulus |
| 40 permintaan serentak dengan kunci | 10 | 10 lulus, dalam 16 hingga 31 ms |
Bagaimana membalas 429 dengan Retry-After yang betul?
Kod 403 atau 400 mengelirukan klien yang berkelakuan baik, dan membalas 200 memaksa setiap klien menyemak badan.
<cfscript>
cfheader(statuscode="429", statusText="Too Many Requests");
cfheader(name="Retry-After", value="37");
cfheader(name="Content-Type", value="application/json");
writeOutput('{"ralat":"terlalu banyak permintaan","cuba_selepas":37}');
</cfscript>
Kepala respons sebenar daripada rig:
HTTP/1.1 429
Retry-After: 37
Content-Type: application/json;charset=UTF-8
Content-Length: 56
Nilai Retry-After mesti dikira, bukan diletakkan tetap. Untuk tetingkap tetap, ia detik sehingga tetingkap baharu bermula: (windowId + 1) * saat - nowSec. Untuk tetingkap bergerak, ia masa sehingga cap masa tertua dalam senarai tamat.
Berapa kos semakan, dan stor mana dipilih?
Setiap permintaan membayar satu bacaan dan satu tulisan cache, dan pada rig 1,000 semakan mengambil 2 milisaat. Angka itu sah pada cache dalam ingatan; dengan Redis atau memcached, setiap semakan menjadi panggilan rangkaian.
| Stor | Baik untuk | Had yang perlu diingat |
|---|---|---|
Struct dalam application | Satu pelayan, laluan jarang dipanggil | Hilang apabila aplikasi dimulakan semula |
| Cache objek Lucee | Satu pelayan, kunci banyak | Setiap nod mengira sendiri |
| Redis atau memcached | Beberapa nod berkongsi satu had | Satu panggilan rangkaian setiap semakan |
| Proksi di tepi | Banjir trafik dan pengimbas | Tidak tahu pengguna atau nama akaun |
Jadual itu menerangkan satu perkara yang selalu terlupa: dua nod bermakna dua kali had.
Had kadar yang laman ini sendiri pakai
Laman ini memakai bentuk tetingkap tetap di dalam Security.cfc: kunci cache ialah gabungan nama baldi, kunci pelanggan dan nombor tetingkap, kiraan disimpan dalam struct dalam ingatan, dan setiap bacaan serta tulisan dibalut lock eksklusif. Ada sapuan yang membuang entri luput setiap lima minit supaya struct itu tidak membesar tanpa henti.
Dua akibat patut disebut: kiraan hilang apabila aplikasi dimulakan semula, dan kerana ia dalam ingatan satu bekas, had hanya tepat selagi satu bekas melayani trafik.
Alamat pelanggan pula bukan cgi.remote_addr mentah. Kerana Lucee berada di belakang proksi, kod itu mengambil hop awam paling kanan daripada X-Forwarded-For. Hop paling kiri tidak boleh dipercayai kerana pelanggan boleh menulisnya sendiri.
Kesilapan yang paling kerap
- Mengunci pada header yang ditulis pelanggan. Jika
X-Forwarded-Fordiambil bulat-bulat, penyerang menukar nilainya setiap permintaan. - Mengira permintaan yang berjaya sahaja. Penyerang yang sentiasa gagal tidak pernah dihadkan.
- Tiada
lock. Seperti diukur di atas, had 10 menjadi 40. - Tetingkap panjang dengan had kecil. Lima cubaan setiap jam terlalu longgar.
Soalan Lazim
Berapa lama tetingkap yang patut saya pilih untuk laluan log masuk? Lima cubaan setiap lima minit untuk setiap gabungan nama pengguna dan IP ialah permulaan yang munasabah. Yang penting ialah gabungan kunci: mengira mengikut IP sahaja membenarkan serangan daripada banyak IP, dan mengira mengikut akaun sahaja membenarkan penyerang mengunci akaun orang lain.
Bolehkah saya mengira had kadar dalam sesi, bukan dalam cache? Boleh, tetapi sesi hilang apabila pelanggan memadam kuki. Kiraan mesti disimpan pada kunci yang penyerang tidak boleh tukar dengan percuma.
Adakah had kadar perlu untuk setiap laluan? Tidak. Laluan yang mahal dan yang mengesahkan identiti perlu had ketat: log masuk, pendaftaran, reset kata laluan, penjanaan imej, penghantaran mel. Laluan bacaan biasa boleh memakai had longgar.
Kenapa kiraan saya tidak tepat apabila aplikasi berjalan pada dua bekas? Kerana setiap bekas mempunyai cache sendiri, jadi had sebenar menjadi dua kali ganda. Pindahkan kiraan ke Redis, atau bahagi had dengan bilangan bekas.
Adakah cflock memperlahankan permintaan?
Dalam ujian di atas, 40 permintaan serentak yang dibalut kunci selesai dalam 16 hingga 31 milisaat. Kunci menjadi mahal hanya apabila kerja berat diletakkan di dalamnya, seperti query pangkalan data atau panggilan HTTP.
Sumber
- Cache query dengan cachedWithin dalam Lucee, untuk tingkah laku cache objek.
- cflock dan keadaan perlumbaan dalam CFML, untuk corak penguncian.
- nginx atau Apache di hadapan Lucee, untuk kedudukan proksi dan IP sebenar.
- Keselamatan aplikasi web CFML: XSS, CSRF dan header keselamatan.
- Senarai semak keselamatan CFML sebelum produksi.
- Dokumentasi
cachePutdancacheGetdi docs.lucee.org. - MDN: HTTP 429 dan Retry-After.