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> <execution> <id>install node and npm</id> <goals> <goal>install-node-and-npm</goal> </goals> <configuration> <nodeVersion>v11.8.0</nodeVersion> </configuration> </execution> <execution> <id>npm install</id> <goals> <goal>npm</goal> </goals> <phase>generate-resources</phase> <configuration> <arguments>install</arguments> </configuration> </execution> <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/> </parent> <properties> <main.basedir>${project.basedir}</main.basedir> </properties> <build> <plugins> <plugin> <groupId>org.jacoco</groupId> <artifactId>jacoco-maven-plugin</artifactId> <executions> <execution> <id>pre-unit-test</id> <goals> <goal>prepare-agent</goal> </goals> </execution> <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) }
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 untukmain.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>
:
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:- Mari masuk sebagai administrator
- Periksa halaman Pengguna:

- Periksa halaman admin:

- Logout dari akun administrator
- Daftarkan akun pengguna biasa
- Periksa ketersediaan halaman Pengguna
- 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.