Memecahkan masalah logout dengan benar dan elegan

Banyak aplikasi Web tidak berisi informasi yang terlalu rahasia dan pribadi seperti nomor rekening bank atau data kartu kredit. Tetapi beberapa memang mengandung data sensitif yang membutuhkan semacam skema perlindungan kata sandi. Misalnya, di pabrik di mana pekerja harus menggunakan aplikasi Web untuk memasukkan informasi lembar waktu, mengakses kursus pelatihan mereka, dan meninjau tarif per jam mereka, dll., Menggunakan SSL (Lapisan Soket Aman) akan berlebihan (halaman SSL tidak disimpan dalam cache; diskusi tentang SSL berada di luar cakupan artikel ini). Namun yang pasti aplikasi ini memang membutuhkan semacam perlindungan sandi. Jika tidak, pekerja (dalam hal ini, pengguna aplikasi) akan menemukan informasi sensitif dan rahasia tentang semua karyawan pabrik.

Contoh serupa untuk situasi di atas termasuk komputer yang dilengkapi Internet di perpustakaan umum, rumah sakit, dan kafe Internet. Dalam lingkungan seperti ini di mana pengguna berbagi beberapa komputer yang sama, melindungi data pribadi pengguna sangatlah penting. Pada saat yang sama, aplikasi yang dirancang dengan baik dan diimplementasikan dengan baik tidak mengasumsikan apa-apa tentang pengguna dan membutuhkan pelatihan paling sedikit.

Mari kita lihat bagaimana aplikasi Web yang sempurna akan berperilaku di dunia yang sempurna: Seorang pengguna mengarahkan browsernya ke sebuah URL. Aplikasi Web menampilkan halaman login yang meminta pengguna untuk memasukkan kredensial yang valid. Dia mengetikkan userid dan password. Dengan asumsi kredensial yang diberikan benar, setelah proses otentikasi, aplikasi Web memungkinkan pengguna untuk mengakses area resminya secara bebas. Saat tiba waktunya untuk berhenti, pengguna menekan tombol Keluar halaman. Aplikasi Web menampilkan halaman yang meminta pengguna untuk mengkonfirmasi bahwa dia memang ingin keluar. Begitu dia menekan tombol OK, sesi berakhir, dan aplikasi Web menampilkan halaman login lain. Pengguna sekarang dapat meninggalkan komputer tanpa khawatir pengguna lain mengakses data pribadinya. Pengguna lain duduk di depan komputer yang sama. Dia menekan tombol Kembali;aplikasi Web tidak boleh menampilkan halaman apa pun dari sesi pengguna terakhir. Faktanya, aplikasi Web harus selalu menjaga halaman login tetap utuh sampai pengguna kedua memberikan kredensial yang valid — hanya setelah itu dia dapat mengunjungi area resminya.

Melalui program contoh, artikel ini menunjukkan cara mencapai perilaku tersebut di aplikasi Web.

Sampel JSP

Untuk mengilustrasikan solusi secara efisien, artikel ini dimulai dengan menunjukkan masalah yang dihadapi dalam aplikasi Web, logoutSampleJSP1 . Aplikasi sampel ini mewakili berbagai aplikasi Web yang tidak menangani proses logout dengan benar. logoutSampleJSP1 terdiri dari beberapa halaman berikut JSP (JavaServer Pages): login.jsp, home.jsp, secure1.jsp, secure2.jsp, logout.jsp, loginAction.jsp, dan logoutAction.jsp. Halaman JSP home.jsp, secure1.jsp, secure2.jsp, dan logout.jspdilindungi terhadap pengguna yang tidak berkepentingan, yaitu, mereka berisi informasi aman dan seharusnya tidak pernah muncul di browser baik sebelum user log in atau setelah user log out. Halaman tersebut login.jspberisi formulir di mana pengguna mengetikkan nama pengguna dan kata sandi mereka. Halamanlogout.jspberisi formulir yang meminta pengguna untuk mengkonfirmasi bahwa mereka memang ingin keluar. Halaman JSP loginAction.jspdan logoutAction.jspbertindak sebagai pengontrol dan berisi kode yang masing-masing menjalankan tindakan login dan logout.

Contoh aplikasi Web kedua, logoutSampleJSP2 menunjukkan cara memperbaiki masalah logoutSampleJSP1. Namun, logoutSampleJSP2 tetap bermasalah. Masalah logout masih bisa muncul dalam keadaan khusus.

Aplikasi Web sampel ketiga, logoutSampleJSP3 ditingkatkan setelah logoutSampleJSP2 dan merupakan solusi yang dapat diterima untuk masalah logout.

Contoh akhir aplikasi Web logoutSampleStruts menunjukkan bagaimana Jakarta Struts dapat dengan elegan menyelesaikan masalah logout.

Catatan: Contoh yang menyertai artikel ini telah ditulis dan diuji untuk browser Microsoft Internet Explorer (IE), Netscape Navigator, Mozilla, FireFox, dan Avant terbaru.

Tindakan login

Artikel bagus Brian Pontarelli "Keamanan J2EE: Kontainer Versus Kustom" membahas pendekatan otentikasi J2EE yang berbeda. Ternyata, pendekatan otentikasi dasar dan berbasis bentuk HTTP tidak menyediakan mekanisme untuk menangani logout. Oleh karena itu, solusinya adalah dengan menerapkan penerapan keamanan khusus, karena penerapannya paling fleksibel.

Praktik umum dalam pendekatan autentikasi kustom adalah mengambil kredensial pengguna dari pengiriman formulir dan memeriksa bidang keamanan backend seperti LDAP (protokol akses direktori ringan) atau RDBMS (sistem manajemen database relasional). Jika kredensial yang diberikan valid, tindakan login menyimpan beberapa objek di HttpSessionobjek. Kehadiran objek ini HttpSessionmenunjukkan bahwa pengguna telah masuk ke aplikasi Web. Demi kejelasan, semua aplikasi contoh yang menyertai hanya menyimpan string nama pengguna di HttpSessionuntuk menunjukkan bahwa pengguna masuk. Daftar 1 menunjukkan potongan kode yang terdapat di laman loginAction.jspuntuk menggambarkan tindakan masuk:

Daftar 1

// ... // menginisialisasi objek RequestDispatcher; setel maju ke halaman rumah secara default RequestDispatcher rd = request.getRequestDispatcher ("home.jsp"); // Siapkan koneksi dan pernyataan rs = stmt.executeQuery ("pilih kata sandi dari PENGGUNA di mana userName = '" + userName + "'"); if (rs.next ()) {// Query hanya mengembalikan 1 record dalam set hasil; hanya 1 kata sandi per nama pengguna yang juga merupakan kunci utama if (rs.getString ("password"). equals (password)) {// If valid password session.setAttribute ("User", userName); // Menyimpan string nama pengguna di objek sesi} else {// Kata sandi tidak cocok, misalnya, kata sandi pengguna request.setAttribute ("Error", "Invalid password."); rd = request.getRequestDispatcher ("login.jsp"); }} // Tidak ada catatan dalam set hasil, yaitunama pengguna lain tidak valid {request.setAttribute ("Error", "Nama pengguna tidak valid."); rd = request.getRequestDispatcher ("login.jsp"); }} // Sebagai pengontrol, loginAction.jsp akhirnya meneruskan ke "login.jsp" atau "home.jsp" rd.forward (request, response); // ...

Dalam contoh aplikasi Web ini dan lainnya yang menyertai, bidang keamanan diasumsikan sebagai RDBMS. Namun, konsep artikel ini transparan dan berlaku untuk semua bidang keamanan.

Tindakan logout

Tindakan logout hanya melibatkan penghapusan string nama pengguna dan memanggil invalidate()metode pada objek pengguna HttpSession. Cantuman 2 menunjukkan cuplikan kode yang terdapat di halaman logoutAction.jspuntuk menggambarkan tindakan logout:

Daftar 2

// ... session.removeAttribute ("User"); session.invalidate (); // ...

Cegah akses yang tidak diautentikasi ke halaman JSP yang diamankan

Singkatnya, setelah validasi kredensial yang berhasil diambil dari pengiriman formulir, tindakan login hanya menempatkan string nama pengguna di HttpSessionobjek. Tindakan logout melakukan yang sebaliknya. Ini menghapus string nama pengguna dari HttpSessiondan memanggil invalidate()metode pada HttpSessionobjek. Agar tindakan login dan logout memiliki arti sama sekali, semua halaman JSP yang dilindungi harus terlebih dahulu memeriksa string nama pengguna yang ada di dalamnya HttpSessionuntuk menentukan apakah pengguna saat ini masuk. Jika HttpSessionberisi string nama pengguna — indikasi bahwa pengguna sedang login— aplikasi Web akan mengirim ke browser konten dinamis di halaman JSP lainnya. Jika tidak, halaman JSP akan meneruskan aliran kontrol kembali ke halaman login login.jsp,. Halaman JSP home.jsp, secure1.jsp,secure2.jsp, dan logout.jspsemuanya berisi potongan kode yang ditunjukkan pada Listing 3:

Daftar 3

// ... String userName = (String) session.getAttribute ("User"); if (null == userName) {request.setAttribute ("Error", "Sesi telah berakhir. Silakan login."); RequestDispatcher rd = request.getRequestDispatcher ("login.jsp"); rd.forward (permintaan, tanggapan); } // ... // Izinkan konten dinamis lainnya di JSP ini untuk disajikan ke browser // ...

Potongan kode ini mengambil string nama pengguna dari HttpSession. Jika string nama pengguna yang diambil adalah null , aplikasi Web akan menyela dengan meneruskan aliran kontrol kembali ke halaman login dengan pesan error "Sesi telah berakhir. Harap login.". Jika tidak, aplikasi Web memungkinkan aliran normal melalui sisa halaman JSP yang dilindungi, sehingga memungkinkan konten dinamis halaman JSP tersebut disajikan.

Menjalankan logoutSampleJSP1

Menjalankan logoutSampleJSP1 menghasilkan perilaku berikut:

  • The berperilaku aplikasi dengan benar dengan mencegah konten dinamis dari halaman JSP yang dilindungi home.jsp, secure1.jsp, secure2.jsp, dan logout.jspdari dilayani jika pengguna belum login. Dengan kata lain, dengan asumsi pengguna belum login tapi poin browser ke URL halaman-halaman JSP , Aplikasi Web meneruskan aliran kontrol ke halaman masuk dengan pesan kesalahan "Sesi telah berakhir. Harap masuk.".
  • Likewise, the application behaves correctly by preventing the dynamic content of the protected JSP pages home.jsp, secure1.jsp, secure2.jsp, and logout.jsp from being served after the user has already logged out. In other words, after the user has already logged out, if he points the browser to the URLs of those JSP pages, the Web application will forward the control flow to the login page with the error message "Session has ended. Please log in.".
  • The application does not behave correctly if, after the user has already logged out, he clicks on the Back button to navigate back to the previous pages. The protected JSP pages reappear on the browser even after the session has ended (with the user logging out). However, continual selection of any link on these pages brings the user to the login page with the error message "Session has ended. Please log in.".

Prevent the browsers from caching

The root of the problem is the Back button that exists on most modern browsers. When the Back button is clicked, the browser by default does not request a page from the Web server. Instead, the browser simply reloads the page from its cache. This problem is not limited to Java-based (JSP/servlets/Struts) Web applications; it is also common across all technologies and affects PHP-based (Hypertext Preprocessor), ASP-based, (Active Server Pages), and .Net Web applications.

After the user clicks on the Back button, no round trip back to the Web servers (generally speaking) or the application servers (in Java's case) takes place. The interaction occurs among the user, the browser, and the cache. So even with the presence of Listing 3's code in the protected JSP pages such as home.jsp, secure1.jsp, secure2.jsp, and logout.jsp, this code never gets the chance to execute when the Back button is clicked.

Depending on whom you ask, the caches that sit between the application servers and the browsers can either be a good thing or a bad thing. These caches do in fact offer a few advantages, but that's mostly for static HTML pages or pages that are graphic- or image-intensive. Web applications, on the other hand are more data-oriented. As data in a Web application is likely to change frequently, it is more important to display fresh data than save some response time by going to the cache and displaying stale or out-of-date information.

Fortunately, the HTTP "Expires" and "Cache-Control" headers offer the application servers a mechanism for controlling the browsers' and proxies' caches. The HTTP Expires header dictates to the proxies' caches when the page's "freshness" will expire. The HTTP Cache-Control header, which is new under the HTTP 1.1 Specification, contains attributes that instruct the browsers to prevent caching on any desired page in the Web application. When the Back button encounters such a page, the browser sends the HTTP request to the application server for a new copy of that page. The descriptions for necessary Cache-Control headers' directives follow:

  • no-cache: forces caches to obtain a new copy of the page from the origin server
  • no-store: directs caches not to store the page under any circumstance

For backward compatibility to HTTP 1.0, the Pragma:no-cache directive, which is equivalent to Cache-Control:no-cache in HTTP 1.1, can also be included in the header's response.

By leveraging the HTTP headers' cache directives, the second sample Web application, logoutSampleJSP2, that accompanies this article remedies logoutSampleJSP1. logoutSampleJSP2 differs from logoutSampleJSP1 in that Listing 4's code snippet is placed at the top of all protected JSP pages, such as home.jsp, secure1.jsp, secure2.jsp, and logout.jsp:

Listing 4

// ... response.setHeader ("Cache-Control", "no-cache"); // Memaksa cache untuk mendapatkan salinan halaman baru dari server asal response.setHeader ("Cache-Control", "no-store"); // Mengarahkan cache untuk tidak menyimpan halaman dalam keadaan apa pun response.setDateHeader ("Expires", 0); // Menyebabkan cache proxy melihat halaman sebagai respon "basi" .setHeader ("Pragma", "no-cache"); // HTTP 1.0 kompatibilitas mundur String userName = (String) session.getAttribute ("User"); if (null == userName) {request.setAttribute ("Error", "Sesi telah berakhir. Silakan login."); RequestDispatcher rd = request.getRequestDispatcher ("login.jsp"); rd.forward (permintaan, tanggapan); } // ...