Aplikasi web di Kotlin + Spring Boot + Vue.js

Selamat siang, penghuni Habr yang terkasih!

Belum lama berselang, saya berkesempatan menerapkan proyek kecil tanpa persyaratan teknis khusus. Yaitu, saya bebas memilih tumpukan teknologi sesuai kebijaksanaan saya. Itu sebabnya saya tidak melewatkan kesempatan untuk "merasakan" yang modis, awet muda, menjanjikan, tetapi tidak akrab bagi saya Kotlin dan Vue.js dalam praktiknya, menambahkan Boot Musim Semi yang sudah akrab dan mencoba semuanya pada aplikasi web langsung.

Ketika saya mulai, saya dengan ceroboh percaya bahwa akan ada banyak artikel dan manual tentang masalah ini di Internet. Bahannya benar-benar cukup, dan semuanya bagus, tetapi hanya sampai pengendali REST pertama. Kemudian kesulitan kontradiksi dimulai. Tetapi bahkan dalam aplikasi sederhana, saya ingin memiliki logika yang lebih kompleks daripada menggambar pada halaman teks yang dikembalikan oleh server.

Setelah menyelesaikannya entah bagaimana, saya memutuskan untuk menulis manual saya sendiri, yang, saya harap, akan bermanfaat bagi seseorang.

Apa dan untuk siapa artikel itu


Materi ini adalah panduan untuk "mulai cepat" mengembangkan aplikasi web dengan backend di Kotlin + Spring Boot dan frontend di Vue.js. Saya harus segera mengatakan bahwa saya tidak "tenggelam" untuk mereka dan tidak berbicara tentang keuntungan nyata dari tumpukan ini. Tujuan artikel ini adalah untuk berbagi pengalaman.

Materi ini dirancang untuk pengembang dengan pengalaman dengan Java, Spring Framework / Spring Boot, React / Angular, atau setidaknya JavaScript murni. Cocok untuk mereka yang tidak memiliki pengalaman seperti itu - misalnya, programmer pemula, tetapi, saya khawatir, maka Anda harus mencari tahu beberapa detail sendiri. Secara umum, beberapa aspek dari panduan ini harus dipertimbangkan secara lebih rinci, tetapi saya pikir lebih baik melakukannya di publikasi lain, agar tidak menyimpang jauh dari topik dan tidak membuat artikel menjadi rumit.

Mungkin itu akan membantu seseorang untuk membentuk ide pengembangan backend di Kotlin tanpa harus menyelami topik ini, dan seseorang - untuk mengurangi waktu kerja, dengan mengambil kerangka kerangka aplikasi yang sudah jadi.

Terlepas dari deskripsi langkah-langkah praktis tertentu, secara umum, menurut pendapat saya, artikel ini memiliki karakter tinjauan eksperimental. Sekarang pendekatan ini, dan pertanyaan itu sendiri terlihat, lebih mungkin sebagai ide hipster - untuk mengumpulkan kata-kata modis di satu tempat. Tetapi di masa depan, mungkin, ia akan menempati ceruknya dalam pengembangan usaha. Mungkin ada programmer pemula (dan berlanjut) di antara kita yang harus tinggal dan bekerja pada saat Kotlin dan Vue.js akan sepopuler dan diminati seperti halnya Java dan React sekarang. Bagaimanapun, Kotlin dan Vue.js benar-benar memiliki harapan yang tinggi.

Selama saya menulis panduan ini, publikasi serupa, seperti yang ini, sudah mulai muncul di jaringan. Saya ulangi, ada cukup banyak bahan di mana urutan tindakan untuk kontroler REST pertama dipahami, tetapi akan menarik untuk melihat logika yang lebih kompleks - misalnya, implementasi otentikasi dengan pemisahan peran, yang merupakan fungsi yang agak diperlukan. Itulah yang saya tambahkan ke kepemimpinan saya sendiri.

Isi




Referensi cepat


Kotlin adalah bahasa pemrograman yang berjalan di atas JVM dan dikembangkan oleh perusahaan internasional JetBrains .
Vue.js adalah kerangka kerja JavaScript untuk mengembangkan aplikasi gaya reaktif satu halaman.


Alat pengembangan


Sebagai lingkungan pengembangan, saya akan merekomendasikan penggunaan IntelliJ IDEA - lingkungan pengembangan dari JetBrains , yang telah mendapatkan popularitas luas di komunitas Java, karena ia memiliki alat dan fitur yang mudah digunakan untuk bekerja dengan Kotlin hingga mengubah kode Java menjadi kode Kotlin. Namun, Anda tidak boleh berharap bahwa dengan cara ini Anda dapat memigrasi seluruh proyek, dan tiba-tiba semuanya akan berjalan dengan sendirinya.

Pemilik bahagia IntelliJ IDEA Ultimate Edition dapat menginstal plugin yang sesuai untuk kenyamanan bekerja dengan Vue.js. Jika Anda mencari kompromi antara harga dan kenyamanan freebie , saya sangat merekomendasikan menggunakan Microsoft Visual Code dengan plugin Vetur .

Saya kira ini jelas bagi banyak orang, tetapi untuk berjaga-jaga, saya mengingatkan Anda bahwa manajer paket npm diharuskan untuk bekerja dengan Vue.js. Petunjuk instalasi untuk Vue.js dapat ditemukan di situs web Vue CLI .

Maven digunakan sebagai pengumpul proyek Java dalam panduan ini, PostgreSQL digunakan sebagai server basis data.


Inisialisasi Proyek


Buat direktori proyek dengan nama, misalnya, kotlin-spring-vue . Proyek kami akan memiliki dua modul - backend dan frontend . Pertama, frontend akan dikumpulkan. Kemudian, selama perakitan, backend akan menyalin sendiri index.html, favicon.ico dan semua file statis (* .js, * .css, gambar, dll.).

Dengan demikian, dalam direktori root kita akan memiliki dua subfolder - / backend dan / frontend . Namun, jangan buru-buru membuatnya secara manual.

Ada beberapa cara untuk menginisialisasi modul backend:

  • secara manual (jalur samurai)
  • Proyek aplikasi Spring Boot dihasilkan menggunakan Spring Tool Suite atau IntelliJ IDEA Ultimate Edition
  • Menggunakan Spring Initializr , menentukan pengaturan yang diperlukan - ini mungkin cara yang paling umum

Dalam kasus kami, konfigurasi utama adalah sebagai berikut:

Konfigurasi modul backend
  • Proyek: Proyek Maven
  • Bahasa: Kotlin
  • Boot Musim Semi: 2.1.6
  • Project Metadata: Java 8, pengemasan JAR
  • Ketergantungan: Spring Web Starter, Spring Boot Actuator, Spring Boot DevTools



pom.xml akan terlihat seperti ini:

pom.xml - backend
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>com.kotlin-spring-vue</groupId> <artifactId>demo</artifactId> <version>0.0.1-SNAPSHOT</version> </parent> <groupId>com.kotlin-spring-vue</groupId> <artifactId>backend</artifactId> <version>0.0.1-SNAPSHOT</version> <name>backend</name> <description>Backend module for Kotlin + Spring Boot + Vue.js</description> <properties> <java.version>1.8</java.version> <kotlin.version>1.2.71</kotlin.version> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <rest-assured.version>3.3.0</rest-assured.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.fasterxml.jackson.module</groupId> <artifactId>jackson-module-kotlin</artifactId> </dependency> <dependency> <groupId>org.jetbrains.kotlin</groupId> <artifactId>kotlin-reflect</artifactId> </dependency> <dependency> <groupId>org.jetbrains.kotlin</groupId> <artifactId>kotlin-stdlib-jdk8</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory> <testSourceDirectory>${project.basedir}/src/test/kotlin</testSourceDirectory> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <mainClass>com.kotlinspringvue.backend.BackendApplicationKt</mainClass> </configuration> </plugin> <plugin> <groupId>org.jetbrains.kotlin</groupId> <artifactId>kotlin-maven-plugin</artifactId> <configuration> <args> <arg>-Xjsr305=strict</arg> </args> <compilerPlugins> <plugin>spring</plugin> </compilerPlugins> </configuration> <dependencies> <dependency> <groupId>org.jetbrains.kotlin</groupId> <artifactId>kotlin-maven-allopen</artifactId> <version>${kotlin.version}</version> </dependency> </dependencies> </plugin> <plugin> <artifactId>maven-resources-plugin</artifactId> <executions> <execution> <id>copy Vue.js frontend content</id> <phase>generate-resources</phase> <goals> <goal>copy-resources</goal> </goals> <configuration> <outputDirectory>src/main/resources/public</outputDirectory> <overwrite>true</overwrite> <resources> <resource> <directory>${project.parent.basedir}/frontend/target/dist</directory> <includes> <include>static/</include> <include>index.html</include> <include>favicon.ico</include> </includes> </resource> </resources> </configuration> </execution> </executions> </plugin> </plugins> </build> </project> 

Perhatikan:

  • Nama kelas utama berakhir pada Kt
  • Menyalin sumber daya dari project_root / frontend / target / dist ke src / main / resources / publik
  • Proyek induk (induk) yang diwakili oleh spring-boot-starter-parent dipindahkan ke level pom.xml utama


Untuk menginisialisasi modul frontend, buka direktori root proyek dan jalankan perintah:

 $ vue create frontend 

Kemudian Anda dapat memilih semua pengaturan default - dalam kasus kami ini sudah cukup.

Secara default, modul akan dikumpulkan di subfolder / dist , namun kita perlu melihat file yang terkumpul di folder / target. Untuk melakukan ini, buat file vue.config.js langsung di / frontend dengan pengaturan berikut:

 module.exports = { outputDir: 'target/dist', assetsDir: 'static' } 

Letakkan file pom.xml dari formulir berikut di modul frontend :

pom.xml - frontend
 <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <artifactId>frontend</artifactId> <parent> <groupId>com.kotlin-spring-vue</groupId> <artifactId>demo</artifactId> <version>0.0.1-SNAPSHOT</version> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> <frontend-maven-plugin.version>1.6</frontend-maven-plugin.version> </properties> <build> <plugins> <plugin> <groupId>com.github.eirslett</groupId> <artifactId>frontend-maven-plugin</artifactId> <version>${frontend-maven-plugin.version}</version> <executions> <!-- Install our node and npm version to run npm/node scripts--> <execution> <id>install node and npm</id> <goals> <goal>install-node-and-npm</goal> </goals> <configuration> <nodeVersion>v11.8.0</nodeVersion> </configuration> </execution> <!-- Install all project dependencies --> <execution> <id>npm install</id> <goals> <goal>npm</goal> </goals> <!-- optional: default phase is "generate-resources" --> <phase>generate-resources</phase> <!-- Optional configuration which provides for running any npm command --> <configuration> <arguments>install</arguments> </configuration> </execution> <!-- Build and minify static files --> <execution> <id>npm run build</id> <goals> <goal>npm</goal> </goals> <configuration> <arguments>run build</arguments> </configuration> </execution> </executions> </plugin> </plugins> </build> </project> 


Dan akhirnya, letakkan pom.xml di direktori root proyek:
pom.xml
 <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.kotlin-spring-vue</groupId> <artifactId>demo</artifactId> <packaging>pom</packaging> <version>0.0.1-SNAPSHOT</version> <name>kotlin-spring-vue</name> <description>Kotlin + Spring Boot + Vue.js</description> <modules> <module>frontend</module> <module>backend</module> </modules> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.3.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <properties> <main.basedir>${project.basedir}</main.basedir> </properties> <build> <plugins> <plugin> <groupId>org.jacoco</groupId> <artifactId>jacoco-maven-plugin</artifactId> <executions> <!-- Prepares the property pointing to the JaCoCo runtime agent which is passed as VM argument when Maven the Surefire plugin is executed. --> <execution> <id>pre-unit-test</id> <goals> <goal>prepare-agent</goal> </goals> </execution> <!-- Ensures that the code coverage report for unit tests is created after unit tests have been run. --> <execution> <id>post-unit-test</id> <phase>test</phase> <goals> <goal>report</goal> </goals> </execution> </executions> </plugin> <plugin> <groupId>org.eluder.coveralls</groupId> <artifactId>coveralls-maven-plugin</artifactId> </plugin> <plugin> <groupId>org.jetbrains.kotlin</groupId> <artifactId>kotlin-maven-plugin</artifactId> <version>${kotlin.version}</version> <executions> <execution> <id>compile</id> <phase>compile</phase> <goals> <goal>compile</goal> </goals> </execution> <execution> <id>test-compile</id> <phase>test-compile</phase> <goals> <goal>test-compile</goal> </goals> </execution> </executions> <configuration> <jvmTarget>1.8</jvmTarget> </configuration> </plugin> </plugins> </build> </project> 

di mana kita melihat dua modul kita - frontend dan backend , dan juga induk - spring-boot-starter-parent .

Penting: modul harus dirangkai dalam urutan ini - pertama frontend, lalu backend.

Sekarang kita dapat membangun proyek:

 $ mvn install 

Dan, jika semuanya sudah terpasang, jalankan aplikasi:

 $ mvn --projects backend spring-boot:run 

Halaman Vue.js default akan tersedia di http: // localhost: 8080 / :




API SISA


Sekarang mari kita buat beberapa layanan REST sederhana. Misalnya, "Halo, [nama pengguna]!" (defaultnya adalah Dunia), yang menghitung berapa kali kami menariknya.
Untuk melakukan ini, kita memerlukan struktur data yang terdiri dari angka dan string - kelas yang tujuan utamanya adalah untuk menyimpan data. Kotlin memiliki kelas data untuk ini . Dan kelas kita akan terlihat seperti ini:

 data class Greeting(val id: Long, val content: String) 

Itu saja. Sekarang kita dapat menulis layanan secara langsung.

Catatan: untuk kenyamanan, ini akan membawa semua layanan ke rute / api yang terpisah menggunakan anotasi @RequestMapping sebelum mendeklarasikan kelas:

 import org.springframework.web.bind.annotation.* import com.kotlinspringvue.backend.model.Greeting import java.util.concurrent.atomic.AtomicLong @RestController @RequestMapping("/api") class BackendController() { val counter = AtomicLong() @GetMapping("/greeting") fun greeting(@RequestParam(value = "name", defaultValue = "World") name: String) = Greeting(counter.incrementAndGet(), "Hello, $name") } 

Sekarang restart aplikasi dan lihat hasilnya http: // localhost: 8080 / api / salam? Nama = Vadim :

 {"id":1,"content":"Hello, Vadim"} 

Kami akan menyegarkan halaman dan memastikan bahwa penghitung berfungsi:

 {"id":2,"content":"Hello, Vadim"} 

Sekarang mari kita bekerja di frontend untuk menggambar hasilnya di halaman.
Instal vue-router untuk mengimplementasikan navigasi pada "halaman" (pada kenyataannya - pada rute dan komponen, karena kita hanya memiliki satu halaman) dalam aplikasi kita:

 $ npm install --save vue-router 

Tambahkan router.js ke / src - komponen ini akan bertanggung jawab untuk perutean:

router.js
 import Vue from 'vue' import Router from 'vue-router' import HelloWorld from '@/components/HelloWorld' import Greeting from '@/components/Greeting' Vue.use(Router) export default new Router({ mode: 'history', routes: [ { path: '/', name: 'Greeting', component: Greeting }, { path: '/hello-world', name: 'HelloWorld', component: HelloWorld } ] }) 


Catatan: rute root ("/") akan tersedia untuk kita komponen Greeting.vue, yang akan kita tulis nanti.

Sekarang kita akan mengimpor router kita. Untuk melakukan ini, lakukan perubahan pada
main.js
 import Vue from 'vue' import App from './App.vue' import router from './router' Vue.config.productionTip = false new Vue({ router, render: h => h(App), }).$mount('#app') 


Lalu
Aplikasi
 <template> <div id="app"> <router-view></router-view> </div> </template> <script> export default { name: 'app' } </script> <style> </style> 


Untuk menjalankan permintaan server, gunakan klien HTTP AXIOS:

 $ npm install --save axios 

Agar tidak menulis pengaturan yang sama setiap kali (misalnya, rute permintaan adalah "/ api") di setiap komponen, saya sarankan untuk memasukkannya ke komponen http-common.js yang terpisah :

 import axios from 'axios' export const AXIOS = axios.create({ baseURL: `/api` }) 

Catatan: untuk menghindari peringatan saat mengeluarkan ke konsol ( console.log () ), saya sarankan menulis baris ini di package.json :

 "rules": { "no-console": "off" } 

Sekarang, akhirnya, buat komponen (in / src / components )

Greeting.vue
 import {AXIOS} from './http-common' <template> <div id="greeting"> <h3>Greeting component</h3> <p>Counter: {{ counter }}</p> <p>Username: {{ username }}</p> </div> </template> <script> export default { name: 'Greeting', data() { return { counter: 0, username: '' } }, methods: { loadGreeting() { AXIOS.get('/greeting', { params: { name: 'Vadim' } }) .then(response => { this.$data.counter = response.data.id; this.$data.username = response.data.content; }) .catch(error => { console.log('ERROR: ' + error.response.data); }) } }, mounted() { this.loadGreeting(); } } </script> 


Catatan:

  • Parameter kueri di-hardcode hanya untuk melihat cara kerjanya
  • Fungsi memuat dan merender data ( loadGreeting() ) dipanggil segera setelah memuat halaman ( mount () )
  • kami sudah mengimpor aksioma dengan pengaturan khusus kami dari http-common



Koneksi Basis Data


Sekarang mari kita lihat proses berinteraksi dengan database menggunakan contoh PostgreSQL dan Spring Data .

Pertama, buat plat tes:

 CREATE TABLE public."person" ( id serial NOT NULL, name character varying, PRIMARY KEY (id) ); 

dan isi dengan data:

 INSERT INTO person (name) VALUES ('John'), ('Griselda'), ('Bobby'); 

Tambahan pom.xml dari modul backend:
 <properties> ... <postgresql.version>42.2.5</postgresql.version> ... </properties> ... <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <version>${postgresql.version}</version> </dependency> ... <plugin> <groupId>org.jetbrains.kotlin</groupId> <artifactId>kotlin-maven-plugin</artifactId> <configuration> <args> <arg>-Xjsr305=strict</arg> </args> <compilerPlugins> <plugin>spring</plugin> <plugin>jpa</plugin> </compilerPlugins> </configuration> ... <dependency> <groupId>org.jetbrains.kotlin</groupId> <artifactId>kotlin-maven-noarg</artifactId> <version>${kotlin.version}</version> </dependency> 


Sekarang kita akan melengkapi file application.properties dari modul backend dengan pengaturan koneksi database:

 spring.datasource.url=${SPRING_DATASOURCE_URL} spring.datasource.username=${SPRING_DATASOURCE_USERNAME} spring.datasource.password=${SPRING_DATASOURCE_PASSWORD} spring.jpa.generate-ddl=true spring.jpa.properties.hibernate.temp.use_jdbc_metadata_defaults = false spring.jpa.database-platform=org.hibernate.dialect.PostgreSQL9Dialect spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true 

Catatan: dalam formulir ini, tiga parameter pertama merujuk ke variabel lingkungan. Saya sangat merekomendasikan melewatkan parameter sensitif melalui variabel lingkungan atau parameter startup. Tetapi, jika Anda yakin bahwa mereka tidak akan jatuh ke tangan penyerang jahat, maka Anda dapat bertanya secara eksplisit.

Mari kita membuat entitas (kelas entitas) untuk pemetaan objek-relasional:

Person.kt
 import javax.persistence.Column import javax.persistence.Entity import javax.persistence.GeneratedValue import javax.persistence.GenerationType import javax.persistence.Id import javax.persistence.Table @Entity @Table (name="person") data class Person( @Id @GeneratedValue(strategy = GenerationType.AUTO) val id: Long, @Column(nullable = false) val name: String ) 



Dan repositori CRUD untuk bekerja dengan tabel kami:

Repository.kt
 import com.kotlinspringvue.backend.jpa.Person import org.springframework.stereotype.Repository import org.springframework.data.repository.CrudRepository import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.repository.query.Param @Repository interface PersonRepository: CrudRepository<Person, Long> {} 

Catatan: Kami akan menggunakan metode findAll() , yang tidak perlu didefinisikan ulang, jadi kami akan membiarkan tubuh kosong.

Dan akhirnya, kami akan memperbarui pengontrol kami untuk melihat cara bekerja dengan database dalam aksi:

BackendController.kt
 import com.kotlinspringvue.backend.repository.PersonRepository import org.springframework.beans.factory.annotation.Autowired โ€ฆ @Autowired lateinit var personRepository: PersonRepository โ€ฆ @GetMapping("/persons") fun getPersons() = personRepository.findAll() 


Jalankan aplikasi, ikuti tautan https: // localhost: 8080 / api / orang untuk memastikan semuanya bekerja:

 [{"id":1,"name":"John"},{"id":2,"name":"Griselda"},{"id":3,"name":"Bobby"}] 


Otentikasi


Sekarang kita dapat beralih ke otentikasi - juga salah satu fungsi dasar aplikasi di mana akses data dibedakan.

Pertimbangkan untuk mengimplementasikan server otorisasi Anda sendiri menggunakan JWT (JSON Web Token).

Mengapa tidak Otentikasi Dasar?

  • Menurut pendapat saya, Otentikasi Dasar tidak memenuhi tantangan ancaman modern bahkan dalam lingkungan penggunaan yang relatif aman.
  • Anda dapat menemukan lebih banyak materi tentang hal ini.

Mengapa tidak OAuth di luar kotak Spring OAuth Security?
  • Karena OAuth memiliki lebih banyak barang.
  • Pendekatan ini dapat ditentukan oleh keadaan eksternal: persyaratan pelanggan, keinginan arsitek, dll.
  • Jika Anda adalah pengembang pemula, maka dalam perspektif strategis akan berguna untuk menggali lebih dalam dengan fungsi keamanan secara lebih rinci.

Backend


Selain tamu, akan ada dua kelompok pengguna dalam aplikasi kami - pengguna biasa dan administrator. Mari kita buat tiga tabel: pengguna - untuk menyimpan data pengguna, peran - untuk menyimpan informasi tentang peran dan users_roles - untuk menautkan dua tabel pertama.

Buat tabel, tambahkan kendala, dan isi tabel peran
 CREATE TABLE public.users ( id serial NOT NULL, username character varying, first_name character varying, last_name character varying, email character varying, password character varying, enabled boolean, PRIMARY KEY (id) ); CREATE TABLE public.roles ( id serial NOT NULL, name character varying, PRIMARY KEY (id) ); CREATE TABLE public.users_roles ( id serial NOT NULL, user_id integer, role_id integer, PRIMARY KEY (id) ); ALTER TABLE public.users_roles ADD CONSTRAINT users_roles_users_fk FOREIGN KEY (user_id) REFERENCES public.users (id) MATCH SIMPLE ON UPDATE CASCADE ON DELETE CASCADE; ALTER TABLE public.users_roles ADD CONSTRAINT users_roles_roles_fk FOREIGN KEY (role_id) REFERENCES public.roles (id) MATCH SIMPLE ON UPDATE CASCADE ON DELETE CASCADE; INSERT INTO roles (name) VALUES ('ROLE_USER'), ('ROLE_ADMIN'); 


Mari kita buat kelas Entity:
User.kt
 import javax.persistence.* @Entity @Table(name = "users") data class User ( @Id @GeneratedValue(strategy = GenerationType.AUTO) val id: Long? = 0, @Column(name="username") var username: String?=null, @Column(name="first_name") var firstName: String?=null, @Column(name="last_name") var lastName: String?=null, @Column(name="email") var email: String?=null, @Column(name="password") var password: String?=null, @Column(name="enabled") var enabled: Boolean = false, @ManyToMany(fetch = FetchType.EAGER) @JoinTable( name = "users_roles", joinColumns = [JoinColumn(name = "user_id", referencedColumnName = "id")], inverseJoinColumns = [JoinColumn(name = "role_id", referencedColumnName = "id")] ) var roles: Collection<Role>? = null ) 

Catatan: tabel pengguna dan peran berada dalam hubungan banyak ke banyak - satu pengguna dapat memiliki beberapa peran (misalnya, pengguna biasa dan administrator), dan beberapa pengguna dapat diberi satu peran.

Informasi untuk dipertimbangkan: Ada pendekatan di mana pengguna diberi kekuatan terpisah (otoritas), sementara peran menyiratkan sekelompok kekuatan. Anda dapat membaca lebih lanjut tentang perbedaan antara peran dan izin di sini: Diberikan Otoritas Versus Peran dalam Keamanan Musim Semi .

Role.kt
 import javax.persistence.* @Entity @Table(name = "roles") data class Role ( @Id @GeneratedValue(strategy = GenerationType.AUTO) val id: Long, @Column(name="name") val name: String ) 


Buat repositori untuk bekerja dengan tabel:

UsersRepository.kt
 import java.util.Optional import com.kotlinspringvue.backend.jpa.User import org.springframework.data.repository.CrudRepository import org.springframework.data.repository.query.Param import org.springframework.data.jpa.repository.JpaRepository import javax.transaction.Transactional interface UserRepository: JpaRepository<User, Long> { fun existsByUsername(@Param("username") username: String): Boolean fun findByUsername(@Param("username") username: String): Optional<User> fun findByEmail(@Param("email") email: String): Optional<User> @Transactional fun deleteByUsername(@Param("username") username: String) } 


RolesRepository.kt
 import com.kotlinspringvue.backend.jpa.Role import org.springframework.data.repository.CrudRepository import org.springframework.data.repository.query.Param import org.springframework.data.jpa.repository.JpaRepository interface RoleRepository : JpaRepository<Role, Long> { fun findByName(@Param("name") name: String): Role } 


Tambahkan dependensi baru ke
modul backend pom.xml
 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>com.fasterxml.jackson.module</groupId> <artifactId>jackson-module-kotlin</artifactId> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.0</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.10.6</version> </dependency> 


Dan tambahkan parameter baru untuk bekerja dengan token di application.properties :
 assm.app.jwtSecret=jwtAssmSecretKey assm.app.jwtExpiration=86400 

Sekarang kita akan membuat kelas untuk menyimpan data yang berasal dari formulir otorisasi dan pendaftaran:

LoginUser.kt
 class LoginUser : Serializable { @JsonProperty("username") var username: String? = null @JsonProperty("password") var password: String? = null constructor() {} constructor(username: String, password: String) { this.username = username this.password = password } companion object { private const val serialVersionUID = -1764970284520387975L } } 


NewUser.kt
 import com.fasterxml.jackson.annotation.JsonProperty import java.io.Serializable class NewUser : Serializable { @JsonProperty("username") var username: String? = null @JsonProperty("firstName") var firstName: String? = null @JsonProperty("lastName") var lastName: String? = null @JsonProperty("email") var email: String? = null @JsonProperty("password") var password: String? = null constructor() {} constructor(username: String, firstName: String, lastName: String, email: String, password: String, recaptchaToken: String) { this.username = username this.firstName = firstName this.lastName = lastName this.email = email this.password = password } companion object { private const val serialVersionUID = -1764970284520387975L } } 


Mari kita membuat kelas khusus untuk respons server - mengembalikan token otentikasi dan universal (string):

JwtRespons.kt
 import org.springframework.security.core.GrantedAuthority class JwtResponse(var accessToken: String?, var username: String?, val authorities: Collection<GrantedAuthority>) { var type = "Bearer" } 


ResponseMessage.kt
 class ResponseMessage(var message: String?) 


Kami juga akan membutuhkan pengecualian "Pengguna Sudah Ada".
UserAlreadyExistException.kt
 class UserAlreadyExistException : RuntimeException { constructor() : super() {} constructor(message: String, cause: Throwable) : super(message, cause) {} constructor(message: String) : super(message) {} constructor(cause: Throwable) : super(cause) {} companion object { private val serialVersionUID = 5861310537366287163L } } 


Untuk mendefinisikan peran pengguna, kami membutuhkan layanan tambahan yang mengimplementasikan antarmuka UserDetailsService :

UserDetailsServiceImpl.kt
 import com.kotlinspringvue.backend.repository.UserRepository import org.springframework.beans.factory.annotation.Autowired import org.springframework.security.core.userdetails.UserDetails import org.springframework.security.core.userdetails.UserDetailsService import org.springframework.security.core.userdetails.UsernameNotFoundException import org.springframework.stereotype.Service import org.springframework.security.core.GrantedAuthority import org.springframework.security.core.authority.SimpleGrantedAuthority import java.util.stream.Collectors @Service class UserDetailsServiceImpl: UserDetailsService { @Autowired lateinit var userRepository: UserRepository @Throws(UsernameNotFoundException::class) override fun loadUserByUsername(username: String): UserDetails { val user = userRepository.findByUsername(username).get() ?: throw UsernameNotFoundException("User '$username' not found") val authorities: List<GrantedAuthority> = user.roles!!.stream().map({ role -> SimpleGrantedAuthority(role.name)}).collect(Collectors.toList<GrantedAuthority>()) return org.springframework.security.core.userdetails.User .withUsername(username) .password(user.password) .authorities(authorities) .accountExpired(false) .accountLocked(false) .credentialsExpired(false) .disabled(false) .build() } } 


Untuk bekerja dengan JWT, kita membutuhkan tiga kelas:
JwtAuthEntryPoint - untuk menangani kesalahan otorisasi dan selanjutnya digunakan dalam pengaturan keamanan web:

JwtAuthEntryPoint.kt
 import javax.servlet.ServletException import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletResponse import org.slf4j.Logger import org.slf4j.LoggerFactory import org.springframework.security.core.AuthenticationException import org.springframework.security.web.AuthenticationEntryPoint import org.springframework.stereotype.Component @Component class JwtAuthEntryPoint : AuthenticationEntryPoint { @Throws(IOException::class, ServletException::class) override fun commence(request: HttpServletRequest, response: HttpServletResponse, e: AuthenticationException) { logger.error("Unauthorized error. Message - {}", e!!.message) response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid credentials") } companion object { private val logger = LoggerFactory.getLogger(JwtAuthEntryPoint::class.java) } } 


JwtProvider - untuk menghasilkan dan memvalidasi token, serta menentukan pengguna dengan token-nya:

JwtProvider.kt
 import io.jsonwebtoken.* import org.springframework.beans.factory.annotation.Autowired import org.slf4j.Logger import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Value import org.springframework.security.core.Authentication import org.springframework.stereotype.Component import org.springframework.security.core.GrantedAuthority import org.springframework.security.core.authority.SimpleGrantedAuthority import com.kotlinspringvue.backend.repository.UserRepository import java.util.Date @Component public class JwtProvider { private val logger: Logger = LoggerFactory.getLogger(JwtProvider::class.java) @Autowired lateinit var userRepository: UserRepository @Value("\${assm.app.jwtSecret}") lateinit var jwtSecret: String @Value("\${assm.app.jwtExpiration}") var jwtExpiration:Int?=0 fun generateJwtToken(username: String): String { return Jwts.builder() .setSubject(username) .setIssuedAt(Date()) .setExpiration(Date((Date()).getTime() + jwtExpiration!! * 1000)) .signWith(SignatureAlgorithm.HS512, jwtSecret) .compact() } fun validateJwtToken(authToken: String): Boolean { try { Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(authToken) return true } catch (e: SignatureException) { logger.error("Invalid JWT signature -> Message: {} ", e) } catch (e: MalformedJwtException) { logger.error("Invalid JWT token -> Message: {}", e) } catch (e: ExpiredJwtException) { logger.error("Expired JWT token -> Message: {}", e) } catch (e: UnsupportedJwtException) { logger.error("Unsupported JWT token -> Message: {}", e) } catch (e: IllegalArgumentException) { logger.error("JWT claims string is empty -> Message: {}", e) } return false } fun getUserNameFromJwtToken(token: String): String { return Jwts.parser() .setSigningKey(jwtSecret) .parseClaimsJws(token) .getBody().getSubject() } } 


JwtAuthTokenFilter - untuk mengautentikasi pengguna dan memfilter permintaan:

JwtAuthTokenFilter.kt
 import java.io.IOException import javax.servlet.FilterChain import javax.servlet.ServletException import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletResponse import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Autowired import org.springframework.security.authentication.UsernamePasswordAuthenticationToken import org.springframework.security.core.context.SecurityContextHolder import org.springframework.security.core.userdetails.UserDetails import org.springframework.security.web.authentication.WebAuthenticationDetailsSource import org.springframework.web.filter.OncePerRequestFilter import com.kotlinspringvue.backend.service.UserDetailsServiceImpl class JwtAuthTokenFilter : OncePerRequestFilter() { @Autowired private val tokenProvider: JwtProvider? = null @Autowired private val userDetailsService: UserDetailsServiceImpl? = null @Throws(ServletException::class, IOException::class) override fun doFilterInternal(request: HttpServletRequest, response: HttpServletResponse, filterChain: FilterChain) { try { val jwt = getJwt(request) if (jwt != null && tokenProvider!!.validateJwtToken(jwt)) { val username = tokenProvider.getUserNameFromJwtToken(jwt) val userDetails = userDetailsService!!.loadUserByUsername(username) val authentication = UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities()) authentication.setDetails(WebAuthenticationDetailsSource().buildDetails(request)) SecurityContextHolder.getContext().setAuthentication(authentication) } } catch (e: Exception) { logger.error("Can NOT set user authentication -> Message: {}", e) } filterChain.doFilter(request, response) } private fun getJwt(request: HttpServletRequest): String? { val authHeader = request.getHeader("Authorization") return if (authHeader != null && authHeader.startsWith("Bearer ")) { authHeader.replace("Bearer ", "") } else null } companion object { private val logger = LoggerFactory.getLogger(JwtAuthTokenFilter::class.java) } } 


Sekarang kita dapat mengkonfigurasi kacang yang bertanggung jawab untuk keamanan web:

WebSecurityConfig.kt
 import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.beans.factory.annotation.Autowired import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter import org.springframework.security.config.http.SessionCreationPolicy import org.springframework.security.authentication.AuthenticationManager import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder import com.kotlinspringvue.backend.jwt.JwtAuthEntryPoint import com.kotlinspringvue.backend.jwt.JwtAuthTokenFilter import com.kotlinspringvue.backend.service.UserDetailsServiceImpl @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) class WebSecurityConfig : WebSecurityConfigurerAdapter() { @Autowired internal var userDetailsService: UserDetailsServiceImpl? = null @Autowired private val unauthorizedHandler: JwtAuthEntryPoint? = null @Bean fun bCryptPasswordEncoder(): BCryptPasswordEncoder { return BCryptPasswordEncoder() } @Bean fun authenticationJwtTokenFilter(): JwtAuthTokenFilter { return JwtAuthTokenFilter() } @Throws(Exception::class) override fun configure(authenticationManagerBuilder: AuthenticationManagerBuilder) { authenticationManagerBuilder .userDetailsService(userDetailsService) .passwordEncoder(bCryptPasswordEncoder()) } @Bean @Throws(Exception::class) override fun authenticationManagerBean(): AuthenticationManager { return super.authenticationManagerBean() } @Throws(Exception::class) override protected fun configure(http: HttpSecurity) { http.csrf().disable().authorizeRequests() .antMatchers("/**").permitAll() .anyRequest().authenticated() .and() .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter::class.java) } } 


Buat pengontrol untuk pendaftaran dan otorisasi:

AuthController.kt
 import javax.validation.Valid import java.util.* import java.util.stream.Collectors import org.springframework.security.core.Authentication import org.springframework.beans.factory.annotation.Autowired import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity import org.springframework.security.authentication.AuthenticationManager import org.springframework.security.authentication.UsernamePasswordAuthenticationToken import org.springframework.security.core.context.SecurityContextHolder import org.springframework.security.core.userdetails.UserDetails import org.springframework.security.crypto.password.PasswordEncoder import org.springframework.security.access.prepost.PreAuthorize import org.springframework.security.core.GrantedAuthority import org.springframework.security.core.authority.SimpleGrantedAuthority import org.springframework.web.bind.annotation.CrossOrigin import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController import com.kotlinspringvue.backend.model.LoginUser import com.kotlinspringvue.backend.model.NewUser import com.kotlinspringvue.backend.web.response.JwtResponse import com.kotlinspringvue.backend.web.response.ResponseMessage import com.kotlinspringvue.backend.jpa.User import com.kotlinspringvue.backend.repository.UserRepository import com.kotlinspringvue.backend.repository.RoleRepository import com.kotlinspringvue.backend.jwt.JwtProvider @CrossOrigin(origins = ["*"], maxAge = 3600) @RestController @RequestMapping("/api/auth") class AuthController() { @Autowired lateinit var authenticationManager: AuthenticationManager @Autowired lateinit var userRepository: UserRepository @Autowired lateinit var roleRepository: RoleRepository @Autowired lateinit var encoder: PasswordEncoder @Autowired lateinit var jwtProvider: JwtProvider @PostMapping("/signin") fun authenticateUser(@Valid @RequestBody loginRequest: LoginUser): ResponseEntity<*> { val userCandidate: Optional <User> = userRepository.findByUsername(loginRequest.username!!) if (userCandidate.isPresent) { val user: User = userCandidate.get() val authentication = authenticationManager.authenticate( UsernamePasswordAuthenticationToken(loginRequest.username, loginRequest.password)) SecurityContextHolder.getContext().setAuthentication(authentication) val jwt: String = jwtProvider.generateJwtToken(user.username!!) val authorities: List<GrantedAuthority> = user.roles!!.stream().map({ role -> SimpleGrantedAuthority(role.name)}).collect(Collectors.toList<GrantedAuthority>()) return ResponseEntity.ok(JwtResponse(jwt, user.username, authorities)) } else { return ResponseEntity(ResponseMessage("User not found!"), HttpStatus.BAD_REQUEST) } } @PostMapping("/signup") fun registerUser(@Valid @RequestBody newUser: NewUser): ResponseEntity<*> { val userCandidate: Optional <User> = userRepository.findByUsername(newUser.username!!) if (!userCandidate.isPresent) { if (usernameExists(newUser.username!!)) { return ResponseEntity(ResponseMessage("Username is already taken!"), HttpStatus.BAD_REQUEST) } else if (emailExists(newUser.email!!)) { return ResponseEntity(ResponseMessage("Email is already in use!"), HttpStatus.BAD_REQUEST) } // Creating user's account val user = User( 0, newUser.username!!, newUser.firstName!!, newUser.lastName!!, newUser.email!!, encoder.encode(newUser.password), true ) user!!.roles = Arrays.asList(roleRepository.findByName("ROLE_USER")) userRepository.save(user) return ResponseEntity(ResponseMessage("User registered successfully!"), HttpStatus.OK) } else { return ResponseEntity(ResponseMessage("User already exists!"), HttpStatus.BAD_REQUEST) } } private fun emailExists(email: String): Boolean { return userRepository.findByUsername(email).isPresent } private fun usernameExists(username: String): Boolean { return userRepository.findByUsername(username).isPresent } } 

Kami telah menerapkan dua metode:

  • masuk - memeriksa apakah pengguna ada dan, jika demikian, mengembalikan token yang dihasilkan, nama pengguna dan perannya (atau lebih tepatnya, otoritas - izin)
  • pendaftaran - memeriksa apakah pengguna ada dan, jika tidak, membuat catatan baru di tabel pengguna dengan tautan eksternal ke peran ROLE_USER


Dan akhirnya, kami menambah BackendController dengan dua metode: satu akan mengembalikan data yang hanya dapat diakses oleh administrator (pengguna dengan hak ROLE_USER dan ROLE_ADMIN) dan kepada pengguna biasa (ROLE_USER).

BackendController.kt
 import org.springframework.security.access.prepost.PreAuthorize import org.springframework.security.core.Authentication import com.kotlinspringvue.backend.repository.UserRepository import com.kotlinspringvue.backend.jpa.User โ€ฆ @Autowired lateinit var userRepository: UserRepository โ€ฆ @GetMapping("/usercontent") @PreAuthorize("hasRole('USER') or hasRole('ADMIN')") @ResponseBody fun getUserContent(authentication: Authentication): String { val user: User = userRepository.findByUsername(authentication.name).get() return "Hello " + user.firstName + " " + user.lastName + "!" } @GetMapping("/admincontent") @PreAuthorize("hasRole('ADMIN')") @ResponseBody fun getAdminContent(): String { return "Admin's content" } 


Frontend


Mari kita membuat beberapa komponen baru:

  • Rumah
  • Masuk
  • Daftar
  • Halaman admin
  • Halaman pengguna

Dengan konten templat (untuk memulai salin-tempel yang nyaman ):

Templat Komponen
 <template> <div> </div> </template> <script> </script> <style> </style> 


Tambahkan id = " component_name " ke setiap div di dalam templat dan ekspor default {name: '[component_name]'} dalam skrip .

Sekarang tambahkan rute baru:

router.js
 import Vue from 'vue' import Router from 'vue-router' import Home from '@/components/Home' import SignIn from '@/components/SignIn' import SignUp from '@/components/SignUp' import AdminPage from '@/components/AdminPage' import UserPage from '@/components/UserPage' Vue.use(Router) export default new Router({ mode: 'history', routes: [ { path: '/', name: 'Home', component: Home }, { path: '/home', name: 'Home', component: Home }, { path: '/login', name: 'SignIn', component: SignIn }, { path: '/register', name: 'SignUp', component: SignUp }, { path: '/user', name: 'UserPage', component: UserPage }, { path: '/admin', name: 'AdminPage', component: AdminPage } ] }) 


Kami akan menggunakan Vuex untuk menyimpan token dan menggunakannya saat meminta server . Vuex adalah pola manajemen negara + perpustakaan Vue.js. Ini berfungsi sebagai gudang data terpusat untuk semua komponen aplikasi dengan aturan untuk memastikan bahwa negara hanya dapat diubah dengan cara yang dapat diprediksi.

 $ npm install --save vuex 

Tambahkan store sebagai file terpisah ke src / store :

index.js
 import Vue from 'vue'; import Vuex from 'vuex'; Vue.use(Vuex); const state = { token: localStorage.getItem('user-token') || '', role: localStorage.getItem('user-role') || '', username: localStorage.getItem('user-name') || '', authorities: localStorage.getItem('authorities') || '', }; const getters = { isAuthenticated: state => { if (state.token != null && state.token != '') { return true; } else { return false; } }, isAdmin: state => { if (state.role === 'admin') { return true; } else { return false; } }, getUsername: state => { return state.username; }, getAuthorities: state => { return state.authorities; }, getToken: state => { return state.token; } }; const mutations = { auth_login: (state, user) => { localStorage.setItem('user-token', user.token); localStorage.setItem('user-name', user.name); localStorage.setItem('user-authorities', user.roles); state.token = user.token; state.username = user.username; state.authorities = user.roles; var isUser = false; var isAdmin = false; for (var i = 0; i < user.roles.length; i++) { if (user.roles[i].authority === 'ROLE_USER') { isUser = true; } else if (user.roles[i].authority === 'ROLE_ADMIN') { isAdmin = true; } } if (isUser) { localStorage.setItem('user-role', 'user'); state.role = 'user'; } if (isAdmin) { localStorage.setItem('user-role', 'admin'); state.role = 'admin'; } }, auth_logout: () => { state.token = ''; state.role = ''; state.username = ''; state.authorities = []; localStorage.removeItem('user-token'); localStorage.removeItem('user-role'); localStorage.removeItem('user-name'); localStorage.removeItem('user-authorities'); } }; const actions = { login: (context, user) => { context.commit('auth_login', user) }, logout: (context) => { context.commit('auth_logout'); } }; export const store = new Vuex.Store({ state, getters, mutations, actions }); 

Mari kita lihat apa yang kita miliki di sini:

  • store โ€” , โ€” , , ( โ€” (authorities): โ€” , , admin user โ€”
  • getters โ€”
  • mutations โ€”
  • actions โ€” ,

: (mutations) โ€” .

Kami akan melakukan perubahan yang sesuai untuk

main.js
 import { store } from './store'; ... new Vue({ router, store, render: h => h(App) }).$mount('#app') 


Agar antarmuka segera terlihat indah dan rapi bahkan dalam aplikasi eksperimental yang saya gunakan. Tapi ini, seperti yang mereka katakan, adalah masalah selera, dan tidak mempengaruhi fungsi dasar:

 $ npm install --save bootstrap bootstrap-vue 

Bootstrap di main.js
 import BootstrapVue from 'bootstrap-vue' import 'bootstrap/dist/css/bootstrap.css' import 'bootstrap-vue/dist/bootstrap-vue.css' โ€ฆ Vue.use(BootstrapVue) 


Sekarang mari kita bekerja pada komponen Aplikasi:

  • Tambahkan kemampuan untuk "logout" untuk semua pengguna yang berwenang
  • Tambahkan pengalihan otomatis ke halaman beranda setelah keluar
  • Kami akan menampilkan tombol menu navigasi "Pengguna" dan "Keluar" untuk semua pengguna yang berwenang dan "Masuk" untuk yang tidak berwenang
  • Kami akan menunjukkan tombol "Admin" dari menu navigasi hanya untuk administrator yang berwenang

Untuk melakukan ini:

tambahkan metode logout ()
 methods: { logout() { this.$store.dispatch('logout'); this.$router.push('/') } } 


dan edit templatnya
 <template> <div id="app"> <b-navbar style="width: 100%" type="dark" variant="dark"> <b-navbar-brand id="nav-brand" href="#">Kotlin+Spring+Vue</b-navbar-brand> <router-link to="/"><img height="30px" src="./assets/img/kotlin-logo.png" alt="Kotlin+Spring+Vue"/></router-link> <router-link to="/"><img height="30px" src="./assets/img/spring-boot-logo.png" alt="Kotlin+Spring+Vue"/></router-link> <router-link to="/"><img height="30px" src="./assets/img/vuejs-logo.png" alt="Kotlin+Spring+Vue"/></router-link> <router-link to="/user" class="nav-link text-light" v-if="this.$store.getters.isAuthenticated">User</router-link> <router-link to="/admin" class="nav-link text-light" v-if="this.$store.getters.isAuthenticated && this.$store.getters.isAdmin">Admin</router-link> <router-link to="/register" class="nav-link text-light" v-if="!this.$store.getters.isAuthenticated">Register</router-link> <router-link to="/login" class="nav-link text-light" v-if="!this.$store.getters.isAuthenticated">Login</router-link> <a href="#" class="nav-link text-light" v-if="this.$store.getters.isAuthenticated" v-on:click="logout">Logout </a> </b-navbar> <router-view></router-view> </div> </template> 

:

  • store , . , , (ยซv-ifยป)
  • Kotlin, Spring Boot Vue.js, /assets/img/ . , ( )


Perbarui komponen:

Beranda
 <template> <div div="home"> <b-jumbotron> <template slot="header">Kotlin + Spring Boot + Vue.js</template> <template slot="lead"> This is the demo web-application written in Kotlin using Spring Boot and Vue.js for frontend </template> <hr class="my-4" /> <p v-if="!this.$store.getters.isAuthenticated"> Login and start </p> <router-link to="/login" v-if="!this.$store.getters.isAuthenticated"> <b-button variant="primary">Login</b-button> </router-link> </b-jumbotron> </div> </template> <script> </script> <style> </style> 


MasukInvue
 <template> <div div="signin"> <div class="login-form"> <b-card title="Login" tag="article" style="max-width: 20rem;" class="mb-2" > <div> <b-alert :show="dismissCountDown" dismissible variant="danger" @dismissed="dismissCountDown=0" @dismiss-count-down="countDownChanged" > {{ alertMessage }} </b-alert> </div> <div> <b-form-input type="text" placeholder="Username" v-model="username" /> <div class="mt-2"></div> <b-form-input type="password" placeholder="Password" v-model="password" /> <div class="mt-2"></div> </div> <b-button v-on:click="login" variant="primary">Login</b-button> <hr class="my-4" /> <b-button variant="link">Forget password?</b-button> </b-card> </div> </div> </template> <script> import {AXIOS} from './http-common' export default { name: 'SignIn', data() { return { username: '', password: '', dismissSecs: 5, dismissCountDown: 0, alertMessage: 'Request error', } }, methods: { login() { AXIOS.post(`/auth/signin`, {'username': this.$data.username, 'password': this.$data.password}) .then(response => { this.$store.dispatch('login', {'token': response.data.accessToken, 'roles': response.data.authorities, 'username': response.data.username}); this.$router.push('/home') }, error => { this.$data.alertMessage = (error.response.data.message.length < 150) ? error.response.data.message : 'Request error. Please, report this error website owners'; console.log(error) }) .catch(e => { console.log(e); this.showAlert(); }) }, countDownChanged(dismissCountDown) { this.dismissCountDown = dismissCountDown }, showAlert() { this.dismissCountDown = this.dismissSecs }, } } </script> <style> .login-form { margin-left: 38%; margin-top: 50px; } </style> 

:

  • POST-
  • storage
  • ยซยป Bootstrap
  • , /home


SignUp.vue
 <template> <div div="signup"> <div class="login-form"> <b-card title="Register" tag="article" style="max-width: 20rem;" class="mb-2" > <div> <b-alert :show="dismissCountDown" dismissible variant="danger" @dismissed="dismissCountDown=0" @dismiss-count-down="countDownChanged" > {{ alertMessage }} </b-alert> </div> <div> <b-alert variant="success" :show="successfullyRegistered"> You have been successfully registered! Now you can login with your credentials <hr /> <router-link to="/login"> <b-button variant="primary">Login</b-button> </router-link> </b-alert> </div> <div> <b-form-input type="text" placeholder="Username" v-model="username" /> <div class="mt-2"></div> <b-form-input type="text" placeholder="First Name" v-model="firstname" /> <div class="mt-2"></div> <b-form-input type="text" placeholder="Last name" v-model="lastname" /> <div class="mt-2"></div> <b-form-input type="text" placeholder="Email" v-model="email" /> <div class="mt-2"></div> <b-form-input type="password" placeholder="Password" v-model="password" /> <div class="mt-2"></div> <b-form-input type="password" placeholder="Confirm Password" v-model="confirmpassword" /> <div class="mt-2"></div> </div> <b-button v-on:click="register" variant="primary">Register</b-button> </b-card> </div> </div> </template> <script> import {AXIOS} from './http-common' export default { name: 'SignUp', data () { return { username: '', firstname: '', lastname: '', email: '', password: '', confirmpassword: '', dismissSecs: 5, dismissCountDown: 0, alertMessage: '', successfullyRegistered: false } }, methods: { register: function () { if (this.$data.username === '' || this.$data.username == null) { this.$data.alertMessage = 'Please, fill "Username" field'; this.showAlert(); } else if (this.$data.firstname === '' || this.$data.firstname == null) { this.$data.alertMessage = 'Please, fill "First name" field'; this.showAlert(); } else if (this.$data.lastname === '' || this.$data.lastname == null) { this.$data.alertMessage = 'Please, fill "Last name" field'; this.showAlert(); } else if (this.$data.email === '' || this.$data.email == null) { this.$data.alertMessage = 'Please, fill "Email" field'; this.showAlert(); } else if (!this.$data.email.includes('@')) { this.$data.alertMessage = 'Email is incorrect'; this.showAlert(); } else if (this.$data.password === '' || this.$data.password == null) { this.$data.alertMessage = 'Please, fill "Password" field'; this.showAlert(); } else if (this.$data.confirmpassword === '' || this.$data.confirmpassword == null) { this.$data.alertMessage = 'Please, confirm password'; this.showAlert(); } else if (this.$data.confirmpassword !== this.$data.password) { this.$data.alertMessage = 'Passwords are not match'; this.showAlert(); } else { var newUser = { 'username': this.$data.username, 'firstName': this.$data.firstname, 'lastName': this.$data.lastname, 'email': this.$data.email, 'password': this.$data.password }; AXIOS.post('/auth/signup', newUser) .then(response => { console.log(response); this.successAlert(); }, error => { this.$data.alertMessage = (error.response.data.message.length < 150) ? error.response.data.message : 'Request error. Please, report this error website owners' this.showAlert(); }) .catch(error => { console.log(error); this.$data.alertMessage = 'Request error. Please, report this error website owners'; this.showAlert(); }); } }, countDownChanged(dismissCountDown) { this.dismissCountDown = dismissCountDown }, showAlert() { this.dismissCountDown = this.dismissSecs }, successAlert() { this.username = ''; this.firstname = ''; this.lastname = ''; this.email = ''; this.password = ''; this.confirmpassword = ''; this.successfullyRegistered = true; } } } </script> <style> .login-form { margin-left: 38%; margin-top: 50px; } </style> 

:

  • POST-
  • Bootstrap
  • , Bootstrap-


UserPage.vue
 <template> <div div="userpage"> <h2>{{ pageContent }}</h2> </div> </template> <script> import {AXIOS} from './http-common' export default { name: 'UserPage', data() { return { pageContent: '' } }, methods: { loadUserContent() { const header = {'Authorization': 'Bearer ' + this.$store.getters.getToken}; AXIOS.get('/usercontent', { headers: header }) .then(response => { this.$data.pageContent = response.data; }) .catch(error => { console.log('ERROR: ' + error.response.data); }) } }, mounted() { this.loadUserContent(); } } </script> <style> </style> 

:
  • , storage


Admin.vue
 <template> <div div="adminpage"> <h2>{{ pageContent }}</h2> </div> </template> <script> import {AXIOS} from './http-common' export default { name: 'AdminPage', data() { return { pageContent: '' } }, methods: { loadUserContent() { const header = {'Authorization': 'Bearer ' + this.$store.getters.getToken}; AXIOS.get('/admincontent', { headers: header }) .then(response => { this.$data.pageContent = response.data; }) .catch(error => { console.log('ERROR: ' + error.response.data); }) } }, mounted() { this.loadUserContent(); } } </script> <style> </style> 

, UserPage .

Peluncuran aplikasi


Kami akan mendaftarkan administrator pertama kami:





Penting: secara default, semua pengguna baru teratur. Mari beri administrator pertama wewenangnya:

 INSERT INTO users_roles (user_id, role_id) VALUES (1, 2); 

Lalu:

  1. Mari masuk sebagai administrator
  2. Periksa halaman Pengguna:

  3. Periksa halaman admin:

  4. Logout dari akun administrator
  5. Daftarkan akun pengguna biasa
  6. Periksa ketersediaan halaman Pengguna
  7. Mari kita coba untuk mendapatkan data admin menggunakan REST API: http: // localhost: 8080 / api / admincontent

 ERROR 77100 --- [nio-8080-exec-2] ckbackend.jwt.JwtAuthEntryPoint : Unauthorized error. Message - Full authentication is required to access this resource 


Cara untuk meningkatkan


Secara umum, selalu ada banyak dari mereka dalam bisnis apa pun. Saya akan daftar yang paling jelas:

  • Gunakan untuk membangun Gradle (jika Anda menganggap ini peningkatan)
  • Segera tutup kode dengan unit test (ini, tidak diragukan lagi, praktik yang baik)
  • Sejak awal, buat CI / CD Pipeline: kode tempat di repositori, berisi aplikasi, automate assembly dan deployment
  • Tambahkan permintaan PUT dan HAPUS (misalnya, memperbarui data pengguna dan menghapus akun)
  • Terapkan aktivasi / penonaktifan akun
  • Jangan gunakan penyimpanan lokal untuk menyimpan token - ini tidak aman
  • Gunakan OAuth
  • Verifikasi alamat email saat mendaftarkan pengguna baru
  • Gunakan perlindungan spam, mis. ReCAPTCHA


Tautan yang bermanfaat




Tambahan untuk bahan ini di sini.

Source: https://habr.com/ru/post/id467161/


All Articles