Panduan Pasukan Lucee MY

Stripe dari CFML: Payment Intents dan 3D Secure

Sambung Stripe ke Lucee tanpa SDK: cipta PaymentIntent guna cfhttp dengan pengesahan Basic, serah 3D Secure kepada Stripe.js, sahkan webhook HMAC SHA256.

Stripe dari CFML: Payment Intents dan 3D Secure
0
Kandungan

Stripe dari CFML: Payment Intents dan 3D Secure

Stripe tidak menyediakan SDK rasmi untuk CFML, jadi anda tidak boleh sekadar menambah sebuah jar dan memanggil kelasnya. Cara yang berfungsi ialah memanggil API REST Stripe secara terus dengan tag cfhttp, mengesahkan setiap permintaan dengan HTTP Basic yang meletakkan kunci rahsia sebagai nama pengguna dan kata laluan kosong, dan menghantar badan dalam bentuk application/x-www-form-urlencoded. Daripada situ anda cipta PaymentIntent, hantar client_secret ke pelayar supaya Stripe.js mengendalikan cabaran 3D Secure, kemudian sahkan hasil akhir melalui webhook payment_intent.succeeded dengan tandatangan HMAC SHA256. Artikel ini memisahkan dengan jelas apa yang telah diuji dalam enjin Lucee berbanding apa yang memerlukan akaun Stripe sebenar.

Kenapa tiada SDK CFML untuk Stripe

Stripe menerbitkan pustaka rasmi untuk Java, PHP, Python, Ruby dan Node.js, tetapi tidak untuk CFML atau Lucee. Namun seluruh produk Stripe terdedah melalui API REST pada https://api.stripe.com/v1, dan CFML sudah mempunyai alat untuk memanggil API luar tanpa pustaka tambahan. Asas memanggil endpoint HTTP daripada Lucee dibincangkan dalam panduan cfhttp API luar. Perkara yang perlu anda bawa sendiri ialah pengesahan, bentuk badan permintaan, dan pengesahan webhook, dan ketiga-tiganya boleh dibina dengan fungsi teras CFML sahaja.

Pengesahan: kunci rahsia sebagai Basic Auth

Stripe mengesahkan panggilan API dengan HTTP Basic Auth, dan bentuknya agak luar biasa. Kunci rahsia anda menjadi nama pengguna, manakala kata laluannya kosong. Dalam CFML anda bina pengepala Authorization dengan mengekod rentetan "kunci:" dalam Base64: auth = "Basic " & toBase64(kunciStripe & ":"). Titik bertindih di hujung kunci itu wajib, kerana ia memisahkan nama pengguna daripada kata laluan kosong. Kunci bermula dengan sk_test_ untuk mod ujian dan sk_live_ untuk mod langsung. Jangan sesekali kod keras kunci langsung dalam sumber; simpan dalam pemboleh ubah persekitaran dan baca semasa mula aplikasi, seperti yang dijelaskan dalam panduan menyimpan rahsia dalam CFML. Kunci ujian pun jangan dibiarkan dalam kod awam, kerana ia masih boleh digunakan untuk mencipta objek ujian pada akaun anda.

Cipta PaymentIntent dengan cfhttp

PaymentIntent ialah objek yang mewakili satu niat bayaran dan melalui kitaran hayatnya sendiri, daripada memerlukan kaedah bayaran hinggalah berjaya atau gagal. Anda cipta ia dengan POST ke https://api.stripe.com/v1/payment_intents. Amaun dinyatakan dalam unit terkecil mata wang, iaitu sen untuk Ringgit Malaysia, jadi RM10.99 menjadi 1099. Medan automatic_payment_methods[enabled] bernilai true memberitahu Stripe supaya menerima mana-mana kaedah yang didayakan dalam dashboard, termasuk kad yang memerlukan 3D Secure:

<cfset kunciStripe = "sk_test_contoh">
<cfset auth = "Basic " & toBase64(kunciStripe & ":")>

<!--- Badan form-urlencoded: kekalkan kurungan segi empat pada kunci, kodkan nilai sahaja --->
<cfset badan = "amount=" & encodeForURL("1099")
    & "&currency=" & encodeForURL("myr")
    & "&automatic_payment_methods[enabled]=" & encodeForURL("true")>

<cfhttp method="post" url="https://api.stripe.com/v1/payment_intents" result="resp" charset="utf-8" timeout="30">
    <cfhttpparam type="header" name="Authorization" value="#auth#">
    <cfhttpparam type="header" name="Content-Type" value="application/x-www-form-urlencoded">
    <cfhttpparam type="body" value="#badan#">
</cfhttp>

<cfset data = deserializeJSON(resp.fileContent)>
<cfset clientSecret = data.client_secret>

Balasan yang berjaya ialah JSON yang mengandungi id, status dan client_secret. client_secret inilah yang anda hantar ke pelayar untuk melengkapkan bayaran, dan status awal biasanya requires_payment_method atau requires_confirmation. Semasa membina badan form-urlencoded, kekalkan kurungan segi empat pada nama medan secara literal dan kodkan nilai sahaja. Fungsi urlEncodedFormat terlalu agresif untuk tujuan ini, kerana ia mengekod garis bawah dan kurungan secara berlebihan, sedangkan format yang Stripe jangkakan mengekalkan automatic_payment_methods[enabled] seperti sedia ada.

Objek Stripe dan bila menggunakannya

Stripe bukan sekadar PaymentIntent. Empat objek ini muncul berulang kali dalam integrasi pembayaran, dan memilih yang salah menghasilkan aliran yang tidak munasabah:

ObjekBila gunakan
PaymentIntentBayaran sekali untuk pesanan biasa. Ia menjejak keseluruhan kitaran hayat daripada memerlukan kaedah bayaran, memerlukan pengesahan, memerlukan tindakan (3D Secure), hingga berjaya.
SetupIntentSimpan butiran kad untuk caj kemudian tanpa mengenakan caj sekarang, contohnya semasa pelanggan mendaftar langganan atau menyimpan kad.
RefundPulangkan sebahagian atau keseluruhan wang selepas PaymentIntent mencapai status succeeded.
CustomerSimpan pelanggan dan kad yang disimpan untuk bayaran berulang atau langganan.

Untuk kedai kecil yang menjual barang sekali sahaja, anda mungkin hanya memerlukan PaymentIntent dan Refund. Customer dan SetupIntent menjadi perlu apabila anda mahu menyimpan kad dan mengenakan caj berulang tanpa meminta pelanggan menaip semula butiran kad setiap kali.

client_secret dan 3D Secure di pelayar

3D Secure tidak dikendalikan oleh pelayan anda. Apabila pembayaran memerlukan pengesahan tambahan, Stripe memulangkan status requires_action, dan cabaran pengesahan itu diselesaikan sepenuhnya di pelayar oleh pustaka Stripe.js. Aliran yang betul ialah: pelayan mencipta PaymentIntent, menghantar client_secret ke pelayar, dan pelayar memanggil stripe.confirmCardPayment atau Element Pembayaran dengan client_secret itu. Stripe.js kemudian menguruskan lencongan ke halaman bank penerbit kad, melengkapkan 3D Secure, dan memulangkan hasilnya kepada pelayar anda.

Penting untuk membezakan dua kunci di sini. Kunci rahsia sk_test_ atau sk_live_ kekal di pelayan dan tidak pernah sampai ke pelayar. Kunci boleh terbit pk_test_ atau pk_live_ direka untuk pelayar dan digunakan oleh Stripe.js untuk memuatkan komponen pembayaran. client_secret bukan kunci API, tetapi ia membenarkan sesiapa yang memegangnya melengkapkan PaymentIntent tertentu, jadi ia mesti dilayan sebagai rahsia yang terhad kepada transaksi itu sahaja.

Jangan simpan client_secret dalam log

client_secret mampu menyempurnakan pembayaran yang ia wakili, jadi menyimpannya dalam fail log atau mendedahkannya dalam URL rujukan ialah lubang keselamatan yang nyata. Apabila anda mencatat log permintaan atau ralat, pastikan penapis log membuang atau menyunting medan client_secret dan mana-mana kunci Stripe. Prinsip yang sama yang dibincangkan dalam panduan keselamatan aplikasi web terpakai: rahsia tidak sepatutnya muncul dalam storan yang lebih longgar daripada pangkalan data aplikasi anda sendiri.

Webhook payment_intent.succeeded

Pelanggan yang menutup tab sebaik selepas 3D Secure lulus boleh menyebabkan halaman terima kasih tidak pernah dimuatkan, jadi jangan tutup pesanan berdasarkan aliran pelayar semata-mata. Stripe menghantar peristiwa payment_intent.succeeded sebagai webhook ke endpoint anda, dan itulah isyarat yang patut anda gunakan untuk menandakan pesanan dibayar. Endpoint ini menerima POST dengan badan JSON mentah, dan pengepala Stripe-Signature yang perlu anda sahkan sebelum mempercayai kandungannya. Untuk ujian semasa pembangunan, webhook perlu sampai ke mesin anda, dan teknik untuk mendedahkan endpoint tempatan diterangkan dalam panduan webhook tempatan.

Sahkan tandatangan Stripe-Signature

Pengepala Stripe-Signature berbentuk t=1492774577,v1=5257a869..., iaitu timestamp diikuti satu atau lebih tandatangan. Tandatangan ialah HMAC SHA256 ke atas rentetan yang menggabungkan timestamp dan badan permintaan mentah dengan titik di antaranya, menggunakan kunci webhook anda yang bermula dengan whsec_. Kunci webhook ini berbeza daripada kunci API dan diperoleh daripada dashboard ketika anda mendaftarkan endpoint:

<cfset kunciWebhook = "whsec_contoh_rahasia123">
<cfset timestamp = "1492774577">
<cfset payload = '{"id":"evt_contoh","type":"payment_intent.succeeded"}'>

<!--- Rentetan yang ditandatangani ialah timestamp titik payload --->
<cfset ditandatangani = timestamp & "." & payload>
<cfset tandatangan = lcase(hmac(ditandatangani, kunciWebhook, "HMACSHA256"))>

<cfset tandatanganDariHeader = "d4f5c03c5f5347fba2ccafb29f98bc8c7c853db74bf4ff62dc3f37028ebebc92">
<cfset sah = (compare(tandatangan, tandatanganDariHeader) eq 0)>

Fungsi hmac wujud dalam teras Lucee dan memulangkan hex. Rentetan yang ditandatangani mesti menggunakan badan mentah tepat seperti yang Stripe hantar, tanpa pemformatan semula, jadi baca terus badan mentah dan bukan objek form atau JSON yang telah dihuraikan. Output sebenar blok ini dalam rig Lucee 7.1 ialah:

Rentetan ditandatangani:
1492774577.{"id":"evt_contoh","type":"payment_intent.succeeded"}

Tandatangan dikira (HMAC-SHA256, hex):
d4f5c03c5f5347fba2ccafb29f98bc8c7c853db74bf4ff62dc3f37028ebebc92

Ujian positif (tandatangan betul diterima): true
Ujian negatif (tandatangan salah ditolak): false

Tandatangan itu juga disemak silang dengan perpustakaan HMAC standard di luar CFML dan menghasilkan rentetan yang sama, jadi nilainya bukan khusus kepada enjin.

Ujian negatif dan perbandingan selamat masa

Membuktikan tandatangan betul diterima tidak mencukupi. Anda juga perlu membuktikan tandatangan yang salah ditolak, dan itulah ujian negatif di atas: menukar satu aksara pada payload atau kunci menghasilkan hex yang berbeza dan perbandingan mengembalikan false. Dua lagi perkara melengkapkan pengesahan. Pertama, semak timestamp supaya tidak terlalu jauh daripada masa semasa, biasanya dalam lingkungan lima minit, untuk menolak permainan semula permintaan lama. Kedua, gunakan perbandingan selamat masa. Fungsi compare melakukan perbandingan bait demi bait tetapi mungkin berhenti awal pada bait pertama yang berbeza, jadi untuk ketegasan penuh gunakan MessageDigest.isEqual melalui interop Java, yang membandingkan keseluruhan bait tanpa jalan pintas:

<cfset md = createObject("java", "java.security.MessageDigest")>
<cfset b1 = createObject("java", "java.lang.String").init(tandatangan).getBytes("UTF-8")>
<cfset b2 = createObject("java", "java.lang.String").init(tandatanganDariHeader).getBytes("UTF-8")>
<cfset sah = md.isEqual(b1, b2)>

Perbandingan ini juga diuji dalam rig dan mengembalikan true untuk tandatangan betul serta false untuk tandatangan salah. Pengepala Stripe-Signature boleh membawa lebih daripada satu tandatangan v1 semasa kunci webhook diputar, dan anda hanya perlu memadankan salah satu daripadanya.

Apa yang diuji dan apa yang belum

Bahagian yang deterministik telah dijalankan dalam enjin Lucee 7.1: pembinaan badan form-urlencoded menghasilkan rentetan amount=1099&currency=myr&automatic_payment_methods[enabled]=true, dan pengiraan serta pengesahan tandatangan HMAC SHA256 berfungsi dengan ujian positif dan negatif seperti di atas. Panggilan cfhttp ke api.stripe.com turut dijalankan dengan kunci ujian rekaan, dan Stripe membalas 401 Unauthorized dengan mesej Invalid API Key, yang membuktikan struktur permintaan sampai ke pelayan Stripe dan difahami, tetapi tidak membuktikan kejayaan penciptaan PaymentIntent. Untuk itu anda memerlukan akaun Stripe sebenar dan kunci ujian sah daripada dashboard; mod ujian adalah percuma dan tidak mengenakan caj, jadi itulah langkah seterusnya yang munasabah sebelum ke mod langsung. Jangan dakwa integrasi berfungsi sepenuhnya sebelum satu bayaran ujian berjaya diselesaikan pada akaun anda sendiri.

Soalan Lazim

Adakah Stripe menyediakan SDK untuk CFML atau Lucee?

Tidak. Stripe menerbitkan SDK untuk Java, PHP, Python, Ruby dan Node.js, tetapi tidak untuk CFML. Anda memanggil API REST terus dengan cfhttp, dan artikel ini menunjukkan pengesahan, badan permintaan, dan pengesahan webhook yang diperlukan untuk berbuat demikian tanpa SDK.

Mengapa client_secret tidak boleh disimpan dalam log?

client_secret membenarkan pemegangnya melengkapkan PaymentIntent tertentu, jadi ia mampu menyempurnakan bayaran. Menyimpannya dalam log bermakna sesiapa yang membaca log boleh menyelesaikan transaksi itu, maka ia mesti disunting daripada sebarang output log.

Bagaimana saya tahu webhook itu benar-benar daripada Stripe?

Sahkan pengepala Stripe-Signature. Kira HMAC SHA256 ke atas timestamp digabungkan dengan badan mentah menggunakan titik sebagai pemisah dan kunci webhook whsec_, kemudian bandingkan dengan tandatangan dalam pengepala. Hanya terima permintaan yang tandatangannya padan dan timestampnya masih segar.

Bolehkah saya menguji integrasi tanpa akaun Stripe sebenar?

Anda masih memerlukan akaun Stripe untuk mendapatkan kunci ujian, tetapi mod ujian adalah percuma dan tidak mengenakan caj sebenar. Dengan kunci sk_test_ anda boleh mencipta PaymentIntent dan menerima webhook ujian tanpa sebarang transaksi wang sebenar.

Apa beza PaymentIntent dan SetupIntent?

PaymentIntent mewakili bayaran sekali dan menjejak kitaran hayat sehingga wang benar-benar berpindah. SetupIntent menyimpan butiran kad untuk caj kemudian tanpa mengenakan caj semasa ia dicipta, dan digunakan untuk langganan atau kad tersimpan.

Sumber

0 komen

0 komen

Log masuk untuk menjawab atau memberi komen.

Log masuk