/blog/*

Spring Security Basic

Keamanan merupakan hal mutlak dan tidak perlu dikompromi lagi dalam membangun suatu website/aplikasi. Dalam ekosistem Spring framework, terdapat module yang sangat powerful untuk menangani aspek2 keamanan seperti proses autentikasi hingga access control. Module tersebut adalah spring-security. Sebelum lanjut lebih jauh lagi, tambahkan dependency spring-security pada Project Spring kita:

<dependencies>
    ...
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
</dependencies>

EnableWebSecurity

Untuk mengaktifkan dan mengkonfigurasi module spring-security, buat class konfigurasi yang di-extends dengan class WebSecurityConfigurerAdapter dan tambahkan annotation @EnableWebSecurity. Class inilah yang merupakan central dari module spring-security, dimana semua konfigurasi security dideklarasikan pada class ini.

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        ...
    }

    @Override
    public void configure(WebSecurity web) {
        ...
    }

}

Terdapat 2 method configure() yang harus di override dimana method tersebut memiliki perbedaan pada parameternya. Keduanya memiliki fungsi masing-masing dimana kedepan kita akan melihat kegunaan dari masing-masing override method tersebut.

Authorization

Dalam konsep security terdapat proses authorization, dimana disini kita mendaftarkan(grant) role atau hak akses tertentu pada suatu user untuk mengakses suatu resource yang terproteksi. Proses authorization dideklarasikan pada method configure(HttpSecurity http). Berikut contohnya:

...

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.httpBasic()
            .and()
            .authorizeRequests()
            .antMatchers("/").permitAll()
            .antMatchers("/profile").permitAll()
            .antMatchers("/config").hasRole("ADMIN")
            .anyRequest().authenticated();
}

Pada contoh diatas terdapat 3 endpoint yang dimana index / dan /profile bisa diakses secara bebas tanpa perlu autentikasi maupun role khusus. Sedangkan endpoint /config hanya user yang memiliki role ROLE_ADMIN yang bisa akses. Jika tidak memiliki hak akses yang sesuai, maka secara otomatis Spring akan mengembalikan status 403.

Kita bisa mendeklarasikan berbagai macam pola endpoint pada method antMatchers() karena mendukung Ant-style path patterns. Kita akan membahas lebih detail mengenai pattern antMatchers() pada tulisan terpisah.

Secara default spring-security akan memproteksi semua endpoint, namun kita bisa mendeklarasikan endpoint tertentu agar tidak perlu diproteksi secara default. Sebagai contoh sederhana, endpoint untuk akses file .css maupun asset image yang memang diperuntukan untuk public.

...

@Override
public void configure(WebSecurity web) {
    web.ignoring()
            .antMatchers("/img/**", "/css/**");
}

note: contoh program pada tulisan ini menggunakan spring-web dan template engine Thymeleaf namun tidak dibahas lebih detail, karena hanya fokus pada meteri spring-security saja.

UserDetailsService

Sebelum masuk proses autentikasi, bagaimana mendeklarasikan user-user beserta hak aksesnya? ada berbagai macam cara. Kita akan bahas satu-persatu. Pertama adalah, secara default spring-security menyediakan 1 'user' dengan username user namun tidak memiliki role apapun. Untuk password dari 'user' ini akan diprint ketika pertama kali menjalankan project Spring:

Using generated security password: ed9daf78-ee8a-4b48-8ab6-2239c382d873

kita bisa meng-override user default ini melalui application.properties:

spring.security.user.name=user
spring.security.user.password=secret
spring.security.user.roles=ADMIN

Untuk kebutuhan yang lebih advance, spring-security menyediakan class UserDetailsService. Spring akan memanggil class ini untuk mengakses semua user yang terdaftar beserta role-nya. Maka dari itu kita cukup menyediakan bean dari class ini.

@Bean
protected UserDetailsService userDetailsService() {

    UserDetails user = User.builder()
            .username("user")
            .password(passwordEncoder().encode("secret"))
            .roles("USER")
            .build();

    UserDetails admin = User.builder()
            .username("admin")
            .password(passwordEncoder().encode("secret"))
            .roles("ADMIN")
            .build();

    return new InMemoryUserDetailsManager(user, admin);
}

@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}

...

Contoh diatas kita menggunakan class InMemoryUserDetailsManager yang merupakan turunan dari class UserDetailsService. Class InMemoryUserDetailsManager digunakan untuk mendaftarkan user secara hardcoded yang akan disimpan pada memory selama project Spring running. Selain itu kita juga menyediakan bean PasswordEncoder agar Spring tahu algoritma apa yang digunakan untuk hash password yang disimpan.

Tentu kita juga bisa menyimpan user pada suatu database untuk kebutuhan yang lebih kompleks dan dinamis. Untuk hal ini kita akan bahas lebih detail pada tulisan berikutnya.

Authentication

Autentikasi merupakan proses untuk mengenali identitas suatu user melalui mekanisme "pembuktian" dimana biasanya user akan memberikan credential-nya untuk proses validasi. Ada berbagai macam mekanisme, salah satunya yang paling umum dan banyak digunakan adalah mekanisme username-password.

Kembali pada Spring, secara default proses autentikasi pada spring-security menggunakan basic auth. Agar lebih jelas berikut contohnya:

curl --user admin:secret http://localhost:8080/config

Cara kerja dari basic auth adalah dengan meng-encode username dan password (dipisah dengan karakter :) menggunakan base64. Hasilnya akan disisipkan pada request header Authorization: Basic [encoded].

Jika kita perhatikan mekanisme basic auth ini mengharuskan kita mengirimkan username dan password setiap kali ingin mengakses suatu resource yang dimana ini bisa menjadi celah keamanan(Vulnerability). Kita bisa non-aktifkan default mekanisme autentikasi ini, dan menggunakan mekanisme lain.

...

@Override
protected void configure(HttpSecurity http) throws Exception {
     http.httpBasic().disable()
            .authorizeRequests()
            .antMatchers("/").permitAll()
            .antMatchers("/profile").permitAll()
            .antMatchers("/config").hasRole("ADMIN")
            .anyRequest().authenticated()
            .and()
            .formLogin();
}

Pada contoh konfigurasi diatas kita non-aktifkan basic auth dengan perintah httpBasic().disable() dan mengaktifkan formLogin(). Mekanisme form login mirip dengan basic auth dimana kita harus menyediakan username dan password namun bedanya form login hanya perlu 1x saja. Jika proses autentikasi form login dinyatakan valid, maka server akan return sebuah cookie yang bernama JSESSIONID. Value dari cookie JSESSIONID inilah yang akan kita kirimkan tiap kali ingin mengakses suatu resource sebagai bentuk credential.

Customize

Ekosistem Spring sudah menyediakan berbagai kebutuhan siap pakai sehingga kita bisa fokus pada bisnis proses utama. Namun tentu saja Spring mendukung berbagai kostumisasi. Sebagai contoh sederhana, Spring menyediakan interface form login siap pakai lengkap dengan bisnis prosesnya. Kita bisa dengan mudah men-customize hal tersebut. Berikut contohnya:

@Override
protected void configure(HttpSecurity http) throws Exception {
     http.httpBasic().disable()
            .authorizeRequests()
            .antMatchers("/").permitAll()
            .antMatchers("/profile").permitAll()
            .antMatchers("/config").hasRole("ADMIN")
            .anyRequest().authenticated()
            .and()
                .formLogin()
                .loginPage("/login")
                .loginProcessingUrl("/perform-login")
                .defaultSuccessUrl("/", true)
                .permitAll()
            .and()
                .logout()
                .logoutUrl("/perform-logout")
                .logoutSuccessUrl("/")
            .and()
                .exceptionHandling().accessDeniedPage("/access-denied");

}

Semua kode program lengkap dari contoh diatas dapat diakses disini.

Ref: