CORS dalam CFML: bila ia perlu dan cara melaksanakannya dalam Lucee
Pelajari CORS dalam CFML pada Lucee: preflight OPTIONS, header Origin yang tidak ada dalam cgi, dua header berganda, dan Vary yang menyelamatkan cache.

Kandungan
- CORS dalam CFML: bila ia perlu dan cara melaksanakannya dalam Lucee
- Bilakah CORS benar-benar diperlukan?
- Kenapa permintaan JSON sentiasa mencetuskan preflight?
- Di manakah header Origin boleh dibaca dalam Lucee?
- Bagaimana membina pengendali preflight yang menolak asal asing?
- Apa yang berlaku kalau endpoint anda tiada cabang OPTIONS?
- Tiga kesilapan header yang menghasilkan ralat CORS yang mengelirukan
- Dua header Access-Control-Allow-Origin dalam satu respons
- Wildcard bersama kredensial
- Vary tiada pada respons yang bergantung pada Origin
- Bagaimana menguji CORS dari terminal?
- Apa yang CORS tidak lindungi?
- Ringkasan kesilapan dan pembetulan
- Soalan Lazim
- Sumber
CORS dalam CFML: bila ia perlu dan cara melaksanakannya dalam Lucee
Jawapan pantas: CORS hanya perlu apabila JavaScript dalam pelayar memanggil domain yang berbeza daripada domain halaman. Pelayan CFML mesti memulangkan Access-Control-Allow-Origin yang sepadan dengan asal pemanggil dan menjawab permintaan OPTIONS awal dengan kod 204 sebelum kerja sebenar bermula. Dalam Lucee, header Origin tiada dalam skop cgi pada rig ujian ini, jadi ia dibaca melalui getHTTPRequestData().headers, dan setiap respons yang bergantung pada asal mesti membawa Vary: Origin.
Semua respons di bawah ialah keluaran sebenar rig Lucee 7.1.1.17-RC diambil dengan curl; fakta had pelayar dipetik daripada MDN dan fakta atribut tag daripada rujukan Lucee.
Bilakah CORS benar-benar diperlukan?
CORS ialah dasar yang dikuatkuasakan oleh pelayar, bukan oleh pelayan. Apabila halaman di https://app.contoh.my memanggil https://api.contoh.my/data.json, pelayar menghantar permintaan itu dan kemudian memeriksa header jawapan. Kalau header tidak membenarkan asal halaman itu, pelayar menyekat kod JavaScript daripada membaca jawapan, walaupun pelayan sudah memproses permintaan tersebut.
Dua akibat penting. Pelayan tetap menerima permintaan yang disekat, jadi CORS tidak boleh menjadi pengganti pengesahan. Dan alat bukan pelayar tidak menguatkuasakan CORS langsung: curl, cfhttp dan aplikasi mudah alih native menerima jawapan yang sama walaupun header CORS tiada.
| Situasi | Perlu CORS | Sebab |
|---|---|---|
| Halaman dan endpoint pada domain yang sama | Tidak | Asal sama, tiada pemeriksaan rentas asal |
| Aplikasi satu halaman pada subdomain berbeza | Ya | Pelayar memanggil domain lain dengan fetch() |
| Aplikasi mudah alih native | Tidak | Tiada dasar asal dalam klien native |
Pelayan ke pelayan dengan cfhttp | Tidak | Bukan pelayar, tiada pemeriksaan CORS |
Kenapa permintaan JSON sentiasa mencetuskan preflight?
Pelayar membahagikan permintaan rentas asal kepada dua kelas: yang dihantar terus, dan yang dihantar sebagai OPTIONS dahulu untuk mendapatkan kebenaran. MDN menyenaraikan nilai Content-Type yang dibenarkan dalam kelas pertama: application/x-www-form-urlencoded, multipart/form-data dan text/plain.
application/json tiada dalam senarai itu, jadi setiap fetch() yang menghantar badan JSON mencetuskan preflight walaupun kaedahnya POST biasa. Header tersuai seperti X-CSRF-Token juga mencetuskan preflight. Ini menerangkan gejala yang selalu mengelirukan: endpoint berfungsi sempurna dengan curl tetapi gagal dari pelayar, kerana curl tidak menghantar preflight. Pelayar menyimpan keputusan preflight mengikut Access-Control-Max-Age, yang lalainya lima saat; MDN menyatakan Chromium sejak versi 76 mengehadkannya pada 7200 saat dan Firefox pada 86400 saat, jadi nilai besar tidak memberi sehari penuh pada Chromium.
Di manakah header Origin boleh dibaca dalam Lucee?
Bukan dalam cgi. Pada rig ini kunci cgi yang berkaitan header hanya http_accept, http_accept_encoding, http_accept_language, http_connection, http_cookie, http_host, http_user_agent dan http_referer. Tiada http_origin, jadi semakan cgi.http_origin sentiasa gagal dan endpoint anda menganggap setiap permintaan datang tanpa asal.
Header sebenar boleh dibaca melalui getHTTPRequestData().headers, dan kuncinya mengekalkan huruf asal yang dihantar, contohnya Origin dan X-CSRF-Token:
mf = getHTTPRequestData();
asal = "";
if (structKeyExists(mf.headers, "Origin")) {
asal = mf.headers["Origin"];
}
kaedah = "GET";
if (structKeyExists(cgi, "request_method")) {
kaedah = cgi.request_method;
}
cfheader(name = "Content-Type", value = "application/json; charset=utf-8");
writeOutput(serializeJSON({ asal = asal, kaedah = kaedah }));
Diminta dengan curl -H 'Origin: https://app.contoh.my', jawapannya:
HTTP/1.1 200
Content-Type: application/json;charset=utf-8
{"ASAL":"https://app.contoh.my","KAEDAH":"GET"}
Dua butiran kecil berguna semasa menyahpepijat: Lucee menulis semula application/json; charset=utf-8 tanpa jarak, dan serializeJSON menukar kunci struct kepada huruf besar. Bentuk JSON yang tepat untuk kontrak API dibincangkan dalam artikel serializeJSON.
Bagaimana membina pengendali preflight yang menolak asal asing?
Satu fungsi kecil yang menetapkan header hanya apabila asal ada dalam senarai, dan satu cabang OPTIONS yang berhenti sebelum kerja sebenar:
senaraiAsal = ["https://app.contoh.my", "https://admin.contoh.my"];
function kepalaCors(required array senarai, required string asal,
string kaedah = "GET, POST, OPTIONS",
string tajuk = "Content-Type, X-CSRF-Token") {
cfheader(name = "Vary", value = "Origin");
if (!len(asal) or !arrayFindNoCase(senarai, asal)) {
return false;
}
cfheader(name = "Access-Control-Allow-Origin", value = asal);
cfheader(name = "Access-Control-Allow-Methods", value = kaedah);
cfheader(name = "Access-Control-Allow-Headers", value = tajuk);
cfheader(name = "Access-Control-Max-Age", value = "600");
return true;
}
mf = getHTTPRequestData();
asal = "";
if (structKeyExists(mf.headers, "Origin")) {
asal = mf.headers["Origin"];
}
kaedah = "GET";
if (structKeyExists(cgi, "request_method")) {
kaedah = cgi.request_method;
}
if (kaedah == "OPTIONS") {
if (!kepalaCors(senaraiAsal, asal)) {
cfheader(statuscode = "403");
cfheader(name = "Content-Type", value = "application/json; charset=utf-8");
cfcontent(type = "application/json; charset=utf-8",
variable = charsetDecode(serializeJSON({ ralat = "asal tidak dibenarkan" }), "utf-8"));
cfabort;
}
cfheader(statuscode = "204");
cfabort;
}
if (len(asal) and !arrayFindNoCase(senaraiAsal, asal)) {
cfheader(statuscode = "403");
cfheader(name = "Content-Type", value = "application/json; charset=utf-8");
cfcontent(type = "application/json; charset=utf-8",
variable = charsetDecode(serializeJSON({ ralat = "asal tidak dibenarkan" }), "utf-8"));
cfabort;
}
kepalaCors(senaraiAsal, asal);
cfheader(name = "Content-Type", value = "application/json; charset=utf-8");
cfcontent(type = "application/json; charset=utf-8",
variable = charsetDecode(serializeJSON({ status = "ok", asal = asal }), "utf-8"));
Empat keadaan yang saya jalankan pada fail yang sama:
GET, Origin dibenarkan
HTTP/1.1 200
Access-Control-Allow-Origin: https://app.contoh.my
Access-Control-Max-Age: 600
OPTIONS, Origin dibenarkan
HTTP/1.1 204
Access-Control-Allow-Origin: https://app.contoh.my
Content-Type: text/html;charset=UTF-8
GET, Origin asing
HTTP/1.1 403
{"RALAT":"asal tidak dibenarkan"}
GET, tanpa Origin
HTTP/1.1 200
{"ASAL":"","STATUS":"ok"}
Jawapan 204 masih membawa Content-Type: text/html;charset=UTF-8 kerana Lucee menetapkan jenis kandungan lalai walaupun tiada badan dihantar. Ia tidak menggagalkan preflight, tetapi mengelirukan semasa membaca log. Apabila asal ditolak, pelayar tidak menunjukkan kod 403 itu kepada JavaScript kerana tiada Access-Control-Allow-Origin; klien bukan pelayar pula menerima 403 dengan badan JSON yang boleh dibaca.
Apa yang berlaku kalau endpoint anda tiada cabang OPTIONS?
Lucee tidak mengendalikan OPTIONS secara berasingan. Ia menjalankan templat atau pengendali seperti biasa. Saya menguji ini pada endpoint yang hanya menulis JSON: permintaan OPTIONS dengan Origin memulangkan HTTP/1.1 201 beserta badan JSON penuh yang sama seperti permintaan biasa.
Maksudnya, preflight akan menjalankan logik anda. Untuk endpoint baca itu hanya membazir kerja, tetapi untuk endpoint tulis pelayar boleh mencetuskan INSERT, pemadaman atau penghantaran e-mel melalui permintaan yang tidak pernah sampai ke kod JavaScript. Cabang OPTIONS mesti diletakkan sebelum sebarang penulisan.
Tiga kesilapan header yang menghasilkan ralat CORS yang mengelirukan
Dua header Access-Control-Allow-Origin dalam satu respons
cfheader menambah header, bukan menggantikannya. Dua panggilan dengan nama yang sama menghasilkan dua baris:
Access-Control-Allow-Origin: https://satu.contoh.my
Access-Control-Allow-Origin: https://dua.contoh.my
MDN menyatakan nilai header itu ialah satu asal sahaja, dan pelayan mesti memulangkan asal klien yang membuat permintaan. Respons dengan dua nilai tidak memenuhi syarat itu, jadi pelayar menolaknya walaupun kedua-dua asal ada dalam senarai anda. Punca biasa: pembantu CORS dipanggil sekali dalam penapis, sekali lagi dalam pengendali.
Wildcard bersama kredensial
Access-Control-Allow-Origin: * hanya sah untuk permintaan tanpa kredensial. MDN menyatakan percubaan menggunakan wildcard bersama kredensial menghasilkan ralat, dan pelayan mesti menyatakan asal secara eksplisit. Peraturan yang sama terpakai pada Access-Control-Allow-Headers, Access-Control-Allow-Methods dan Access-Control-Expose-Headers: ketiga-tiganya mesti menyenaraikan nilai sebenar.
Sebaik sahaja endpoint memerlukan kuki atau header pengesahan, wildcard mesti dibuang dan senarai asal dibina dari awal.
Vary tiada pada respons yang bergantung pada Origin
Respons yang sama boleh dibenarkan untuk satu asal dan ditolak untuk asal lain, tetapi URLnya sama. Tanpa Vary: Origin, cache perantaraan atau proksi terbalik boleh menyimpan jawapan untuk asal pertama dan menghidangkannya kepada asal kedua. Gejalanya ialah CORS berfungsi di satu domain dan gagal di domain lain selepas cache mengumpul beberapa jam trafik. Kalau proksi menambah header CORS sendiri, matikan salah satu supaya tidak berlaku dua header.
Bagaimana menguji CORS dari terminal?
Tiga permintaan ini memisahkan masalah header daripada masalah pelayar:
curl -s -D - -H 'Origin: https://app.contoh.my' https://api.contoh.my/data.json
curl -s -D - -X OPTIONS -H 'Origin: https://app.contoh.my' \
-H 'Access-Control-Request-Method: POST' \
-H 'Access-Control-Request-Headers: content-type,x-csrf-token' \
https://api.contoh.my/data.json
curl -s -D - -o /dev/null https://api.contoh.my/data.json
Yang dicari bukan kod 200, tetapi bilangan baris Access-Control-Allow-Origin. Mesti tepat satu, dan nilainya mesti asal yang dihantar. Kalau tiada, semak dahulu sama ada kod membaca Origin daripada tempat yang betul. Kalau ada dua, cari panggilan cfheader yang berulang. Ujian ini membuktikan header, bukan tingkah laku pelayar; selepas curl bersih, barulah semak konsol pelayar.
Apa yang CORS tidak lindungi?
CORS tidak mengesahkan pengguna dan tidak menyekat permintaan daripada pelayan lain. Ia juga tidak menggantikan pertahanan CSRF: MDN menyatakan pelayan memang sudah perlu melindungi diri daripada pemalsuan permintaan rentas laman, kerana borang HTML biasa boleh dihantar ke mana-mana asal. Endpoint JSON yang menerima kuki sesi masih perlu token CSRF dan semakan kaedah. Corak pertahanan untuk borang dan sesi ada dalam artikel keselamatan XSS dan CSRF, manakala pembalut try dan catch untuk badan ralat JSON ada dalam artikel pengendalian ralat CFML. Kalau endpoint itu sebahagian daripada API berasaskan komponen, pertimbangkan pendekatan REST Lucee yang dibandingkan dalam artikel REST API Lucee.
Ringkasan kesilapan dan pembetulan
| Gejala | Punca | Pembetulan |
|---|---|---|
CORS gagal di pelayar, curl nampak normal | Tiada cabang OPTIONS, preflight menjalankan pengendali | Tangani OPTIONS sebelum sebarang penulisan, balas 204 |
| Asal sentiasa kosong dalam kod | Membaca cgi.http_origin yang tidak wujud | Baca getHTTPRequestData().headers["Origin"] |
| Pelayar menolak respons yang nampak betul | Dua baris Access-Control-Allow-Origin | Panggil pembantu CORS sekali per respons |
| Berfungsi di satu domain, gagal di domain lain | Cache menyimpan jawapan tanpa Vary: Origin | Hantar Vary: Origin pada setiap respons |
| Kredensial gagal walaupun asal betul | Wildcard * bersama kredensial | Nyatakan asal sebenar dan senarai header eksplisit |
Soalan Lazim
Adakah CORS perlu kalau API dan laman berada pada domain yang sama? Tidak. Pelayar hanya memeriksa header CORS untuk permintaan rentas asal, jadi laluan pada domain yang sama tidak memerlukannya.
Kenapa cgi.http_origin kosong walaupun pelayar menghantar Origin?
Kerana pada rig Lucee 7.1.1.17-RC ini skop cgi tidak mengandungi kunci http_origin langsung. Header itu ada dalam getHTTPRequestData().headers dengan nama Origin.
Bolehkah saya guna Access-Control-Allow-Origin: * untuk semua endpoint awam?
Boleh selagi tiada kredensial, tetapi hadnya cepat sampai. Sebaik sahaja endpoint memerlukan kuki atau header pengesahan, wildcard mesti diganti dengan asal eksplisit.
Kenapa preflight memulangkan 204 tetapi pelayar masih menyekat permintaan?
Semak sama ada Access-Control-Allow-Origin betul-betul ada pada jawapan 204, dan sama ada Access-Control-Allow-Headers menyenaraikan setiap header yang diminta klien. Jawapan 204 tanpa header CORS ialah preflight yang gagal.
Adakah CORS melindungi endpoint saya daripada permintaan luar?
Tidak. CORS ialah dasar pelayar, jadi curl dan pelayan lain tidak terjejas. Endpoint tetap memerlukan pengesahan, kebenaran dan token CSRF untuk permintaan yang mengubah data.
Sumber
- MDN, panduan CORS: permintaan mudah, preflight dan kredensial
- MDN, header Access-Control-Max-Age dan had pelayar
- MDN, header Access-Control-Allow-Origin dan peraturan satu asal
- Rujukan tag cfheader Lucee, atribut statusCode dan statusText
- Rujukan tag cfcontent Lucee, atribut type, variable dan reset