Skip to content

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;
    }
}
  1. @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.

  2. UserDetailsService adalah Spring interface securoty untuk mendefinisikan source of users. Definisi bean ini, ditandai dengan @Bean, yang membuat InMemoryUserDetailsManager.

    English version

    xUserDetailsService is Spring Security’s interface for defining a source of users. This bean definition, marked with @Bean, creates InMemoryUserDetailsManager.

  3. Memanfaat InMemoryUserDetailsManger, kita dapat membuat beberapa user. Setiap user memiliki username, password dan role. Kode fragment diats juga menggunakan withDefaultPasswordEncoder() 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.

Login page

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

@Entity
public class UserAccount {
    @Id
    @GeneratedValue
    private Long id;
    private String username;
    private String password;

    @ElementCollection(fetch = FetchType.EAGER)
    private List<GrantedAuthority> authorities = new ArrayList<>();

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 daftar authorites
  • Karena field athorites bertipe Collection, 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.

Code

public interface UserManagementRepository extends JpaRepository<UserAccount, Long> {}

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

public interface UserRepository extends Repository<UserAccount, Long> {
    UserAccount findByUsername(String userName);
}

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)

  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){
    return username -> userRepository.findByUsername(username).asUser();
}
@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

public interface UserDetailsService {
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

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
public interface UserDetails extends Serializable {
    Collection<? extends GrantedAuthority> getAuthorities();

    String getPassword();

    String getUsername();

    boolean isAccountNonExpired();

    boolean isAccountNonLocked();

    boolean isCredentialsNonExpired();

    boolean isEnabled();
}

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.

  1. Dapat kita lihat dari return typenya UserDetailsService
    UserDetailsService userService (UserRepository userRepository){/** same like above */}
    

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

public UserDetails asUser(){
    return User.withDefaultPasswordEncoder()
            .username(getUserName())
            .password(getPassword())
            .authorities(getAuthorities()).build();
}

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

Insert username and Password

Login 2

If login failed

Login 2

If login succsesfully

Login 2

select * from user_account
select * from user_account_authorities
Output
id  password    username
752 password    user
753 password    admin

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");
        }
    }
Penjelasan kode diatas sebagai berikut;

  • 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-render SecurityFilterChain 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.

  1. Form Authentication membutuhkan form dalam bentuk HTML pada umumnya. Bahkan, Spring security menyediakan form bawaan (sebagaimana yang kita gunakan pada catatan ini).

  2. Basic Authenticaiton tidak berurusan dengan HTML dan form, akantetapi melibatkan popup (js) (1) yang tertanam secara bawaan pada setiap browser.

  3. image

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");
        }
    }
Pada bean configuration ini, kita membuat sebuah kode untuk memberikan terbatas pada method GET pada URL /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();
}
  1. Kita menambahkan Authentication authetication untuk mengambil nama dari obejek user yang terauthentikasi authentication.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

@PostMapping("/add-video")
public String add(@ModelAttribute VideoSearch search, Model model, Authentication authentication){
    List<VideoEntity> update = videoService.addVideo(search,authentication);
    model.addAttribute("videos", update);
    return "index";
}

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:/";
}
Kita menggunakan @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"));
}
Fokus hanya ke method deleteVideo saja. Karena depedency inject diatas sebagai informasi saja bahwa kita akan menggunakan variable repository untuk mengambil data dengan id tertentu lalu menggunakan hasil tersebut dalam bentuk entitiy sebagai parameter pada function repository.delete yang membutuhkan parameter entitiy tersebut.

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;

  1. Memanggil method repositry.delete
  2. 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

@PreAuthorize("#entity.userName == authentication.name")
@Override
void delete(VideoEntity entity);
  • @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.

Code

@EnableMethodSecurity
public class SecurityConfig {
    // ... dipotong
}

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.

  1. 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

Code


Output

Code


Output


Output

Code



Output


Output