Securing Application With SpringBoot
Creating our own users with a costume security policy
Spring Security has a highly pluggable architecture;
- Defining the source of users
- Creating access rules for the users
- Associating various parts of the app with the access rules
- Applying the policy to all aspects of the application
Creating source of users
Mari sekarang kita mulai dengan membuat source of users
Spring Security memiliki sebuah interface untuk melakukan tugas pembuatan user. UserDetailsService
. Untuk memanfaat interface tersebut kita akan membuat SercurityConfig.java
class dengan kode dibawah ini.
Code
@Configuration // What is this annotation (1)
public class SecurityConfig {
// What is @Bean and UserDetailsService (2)
@Bean
public UserDetailsService userDetailsService(){
UserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();
/**
* (3)
**/
userDetailsManager.createUser(
User.withDefaultPasswordEncoder().username("user").password("password").roles("UMUM").build());
userDetailsManager.createUser(
User.withDefaultPasswordEncoder().username("admin").password("password").roles("ADMIN").build());
return userDetailsManager;
}
}
-
@Configuration
adalah anotasi spring untuk memberi sinyal bahwa class ini adalah source of bean definition, bukan application code. Spring boot akan mendeteksinya saat component scanning dan secara otomatis menambahkan semua bean definition-nya ke application context.English version
is Spring’s annotation to signal that this class is a source of bean definitions rather than actual application code. Spring Boot will detect it through its component scanning and automatically add all its bean definitions to the application context.
-
UserDetailsService
adalah Spring interface securoty untuk mendefinisikan source of users. Definisi bean ini, ditandai dengan@Bean
, yang membuatInMemoryUserDetailsManager
.English version
xUserDetailsService is Spring Security’s interface for defining a source of users. This bean definition, marked with
@Bean
, creates InMemoryUserDetailsManager. -
Memanfaat
InMemoryUserDetailsManger
, kita dapat membuat beberapa user. Setiap user memiliki username, password dan role. Kode fragment diats juga menggunakanwithDefaultPasswordEncoder()
funcion untuk menghindari encoding password.English version
Using InMemoryUserDetailsManager, we can then create a couple of users. Each user has a username, a password, and a role. This code fragment also uses the withDefaultPasswordEncoder() method to avoid encoding the password.
What’s important to understand is that when Spring Security gets added to the classpath, Spring Boot’s autoconfiguration will activate Spring Security’s annotation. This switches @EnableWebSecurity on a standard configuration of various filters and other components.
One of the beans required is a UserDetailsService bean.
Tip
In the code so far, we used withDefaultPasswordEncoder()
to store passwords in the clear. Do NOT do this in production! Passwords need to be encrypted before being stored. In fact, there is a long and detailed history of the proper storage of passwords that reduces the risk of not just sniffing out a password but guarding against dictionary attacks. See https://springbootlearning.com/password-storage for more details on properly securing passwords when using Spring Security.
Sekarang mari kita coba jalankan aplikasi dengan mengclick kanan pada method publi static void main()
method dan run atau menggunakan `./mvnw spring-boot:run
pada terminal. Kungjungi localhost:8080
, maka aplikasi akan melakukan redirect kealamant localhost:8080/login
dan akan muncul login page dibawah ini.
Swapping hardcoded users with a Spring Data-backed set of users
Membuat user secara hardcoded sangat tidak disarankan. Lebih baik dalam mengelola user management menggunakan external database.
Dengan memisahkan aplikasi dengan sumber daya luar dalam mengelola autentikasi user, memungkinkan oranglain (dalam tim kita) seperti security enginerring, untuk mengelola user menggunakan tools laiinnya untuk mengelola database.
Pemisahan user management dari user authentication adalah cara yang tepat untuk meningkatkan keamanan sistem. InsyaAllah, kita akan mengkombinasikan teknik yang telah kita pelajari pada catatan ini sebelumnya dengan interface UserDetailService
yang baru saja kita lihat pada sesi catatan tepat sebelum ini.
Sekarang mari kita buat class entities untuk UserAccount.
Code
Kode diatas terdari dari;
- Sebagaimana yang telah kita ketahui,
@Entitiy
adalah anotasi Java Persistance Api untuk menujukan class yang akan dipetakan kedalam relational table. - Primary key ditantadi dengan anotasi
@Id
, dan@GeneratedValue
memberitahu JPA agar memberikan nilai acak dan unik untuk field tersebut. - Class tersebut memiliki field
username
,password
dan daftarauthorites
- Karena field
athorites
bertipeCollection
, JPA 2 menawarkan cara yang mudah untuk menangani menggunakan anotasi@ElementCollection
. Semua otoritas pada daftar ini akan ditempatkan pada table yang terpisah. n
Sebelum kita meminta Sprint Security untuk mengambil user data. Kita harus membuat Spring Data JPA repository yang ditunjukan untuk user manager. Kita harus membuat interface UserManagerRepository.
Kode diatas, kita menurunkan Spring Data JPA, JpaRepository
yang menyediakan semua operasi yang dibutuhkan oleh berbagai alat untuk memanage user.
Selanjutnya kita membutuhkan Spring Boot agar menjalankan beberapa kode ketika aplikasi dijalankan. Tambahkan beberapa kode dibawah ini pada class SecurityConfiguration
Code
@Bean
CommandLineRunner initUsers(UserManagementRepository repository){
return n -> {
repository.save(new UserAccount("user","paswword","ROLE_USERS"));
repository.save(new UserAccount("admin","paswword","ROLE_ADMIN"));
};
}
Warning
Jalankan method ini hanya sekali saat kita menjalankan applikasi. Dapat meng-comment method ini jika data user sudah masuk kedalam database. Dapat menyebabkan error jika ada dua user yang tidak unique.
The preceding bean defines Spring Boot's CommandLineRunner (through a Java 8 lambda function).
Tip
CommandLineRunner
adalah single abstract method (SAM),artinya class tersebut hanya memiliki satu method yang harsu didefinisikan. Peraturan ini membuat kita instantiate CommandLineRunner menggunakan lambda expression dari pada harus membuat anonymous class.
Diatas, kita memiliki bean definition yg bergantung pada UserManagementRepository
. Didalam bean definition tersebut, kita memliki lambda expression yang mana menggunakan repository untuk menyimpang dua buah entries: satu user dan satu admin account. Denga entries ini, kita dapat membuat kode JPA-Oriented UserDetailService
.
Untuk mengambil UserAccount
. Kita butuh spring data repository lainnya (memisahkan antara delete, save dengan select). Repository ini sangat sederhana, yaitu hanya untuk mengambil data user saja. Kita buat interface class UserRepository
.
Code
Berbeda dengan UserManagementRepository
, interface ini menurunkan Repository
dari pada JpaRepository
. Artinya Interface ini tidak ber isi apa2, keculi apa yang ada pada interface ini. (Tidak ada method turunan). kode diatas hanya berisi method findByUsername
yang mengamblikan Entitiy UserAccount
berdasarkan parameter username.
Selanjutnya kita dapat membuat bean definition yang membuat kita dapat mengganti UserDetailsService
yang telah kita buat sebelumnya di Creating Source of Users (1)
- Me-replace method dibawa ini
@Bean public UserDetailsService userDetailsService(){ UserDetailsManager userDetailsManager = new InMemoryUserDetailsManager(); /** * (3) **/ userDetailsManager.createUser( User.withDefaultPasswordEncoder().username("user").password("password").roles("UMUM").build()); userDetailsManager.createUser( User.withDefaultPasswordEncoder().username("admin").password("password").roles("ADMIN").build()); return userDetailsManager; }
Code
@Bean
UserDetailsService userService (UserRepository userRepository){
System.out.println("Its get all User");
return username -> {
UserAccount account = userRepository.findByUsername(username);
try {
System.out.println("The user name :"+account.getUserName());
System.out.println("The user name :"+account.getPassword());
return account.asUser();
}
catch (Exception e){
throw new UsernameNotFoundException("User tidak ada coy");
}
};
}
Bean definition diatas memanggil UserRepository
. Selanjutnya kita menulis lambda expression yang mengembalikan UserDetailsService. Jika kita melihat pada inerface UsersDetailsSerive, kita akan menemukan lagi Singgle Abstract Method, method tunggal dengan nama loadUserByName
, mengubah string berdasarkan username filed kedalam objek UserDetails
. Interface method tersebut menerima argumen username yang dapat kita lanjutkan ke UserRepository.
Code
Interface UserDetails
adalah Representasi Spring Secuirty dari user's information. Termasuk username, password, authoriteis, dan beberapa nilai boolean yang merepresentasikan locked, expired dan enabled.
Code
Mari baca ini dengan seksama. userService
bean yang telah kita buat diatas pada SecurityConfiguration.java
menghasilkan UserDetailsService
bean, bukan objek Userdetails itu sendiri(1). Artinya method tersebut akan mengambalika data dari user.
- Dapat kita lihat dari return typenya
UserDetailsService
Lambda expression didalam bean definition mentransformasi kedalam UserDetailsService
. Function loadUserName()
, fungsi yang menerika username sebagai input dan mengasilkan UserDetails
object sebagai nilai yang dikembalikan. Jika seseorang memasukan username saat login prompt. Nilai tersebutlah yang akan diberikan pada fungsi ini.
Repository class mengambil peranan pentin dalam mengambil UserAccount
dari database berdasarkan username yang diterima. Agar Sprint Security dapat berkerja dengan entity ini, hasil dari entitiy tersebut harus dikonversi ke SpringSecurity User object yang mana mengimplementasi UserDetails Interface.
Maka dari itu, kembalie ke entitiy class UserAccount
dan buat method baru dengan nama asUser()
untuk mengkonversi-nya ke UserDetails
object.
Code
Method diatas secara sederhana membuat Spring Security Userdetails object. Sekarang, jalankan aplikasi, lalu menuju ke lolcahost:8080
, masukan username dan password sesuai dengan username dan password yang ada pada database.
Result
Alhamdulillah, kita telah mendefinisikan source of users. Selanjutnya kita akan mendefinisikasn access roles.
Securing web routes and HTTP verbs
Membatasi aplikasi dan hanya memberikan akses ke user tertentu membutuhkah langkah yang besar. InsyaAllah kita akan membahas sedikit tentang security yang harus kita terapkan di aplikasi yang sesungguhnya, yaitu authorization.
Spring security membuat langkah bersar tersebut menjadi lebih ringkas. Langkah pertama adalah dengan mengatur kebijakan keamanan dengan menambahkan bean definition ke class SecurityConfig
.
Spring Boot memiliki tempat khusus untuk mengkonfigurasi kebijakan keamanan. Tambahkan bean definition tipe SecurityFilterChain
ini pada class SecurityConfig
.
Code
@Bean
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity httpSecurity) throws Exception{
try {
System.out.println("This bean will start when server is started");
// Emable form login to permitAll
httpSecurity.formLogin(AbstractAuthenticationFilterConfigurer::permitAll);
httpSecurity.httpBasic(Customizer.withDefaults());
return httpSecurity.build();
}
catch (Exception e){
e.printStackTrace();
throw new Exception("Error");
}
}
SecurityFilterChain
adalah tipe bean yang dibutuhkan untuk mendifinisikan Spring Security Policy- Untuk membuat kebijakan, kita meminta dari Spring Secuiryt
HttpSecurity
bean. Bean tersebut akan memberikan kita kebebasan dalam membuat aturan untuk mengatur aplikasi. - In addition to that, the formLogin and httpBasic directives are switched on, enabling both HTTP Form and HTTP Basic, two standard authentication mechanisms
HttpSecurity
Builder, dengan setting ini, akan digunakan untuk me-renderSecurityFilterChain
melalui semua servlet request.
More about security look here
Perhatikan baik-baik. Berikut ini akan diterangkan secara detail dari formLogin
dan httpBasic
yang telah kita gunakan pada snippet diatas.
-
Form Authentication membutuhkan form dalam bentuk HTML pada umumnya. Bahkan, Spring security menyediakan form bawaan (sebagaimana yang kita gunakan pada catatan ini).
-
Basic Authenticaiton tidak berurusan dengan HTML dan form, akantetapi melibatkan popup (js) (1) yang tertanam secara bawaan pada setiap browser.
Kebijakan keamanan yang kita buat diatas sangat longgar, yaitu dengan memberikan akses kepada semua user asalkan terdaftar. Namun esensialnya, hak akases diberikan bukan semata pada user yang terautentikasi namun juga pada hak aksesnya masing-masing (role and authority)
Dibawah ini adalah contoh yang lebih spesifik dalam menentukan hak akses pengguna
Code
@Bean
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity httpSecurity) throws Exception{
try {
System.out.println("Build security filter chain");
/**
* Request yang dikhususkan untuk role admin dan super
*/
httpSecurity.authorizeHttpRequests(auth -> {
auth.requestMatchers(HttpMethod.GET,"/add**/**").access((authentication, object) -> {
boolean hitGranted = Stream.of("ADMIN","SUPER").map(s -> {
System.out.println("Allowed role : "+s);
return hasRole(s).check(authentication,object).isGranted();
}).filter(granted -> {
System.out.println("Hasil granted : "+granted);
return granted;
}).findAny().orElse(false);
return new AuthorizationDecision(hitGranted);
}).anyRequest().authenticated();
});
// HTTP Security, login dari HTML
// Mengizinkan semua orang bisa mengakses form login
httpSecurity.formLogin(AbstractAuthenticationFilterConfigurer::permitAll);
// HTTP Security, login melalui CURL
httpSecurity.httpBasic(Customizer.withDefaults());
return httpSecurity.build();
}
catch (Exception e){
e.printStackTrace();
throw new Exception("Error");
}
}
/add**/**
. Pada kali ini kita menggunakan method requestMatchers.access()
. Kita membuat daftar role yang dapat mengakses alamat ini dan menggunakan stream untuk melihat apakah hak ases dari user setidaknya terdiri dari salah satu role berikut, yaitu ADMIN
dan SUPER
. Kita menggunakan static method dari AuthorityAuthorizationManager hasRole()
dan memberikan hasil stream boolean.
Selanjutanya, kita menyaring filter()
stream boolean tersebut yang memiliki nilai true atau dalam kata lain salah satu dari role yang dizikan cocok dengan role user yang bersangkutan. Selanjutanya kita menggunakan fungsi findAny()
yang artinya setidaknya ada tidak yang bernilai true, dan diakhir kita menggunakan fungsi orElse(false)
jika tidak ada role yang cocok.
Keamanan pasti akan selalu menjadi pekerjaan yang kompleks. Oleh karena itu, dilain tersedianya rule atau aturan yang disediakan oleh Spring Boot kita tetap harus dapat membuat atau membentuk costume access checks (aturan yang dibentuk sendiri).
Karena, dari pada kita menggunakan Spring Security untuk menangkap semua kemungkinan-kemungkinan dari aturan2, lebih mudah kita membuat sendiri pengujian dari setiap hak akses user yang telah kita buat tepat pada snippet diatas tulisan ini.
Note
Roles versus authorities: Spring Security has a fundamental concept known as authorities. Essentially, an authority is a defined permission to access something. However, the concept of having ROLE_ADMIN, ROLE_USER, ROLE_DBA, and others where the prefix of ROLE_ categorizes such authorities is so commonly used that Spring Security has a full suite of APIs to support role checking. In this situation, a user who has the authority of ROLE_ADMIN or simply the role of ADMIN would be able to GET any of the admin pages.
Taking ownership of data
Pada bagian ini kita akan membuat fitur delete. Fitur ini dapat menghapus data video namun dengan syarat hanya user yang membuat data tersebut yang dapat menghapusnya. Maka dari itu kita perlu menambahkan satu buah column yang menampung pembuat data video tersebut.
Sebelumnya kita telah membuat videoEntity
yang berisikan Id, nama dan deskripsi. Kita akan menambahkan satu property untuk menampung user pembuat data video. Bukan file VideoEntity.java
dam tambahkan seperti kode dibawah ini
Code
@Entity
@Table(name="video")
public class VideoEntity {
private @Id @GeneratedValue(strategy = GenerationType.IDENTITY) Long id;
private String userName;
private String name;
private String description;
public VideoEntity(String userName, String name, String description){
this.id = null;
this.userName = userName;
this.name = name;
this.description = description;
}
/**
* Selanjutnya berisikan getter and setter
*/
}
Perubahan entitiy class ini akan berdampak pada struktur pada database, yaitu dengan meng-ALTER tabel untuk menambah kolom baru dengan nama userName.
Selanjutnya kita akan merubah VideorService.java
yaitu dengan tujuan agar data video baru yang dimasukan mengambil username dari user ybs dan memasukannya pada database.
Code
/**
* Untuk menambahkan data video
*/
public List<VideoEntity> addVideo (VideoSearch videoSearch, Authentication authentication){ // (1)!
repository.save(new VideoEntity(authentication.getName() ,videoSearch.name(),videoSearch.description()));
return repository.findAll();
}
- Kita menambahkan
Authentication authetication
untuk mengambil nama dari obejek user yang terauthentikasiauthentication.getName()
.
Oke, kita sekarang menuju ke home controller. Disini kita menambahkan paramater Authentication
dan memberikannya kepada method addVideo yang telah kita tambahakn sebelumnya agar dapat menerima objek Authentication tersebut.
Code
Alhamdulillah, sejauh ini untuk membuat fitur delete kita telah memodifikasi pencatatan data video dengan menambahkan data user. Dengan demikian kita dapat membuat aturan dalam menghapus data video hanya terbatas pada user pembuatnnya.
Sekarang mari kita buat dahulu controller untuk mengambil url delete.
Code
@PostMapping("/delete/video/{videoId}")
public String deleteVideo (@PathVariable Long videoId){
System.out.println("Id yang didelete : "+videoId);
videoService.deleteVideo(videoId);
return "redirect:/";
}
@PostMapping("/delete/video/{videoId}")
yang artinya controller ini akan menerima dari URL /delete/video/{videoId}
. Yang baru pada catatan ini adalah penggunakan sebuah nilai yang ada didalam curly bracket. Nilai ini nantinya dinamin, artinya berubah-ubah, dan variabel tersebut dapat kita ambil nilainya menggunakan parameter yang adad di variable tersebut.
Yaitu menggunakan @PathVariable Long videoId
. Parameter ini akan mengikuti path dari request tersebut. Selanjutnya kita memanggil method dari class videoService dan memberikan parameter path variable ke method deleteVideo
Menindaklanjuti snippet diatas, artinya kita harus membuat satu buah method dengan nama deleteVideo pada VideoService.java
Code
VideoRepository repository;
// Depedency injection
public VideoService(VideoRepository repository){
this.repository = repository;
}
public void deleteVideo(Long id){
repository.findById(id).map(data -> {
repository.delete(data);
return true;
}).orElseThrow(() -> new RuntimeException("No video at"));
}
Pada method tersebut kita mengambil entitiy melalui ID yang diberikan. Nilai kembalian dari method tersebut adalah objek Opsional. Untuk me-looping objek entitiy tersebut kita menggunakan fungsi map dari objek opsional tersebut lalau mengisinya dengan lambda function yang berisikan;
- Memanggil method repositry.delete
- Mengambilakan nilai true
Selanjutnya kita memanggil method dari Opsional orElseThrow
yang berisikan lambda function untuk menge- raise exception.
The last step, sekarang kita ke bagian repository untuk membuat sebuah override method delete
Code
@Override
: Anotasi ini memastikan bahwa kita tidang ingin merubah nama dari method atau aspek apapun dari method ini.@PreAuthorize
: Ini adlah anotasi Spring Security method yang membuat kita dapat menulsi sebuah kostum pengujian keamanan.#entity.username
: ini merujuk pada entity argumen pada parameter pertama (entitiy) dan mengambil parameter username menggunakan Java bean properties (Mengambil dari nama pada record video)authentication.name
:Argument security Spring untuk mengakses konteks authentication objek saat ini dan mengakses nama user yang terautentikasi tersebut. (Mengambil nama dari user authentication saat memanggil method ini)
{{Dengan membandingkan username dari videoEntitiy
dengan nama dari user, kita dapat memastikan bahwa method ini hanya akan berkerja jika user yang yang miliki data video tersebut yang dapat menghapusnya}}
Mengaaktifkan Method Security
Apa yang telah kita kerjakan diatas tidak akan berjalan jika kita tidak mengaktifkan method tersebut. Untuk mengaktifkannya kita dapat menuju ke class SecurityConfig.java dan menambahkan anotasi @EnableMethodSecurity.
Menampilkan nama pemilik video
Hmmm, catatan ini agak berbeda dengan buku induknya pada bagian interface. Saya menambahkan pada table menggunakan button form yang berisikan action ke alamat url /delete/video/{videoId}
.
Code
<div class="mt-3 card shadow p-3 mb-5 bg-body rounded">
<div class="card-body">
<table class="table table-striped table-hover">
<tr>
<th>Name</th>
<th>Description</th>
<th>Action</th>
</tr>
{{#videos}}
<tr>
<td>
{{name}}
</td>
<td>
{{description}}
</td>
<td>
<form action="/delete/video/{{id}}" method="post">
<input type="hidden" name="{{_csrf.parameterName}}" value="{{_csrf.token}}"/>
<button type="submit" class="btn btn-danger mt-2">Search</button>
</form>
</td>
</tr>
{{/videos}}
</table>
</div>
</div>
Ingat, pada controller kita memberikan attribute videos (1), dimana entitiy tersebut kita mendapatkan id dari data video yang kita berikan nilai tersebut pada attribute action do tag form.
model.addAttribute("videos", searchResult);
Membuat data sample
Anda dapat membuat data video langsung menggunakan form yang telah kita buat sebelumnya, namun kita juga dapat menggunakan fitur anotasi dari spring boot yaitu @PostConstruct
Code
@PostConstruct
void initDataBase(){
// Mambuat datasample video
repository.save(new VideoEntity("admin2","AADC2","Ada Apa Dengan Cinta"));
repository.save(new VideoEntity("admin","AADC3","Ada Apa Dengan Cinta season 3"));
repository.save(new VideoEntity("admin2","AADC4","Ada Apa Dengan Cinta season 4"));
repository.save(new VideoEntity("admin2","AADC5","Ada Apa Dengan Cinta season 5"));
repository.save(new VideoEntity("admin","AADC6","Ada Apa Dengan Cinta season 6"));
}
@PostConstruct
adalah JakartaEE anotasi yang memberikan sinyal bahwa method ini harus dijalankan setelah aplikasi dijalankan.
Prove
Code
Memanfaatkan Google untuk autentikasi pengguna
sample