01 March 2016

Spring-boot, MVC, Freemarker, dan Twitter-Bootstrap (bag 7 - Habis)

Pada tulisan lalu kita mempelajari implementasi keamanan data pada aplikasi web dengan menggunakan spring-security. Kita belajar mengkonfigurasi spring-security tersebut dengan menggunakan metode otentikasi yang nantinya bisa kita pilih berdasarkan tingkat kebutuhan aplikasi web kita. Dan juga kita belajar bagaimana para developer umumnya menggunakan trik-trik tertentu agar deployment aplikasi web siap dengan “predefined data”. 
Pemanfaatan spring-security selain untuk melindungi data aplikasi web yang kita buat dari tangan-tangan jahil, juga ada fungsi lain yang tidak kalah penting, yaitu penggunaan level hak akses untuk melindungi suatu data tertentu, seperti ada user yang hanya boleh melihat atau mengedit suatu data tertentu dan ada user lain yang tidak boleh melakukannya. Pengaturan semacam itu sangat mungkin dilakukan dengan spring-security berdasarkan prinsip-prinsip hak akses. Dan ini akan menjadi salah satu pembahasan kita selanjutnya dalam artikel ini. 
Selain pembelajaran mengenai implementasi level hak akses, kita juga akan mencoba membuat beberapa kustomisasi, antara lain kustomisasi form-login, kustomisasi error page untuk memberitahu user bahwa mungkin user tersebut tidak memiliki hak akses yang cukup untuk mengakses suatu data. 
Sebagai bahasan pertama kita akan belajar menggunakan fitur level hak akses untuk membatasi suatu data yang boleh di akses. Kita akan membuat sebuah url baru, dan url ini hanya boleh diakses oleh user-user yang memiliki role sebagai ROLE_ADMIN, artinya user-user yang tidak memiliki ROLE_ADMIN ini tidak boleh mengakses data tersebut.
Url baru yang akan kita buat adalah “http://localhost:8080/admin/**”, url ini merupakan suatu url path pertama yang boleh diakses oleh user-user dengan ROLE_ADMIN. Dalam url ini nanti akan ada path contohnya “/admin/user/list” atau “/admin/user/edit?u=user” dan lain-lain. Path kedua dalam contoh url tersebut merupakan path url untuk melakukan fungsi penambahan, modifikasi atau penghapusan data user standar, hanya hak akses saja yang membedakan url ini. Langkah pertama yang akan kita lakukan adalah memodifikasi class SecurityConfig agar url baru ini terlindungi dari user-user yang memiliki role selain ROLE_ADMIN. Adapun modifikasi class SecurityConfig ini adalah sebagai berikut:
@Override
protected void configure(HttpSecurity http)throws Exception{
    http.authorizeRequests()
            .antMatchers("/").permitAll() //izinkan semua
            .antMatchers("/css/**").permitAll() //izinkan semua
            .antMatchers("/js/**").permitAll() //izinkan semua
            .antMatchers("/fonts/**").permitAll() //izinkan semua
            .antMatchers("/login").permitAll() //izinkan semua (termasuk default spring-security login page
            .antMatchers("/admin/**").hasRole("ADMIN")
            .anyRequest().authenticated() //selain yang di atas harus authenticated
            .and()
            .formLogin()
            .and()
            .logout() //logout config
            .logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
            .logoutSuccessUrl("/");
}
Perhatikan penambahan baris .antMatchers(“/admin/**”).hasRole(“ADMIN”) pada fungsi configure di atas, dengan cara seperti ini maka kita sudah melindungi url tersebut dari user-user selain user dengan ROLE_ADMIN. Untuk mengujinya kita akan membuat sebuah controller baru dengan nama UserManagementController dan berikut ini isi dari class tersebut:
package org.josescalia.blog.simple.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
/**
 * Created by josescalia on 27/02/16.
 */
@Controller
@RequestMapping("/admin/")
public class UserManagementController {
    
    @RequestMapping(value = "/user/test_auth", method = RequestMethod.GET)
    public @ResponseBody String testAuth(){
        return "Halaman ini hanya boleh diakses oleh user yang memiliki ROLE_ADMIN";
    }
    
}
Perhatikan anotasi @RequestMapping pada deklarasi class UserManagementController tersebut, kita meletakkan path pertama sebagai url/admin/”, dengan cara seperti ini maka nantinya setiap fungsi dalam controller ini hanya menambahkan yang perlu-perlu saja, sebab url dengan awalan “/admin/” sudah kita definisikan secara global pada deklarasi class UserManagementController ini. Untuk fungsi testAuth pada class controller tersebut, sederhana saja jika urlhttp://localhost:8080/admin/user/test_auth” dipanggil dalam browser, maka akan menampilkan sebuah teks dengan tulisan “Halaman ini hanya boleh diakses oleh user yang memiliki ROLE_ADMIN”. Sekarang silahkan jalankan aplikasi web tersebut, dan eksekusi urlhttp://localhost:8080/admin/user/test_auth” pada browser anda, jika kita disodorkan form-login masukkanlah usernameadministrator” dan passwordadmin123” dan lihatlah apakan yang terjadi seperti gambar saya dibawah ini:
Jika iya, sekarang eksekusi url "http://localhost:8080/logout" kemudian eksekusi lagi url sebelumnya “http://localhost:8080/admin/user/test_auth” tapi kali ini dalam form-login masukkan user dengan usernameuser” dan password-nya “password”, apakah hasilnya seperti gambar dibawah ini?
Jika ya, maka sesungguhnya kita sudah selesai menerapkan hak akses berdasarkan level atau role, sederhana dan mudah bukan?. Ketika kita memasukkan usernameadministrator” pada tes pertama dan jika username dan password benar, maka kita diijinkan untuk melihat halaman tersebut, sebab salah satu role dari useradministrator” adalah ROLE_ADMIN, dan ketika kita memasukkan usernameuser” pada percobaan kedua, jika login berhasil maka user ini hanya memiliki ROLE_USER saja, itulah yang menyebabkan halaman tersebut tidak bisa diakses seperti gambar kedua. Dan perhatikan benar-benar tulisan tersebut, ada tulisan “Access is Denied” dan status=403 di dalamnya. Dan halaman pada gambar kedua tersebut merupakan halaman default error untuk tipe http status error 403 (Access Denied Page).
Untuk pembahasan kedua kita akan melakukan kustomisasi dua halaman default aplikasi web kita, yaitu form-login spring-security dan halaman 403 itu sendiri. Kustomisasi form-login bisa kita mulai dengan memodikasi class SecurityConfig sebagai langkah pertama. Berikut modifikasi class SecurityConfig tersebut.
@Override
protected void configure(HttpSecurity http)throws Exception{
    http.csrf().disable() //disable cross site request forgery (csrf) protection
            .authorizeRequests()
            .antMatchers("/").permitAll() //izinkan semua
            .antMatchers("/css/**").permitAll() //izinkan semua
            .antMatchers("/js/**").permitAll() //izinkan semua
            .antMatchers("/fonts/**").permitAll() //izinkan semua
            .antMatchers("/login").permitAll() //izinkan semua (termasuk default spring-security login page
            .antMatchers("/admin/**").hasRole("ADMIN")
            .anyRequest().authenticated() //selain yang di atas harus authenticated
            .and()
            .formLogin()
            .loginPage("/login")
            .and()
            .logout() //logout config
            .logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
            .logoutSuccessUrl("/");
}
Bisakah anda mencari tahu baris manakah yang kita tambahkan pada fungsi configure di atas?, ya kita menambahkan baris .loginPage('login') setelah baris .formLogin() pada fungsi configure tersebut, dan ada juga baris .csrf().disable() pada baris-baris awal konfigurasi tersebut. Untuk baris .csrf().disabled(), sebetulnya ini adalah fitur keamanan yang disediakan oleh spring-security untuk membuat keamanan data lebih ketat lagi berkaitan dengan Cross Site Request Forgery, tapi untuk latihan ini kita disable saja terlebih dahulu, sebab bila kita enable akan banyak perubahan yang kita lakukan dalam form-form yang sebelumnya kita buat, lain kali akan kita bahas mengenai fitur .csrf ini. 
Kemudian langkah selanjutnya adalah membuat sebuah fungsi baru pada class IndexController yang berfungsi memanggil form-login yang kita kustomisasi. Berikut ini fungsi baru pada class IndexController.
…
@RequestMapping(value = "/login", method = RequestMethod.GET)
public String loginPage(){
    return "login";
}
…
Fungsi baru yang kita buat pada class IndexController tersebut memiliki mapping url sebagai “/login” dan menggunakan sebuah template bernama login.ftl yang ada dalam foldersrc/main/resources/templates/”. Dan pastinya kita mesti buat file login.ftl tersebut, adapun isi dari file login.ftl ini adalah sebagai berikut:
<html>
<head>
    <link rel="stylesheet" href="${springMacroRequestContext.contextPath}/css/bootstrap.min.css">
    <link rel="stylesheet" href="${springMacroRequestContext.contextPath}/css/style.css">
    <title>Login Form</title>
</head>
<body>
<#if springMacroRequestContext.getQueryString()?exists >
<div class="alert alert-danger alert-dismissible" role="alert" style="width:45%;margin:10px auto">
    <button type="button" class="close" data-dismiss="alert"><span aria-hidden="true">&times;</span><span
            class="sr-only">Close</span></button>
    <strong>Login Failed</strong><br>Please Check your username and password
</div>
</#if>
<div class="container">
    <div class="row">
        <div class="col-lg-4 col-lg-offset-4 col-md-6 col-md-offset-2  col-sm-12">
            <div class="panel panel-primary" style="margin:50px auto;">
                <div class="panel-heading text-center">
                <h1 class="text-center"
                    style="background-color:white;color:#a9a9a9;font-size:60px;width:90px;height:85px;border-radius:50%;padding-top:8px;margin:30px auto;  ">
                    <span class="glyphicon glyphicon-user"></span>
                </h1>
                    <h3 class="text-center">Login Page</h3>
                </div>
                <div class="panel-body">
                    <form name='f' action="login" method='POST' class="form-inline">
                        <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}">
                        <div class="input-group">
                            <span class="input-group-addon"><span class="glyphicon glyphicon-user"></span></span>
                            <input type='text' id="username" name='username' value='' placeholder="username" class="form-control">
                        </div>
                        <p>&nbsp;</p>
                        <div class="input-group">
                            <span class="input-group-addon"><span class="glyphicon glyphicon-lock"></span></span>
                            <input type='password' class='form-control' name='password' id='password' placeholder="Password"/>
                        </div>
                        <p>&nbsp;</p>
                        <div>
                            <input name="submit" type="submit" value="Login" class="btn btn-primary"/>
                        </div>
                    </form>
                </div>
            </div>
        </div>
    </div>
</div>
<script type="text/javascript" src="${springMacroRequestContext.contextPath}/js/jquery-1.9.1.min.js"></script>
<script type="text/javascript" src="${springMacroRequestContext.contextPath}/js/bootstrap.min.js"></script>
<script type="text/javascript" ></script>
</body>
</html>
Ada beberapa catatan untuk kode-kode pada file login.ftl yang kita buat ini, diantaranya adalah penggunaan beberapa directive dan variable default freemarker seperti ${springMacroRequestContext.contextPath} atau ${springMacroContextRequest.getQueryString()?exists}. Directives dan variabel freemarker yang kita buat ini adalah freemarker predefined directives, sementara penggunaan ?exists pada salah satu directives tersebut adalah trik untuk memeriksa apakah variabel query string pada directives springMacroContextRequest ada atau tidak.
Silahkan jalankan aplikasi web kita ini dan bukalan alamat url http://localhost:8080/login pada browser anda, seharusnya tampilan halaman login sudah bukan lagi milik spring-security secara default, melainkan form-login yang sudah kita kustomisasi seperti gambar dibawah ini:
Bukankah tampilan form-login tersebut lebih menarik dari tampilan form-login default milik spring-security?. Ok kita akan jabarkan dahulu apa-apa yang telah kita buat sebelumnya. Jadi begini “jika ada user yang me-request form-login, maka spring-security akan mengarahkan ke url “/login” dimana url tersebut telah kita mapping di dalam class IndexController dan ujungnya akan menampilkan template form (login.ftl) seperti gambar diatas, demikian juga dengan jika belum ada session security, namun user berusaha untuk mengakses suatu halaman yang diproteksi oleh spring-security, spring-security akan mengarahkan request tersebut ke form ini, sebab memang konfigurasi spring -security membuat url/login” menjadi halaman default form-login”.

Pembahasan selanjutnya kita akan melakukan kustomisasi error page Access is Denied (403), sehingga tampilan error page tersebut tidak lagi menggunakan halaman default spring-security. Langkah pertama yang kita lakukan adalah memodifikasi lagi class SecurityConfig menjadi seperti berikut ini:
@Override
protected void configure(HttpSecurity http)throws Exception{
    http.csrf().disable()
            .authorizeRequests()
            .antMatchers("/").permitAll() //izinkan semua
            .antMatchers("/css/**").permitAll() //izinkan semua
            .antMatchers("/js/**").permitAll() //izinkan semua
            .antMatchers("/fonts/**").permitAll() //izinkan semua
            .antMatchers("/login").permitAll() //izinkan semua (termasuk default spring-security login page
            .antMatchers("/admin/**").hasRole("ADMIN")
            .anyRequest().authenticated() //selain yang di atas harus authenticated
            .and()
            .formLogin()
            .loginPage("/login")
            .and()
            .logout() //logout config
            .logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
            .logoutSuccessUrl("/")
            .and()
            .exceptionHandling().accessDeniedPage("/403");
}
Yap, perhatikan dua baris paling bawah pada kode-kode diatas, kita menambahkan suatu statement yang berfungsi mengalihkan exception Access Denied ke url/403”, jadi jika ada user yang tidak berhak mengakses suatu url dalam aplikasi web kita, maka spring-security akan mengalihkan user tersebut ke halaman “/403”. Kemudian kita harus membuat mapping url ini dalam controller, sekali lagi kita modifikasi class IndexController dengan menambahkan sebuah fungsi untuk menangani url tersebut, adapun tambahan fungsi tersebut adalah sebagai berikut:  
...
@RequestMapping("/403")
public String get403Page(){
    return "error/access_denied";
}
...
Sederhana bukan?, kita akan menggunakan file freemarker template bernama access_denied.ftl yang ada pada foldersrc/main/resources/templates/error/”, jika belum ada folder dan file tersebut dalam source aplikasi web anda, maka buatlah. Adapun isi dari file access_denied.ftl ini adalah sebagai berikut:
<!DOCTYPE html>
<html lang="en">
<head>
    <link rel="stylesheet" href="${springMacroRequestContext.contextPath}/css/bootstrap.min.css">
    <link rel="stylesheet" href="${springMacroRequestContext.contextPath}/css/style.css">
    <meta charset="UTF-8">
    <title>Access Denied</title>
</head>
<body>
    <div class="container">
    <h1 class="text-center text-danger">403</h1>
    <hr>
    <h4 class="text-center text-danger">You're not authorize to view this page</h4>
    <p class="text-center"><a href="/">Back</a> </p>
    </div>
</body>
</html>
Jika sudah, maka mari kita tes aplikasi web kita ini, jalankan aplikasi web ini dan buka alamat "http://localhost:8080/admin/user/test_auth", namun pas pada saat login form yang sudah kita kustomisasi tersebut muncul, masukkan usernameuser” dan passwordpassword”, kita ingin melihat apakah halaman 403 tersebut akan muncul karena usernameuser” tidak memiliki akses sebagai ROLE_ADMIN, pada browser saya pun terlihat seperti gambar berikut ini:
Hmm, lebih baik tampilannya dari pada tampilan default milik spring-security bukan?. Sampai disini kita telah selesai mempelajari bagaimana meng-kustomisasi halaman-halaman default spring-security dalam hal ini tentunya form-login dan halaman Access Denied. Silahkan teruskan berlatih dan mengembangkan untuk aplikasi web yang lebih bagus, efektif dan efisien. Semoga apa-apa yang telah kita lakukan pada tulisan ini menjadi bermanfaat buat kita semua.

Depok, 28 Februari 2016

Salam 

Josescalia

No comments: