Aplikasi web di Kotlin + Spring Boot + Vue.js (add-on)

Selamat siang, penghuni Habr yang terkasih!

Sesuai namanya, artikel ini adalah tambahan untuk aplikasi Web yang ditulis sebelumnya di Kotlin + Spring Boot + Vue.js , yang memungkinkan kita untuk meningkatkan kerangka aplikasi masa depan dan membuatnya lebih mudah untuk bekerja dengannya.

Sebelum memulai cerita, izinkan saya mengucapkan terima kasih kepada semua orang yang berkomentar di artikel sebelumnya.

Isi




Pengaturan CI / CD (Heroku)


Pertimbangkan implementasi integrasi dan pengiriman berkelanjutan menggunakan platform Heroku cloud PaaS sebagai contoh.

Hal pertama yang perlu kita lakukan adalah meletakkan kode aplikasi di repositori di GitHub . Agar tidak ada yang berlebihan dalam repositori, saya merekomendasikan konten berikut dari file .gitignore :

.gitignore
*.class # Help # backend/*.md # Package Files # *.jar *.war *.ear # Eclipse # .settings .project .classpath .studio target # NetBeans # backend/nbproject/private/ backend/nbbuild/ backend/dist/ backend/nbdist/ backend/.nb-gradle/ backend/build/ # Apple # .DS_Store # Intellij # .idea *.iml *.log # logback logback.out.xml backend/src/main/resources/public/ backend/target backend/.mvn backend/mvnw frontend/dist/ frontend/node/ frontend/node_modules/ frontend/npm-debug.log frontend/target !.mvn/wrapper/maven-wrapper.jar 


Penting: sebelum Anda mulai bekerja dengan Heroku, tambahkan file bernama Procfile (tanpa ekstensi apa pun) ke direktori root dengan baris:
web: java -Dserver.port=$PORT -jar backend/target/backend-0.0.1-SNAPSHOT.jar , di mana backend-0.0.1-SNAPSHOT.jar adalah nama file JAR rakitan . Dan pastikan untuk melakukan komit dan dorong .

Catatan: Anda juga dapat menambahkan file travis.yaml ke direktori root untuk mengurangi waktu pembuatan dan penerapan aplikasi di Heroku:

travis.yaml
 language: java jdk: - oraclejdk8 script: mvn clean install jacoco:report coveralls:report cache: directories: - node_modules 


Lalu:

# 1 Mendaftar di Heroku .

# 2 Buat aplikasi baru:

Buat aplikasi baru


# 3 Heroku memungkinkan Anda untuk menghubungkan sumber daya tambahan ke aplikasi, misalnya, database PostreSQL. Untuk melakukan ini, lakukan: Aplikasi -> Sumber Daya -> Pengaya -> Heroku Postgres :

Postoku Heroku


# 4 Pilih paket:

Rencanakan pemilihan


# 5 Sekarang Anda dapat melihat sumber daya yang terhubung:

Sumber Daya Terhubung


# 6 Lihatlah kredensial, mereka akan diperlukan untuk mengatur variabel lingkungan: Pengaturan -> Lihat Kredensial :

Lihat kredensial


# 7 Atur variabel lingkungan: Aplikasi -> Pengaturan -> Reveal Config Vars :

Variabel lingkungan


# 8 Atur variabel lingkungan untuk koneksi dalam format berikut:

 SPRING_DATASOURCE_URL = jdbc:postgresql://<i>hostname:port</i>/<i>db_name</i> SPRING_DATASOURCE_USERNAME = <i>username</i> SPRING_DATASOURCE_PASSWORD = <i>password</i> 

Seperti apa bentuknya


# 9 Buat semua tabel yang diperlukan di database baru.

# 10 File application.properties, masing-masing, akan terlihat seperti ini:

properti aplikasi
 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 


# 11 Buat pipa baru - Buat pipa baru :

Buat pipa baru


# 12 Metode penyebaran - GitHub (klik Sambungkan ke GitHub dan ikuti instruksi di jendela baru).

# 13 Aktifkan Penyebaran Otomatis :

Aktifkan Penyebaran Otomatis


# 14 Deploy Manual - Klik Deploy Branch untuk penerapan pertama. Tepat di browser Anda akan melihat output dari baris perintah.

Penyebaran manual


# 15 Klik View setelah bangunan berhasil untuk membuka aplikasi yang digunakan:

Lihat



Perlindungan Bot (reCAPTCHA)


Langkah pertama untuk mengaktifkan verifikasi reCAPTCHA di aplikasi kami adalah membuat reCAPTCH baru di panel admin Google . Di sana kami membuat situs baru (Tambah situs baru / Buat) dan atur pengaturan berikut:

Pengaturan ReCAPTCHA


Di bagian Domain , Anda harus menentukan selain alamat tempat aplikasi akan hidup, Anda harus menentukan localhost , sehingga selama debugging Anda menghindari masalah dalam bentuk ketidakmampuan untuk masuk ke aplikasi Anda.

Backend

Simpan kunci situs dan kunci rahasia ...

kunci situs / kunci rahasia


... lalu untuk menetapkan mereka ke variabel lingkungan, dan nama variabel, pada gilirannya, untuk menetapkan ke properti application.properties baru:

 google.recaptcha.key.site=${GOOGLE_RECAPTCHA_KEY_SITE} google.recaptcha.key.secret=${GOOGLE_RECAPTCHA_KEY_SECRET} 

Tambahkan dependensi baru di pom.xml untuk verifikasi di sisi Google token reCAPTCHA yang akan dikirimkan klien kepada kami:

 <dependency> <groupId>com.mashape.unirest</groupId> <artifactId>unirest-java</artifactId> <version>1.4.9</version> </dependency> 

Sekarang adalah waktu untuk memperbarui entitas yang kami gunakan untuk mengotorisasi dan mendaftarkan pengguna dengan menambahkan bidang string ke mereka untuk token reCAPTCHA yang sama:

LoginUser.kt
 import com.fasterxml.jackson.annotation.JsonProperty import java.io.Serializable class LoginUser : Serializable { @JsonProperty("username") var username: String? = null @JsonProperty("password") var password: String? = null @JsonProperty("recapctha_token") var recaptchaToken: String? = null constructor() {} constructor(username: String, password: String, recaptchaToken: String) { this.username = username this.password = password this.recaptchaToken = recaptchaToken } 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 @JsonProperty("recapctha_token") var recaptchaToken: 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 this.recaptchaToken = recaptchaToken } companion object { private const val serialVersionUID = -1764970284520387975L } } 


Tambahkan layanan kecil yang akan menyiarkan token reCAPTCHA ke layanan Google khusus dan informasikan sebagai tanggapan apakah token tersebut lolos verifikasi:

ReCaptchaService.kt
 import org.springframework.beans.factory.annotation.Value import org.springframework.stereotype.Service import org.springframework.web.client.RestOperations import org.springframework.beans.factory.annotation.Autowired import com.mashape.unirest.http.HttpResponse import com.mashape.unirest.http.JsonNode import com.mashape.unirest.http.Unirest @Service("captchaService") class ReCaptchaService { val BASE_VERIFY_URL: String = "https://www.google.com/recaptcha/api/siteverify" @Autowired private val restTemplate: RestOperations? = null @Value("\${google.recaptcha.key.site}") lateinit var keySite: String @Value("\${google.recaptcha.key.secret}") lateinit var keySecret: String fun validateCaptcha(token: String): Boolean { val url: String = String.format(BASE_VERIFY_URL + "?secret=%s&response=%s", keySecret, token) val jsonResponse: HttpResponse<JsonNode> = Unirest.get(url) .header("accept", "application/json").queryString("apiKey", "123") .asJson() return (jsonResponse.getStatus() == 200) } } 


Layanan ini harus digunakan dalam pengontrol registrasi dan otorisasi pengguna:

AuthController.kt
 import com.kotlinspringvue.backend.service.ReCaptchaService … @Autowired lateinit var captchaService: ReCaptchaService … if (!captchaService.validateCaptcha(loginRequest.recaptchaToken!!)) { return ResponseEntity(ResponseMessage("Validation failed (ReCaptcha v2)"), HttpStatus.BAD_REQUEST) } else [if]... … if (!captchaService.validateCaptcha(newUser.recaptchaToken!!)) { return ResponseEntity(ResponseMessage("Validation failed (ReCaptcha v2)"), HttpStatus.BAD_REQUEST) } else... 


Frontend

Langkah pertama adalah menginstal dan menyimpan paket reCAPTHA :

 $ npm install --save vue-recaptcha 

Kemudian sambungkan ke skrip di index.html :

 <script src="https://www.google.com/recaptcha/api.js onload=vueRecaptchaApiLoaded&render=explicit" async defer></script> 

Tambahkan captcha ke ruang kosong di halaman:

 <vue-recaptcha ref="recaptcha" size="invisible" :sitekey="sitekey" @verify="onCapthcaVerified" @expired="onCaptchaExpired" /> 

Dan tombol untuk tindakan target (otorisasi atau registrasi) sekarang akan memanggil metode validasi:

 <b-button v-on:click="validateCaptcha" variant="primary">Login</b-button> 

Tambahkan ketergantungan ke komponen:

 import VueRecaptcha from 'vue-recaptcha' 

Edit ekspor standar :

 components: { VueRecaptcha }, … data() { … siteKey: <i>  </i> … } 

Dan tambahkan metode baru:

  • validateCaptcha() - yang dipanggil dengan mengklik tombol
  • onCapthcaVerified(recaptchaToken) onCaptchaExpired() - yang captcha sendiri memanggil

Metode baru
 validateCaptcha() { this.$refs.recaptcha.execute() }, onCapthcaVerified(recaptchaToken) { AXIOS.post(`/auth/signin`, {'username': this.$data.username, 'password': this.$data.password, 'recapctha_token': recaptchaToken}) .then(response => { this.$store.dispatch('login', {'token': response.data.accessToken, 'roles': response.data.authorities, 'username': response.data.username}); this.$router.push('/home') }, error => { this.showAlert(error.response.data.message); }) .catch(e => { console.log(e); this.showAlert('Server error. Please, report this error website owners'); }) }, onCaptchaExpired() { this.$refs.recaptcha.reset() } 


Hasil



Pengiriman email


Pertimbangkan kemungkinan pengiriman surat ke aplikasi kami melalui server surat publik, seperti Google atau Mail.ru.

Langkah pertama, masing-masing, adalah membuat akun di server surat yang dipilih, jika belum.

Langkah kedua kita perlu menambahkan dependensi berikut di pom.xml :

Ketergantungan
 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-mail</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>2.4</version> </dependency> 


Anda juga perlu menambahkan properti baru ke application.properties :

Properti SMTP
 spring.mail.host=${SMTP_MAIL_HOST} spring.mail.port=${SMTP_MAIL_PORT} spring.mail.username=${SMTP_MAIL_USERNAME} spring.mail.password=${SMTP_MAIL_PASSWORD} spring.mail.properties.mail.smtp.auth=true spring.mail.properties.mail.smtp.starttls.enable=true spring.mail.properties.mail.smtp.ssl.enable=true spring.mail.properties.mail.smtp.connectiontimeout=5000 spring.mail.properties.mail.smtp.timeout=5000 spring.mail.properties.mail.smtp.writetimeout=5000 


Anda dapat menentukan pengaturan SMTP di sini: Google dan Mail.ru

Buat antarmuka tempat kami mendeklarasikan beberapa metode:

  • Untuk mengirim pesan teks biasa
  • Untuk mengirim email HTML
  • Untuk mengirim email menggunakan templat

EmailService.kt
 package com.kotlinspringvue.backend.email import org.springframework.mail.SimpleMailMessage internal interface EmailService { fun sendSimpleMessage(to: String, subject: String, text: String) fun sendSimpleMessageUsingTemplate(to: String, subject: String, template: String, params:MutableMap<String, Any>) fun sendHtmlMessage(to: String, subject: String, htmlMsg: String) } 


Sekarang mari kita buat implementasi dari antarmuka ini - layanan pengiriman email:

EmailServiceImpl.kt
 package com.kotlinspringvue.backend.email import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Value import org.springframework.core.io.FileSystemResource import org.springframework.mail.MailException import org.springframework.mail.SimpleMailMessage import org.springframework.mail.javamail.JavaMailSender import org.springframework.mail.javamail.MimeMessageHelper import org.springframework.stereotype.Component import org.thymeleaf.spring5.SpringTemplateEngine import org.thymeleaf.context.Context import java.io.File import javax.mail.MessagingException import javax.mail.internet.MimeMessage import org.apache.commons.io.IOUtils import org.springframework.core.env.Environment @Component class EmailServiceImpl : EmailService { @Value("\${spring.mail.username}") lateinit var sender: String @Autowired lateinit var environment: Environment @Autowired var emailSender: JavaMailSender? = null @Autowired lateinit var templateEngine: SpringTemplateEngine override fun sendSimpleMessage(to: String, subject: String, text: String) { try { val message = SimpleMailMessage() message.setTo(to) message.setFrom(sender) message.setSubject(subject) message.setText(text) emailSender!!.send(message) } catch (exception: MailException) { exception.printStackTrace() } } override fun sendSimpleMessageUsingTemplate(to: String, subject: String, template: String, params:MutableMap<String, Any>) { val message = emailSender!!.createMimeMessage() val helper = MimeMessageHelper(message, true, "utf-8") var context: Context = Context() context.setVariables(params) val html: String = templateEngine.process(template, context) helper.setTo(to) helper.setFrom(sender) helper.setText(html, true) helper.setSubject(subject) emailSender!!.send(message) } override fun sendHtmlMessage(to: String, subject: String, htmlMsg: String) { try { val message = emailSender!!.createMimeMessage() message.setContent(htmlMsg, "text/html") val helper = MimeMessageHelper(message, false, "utf-8") helper.setTo(to) helper.setFrom(sender) helper.setSubject(subject) emailSender!!.send(message) } catch (exception: MailException) { exception.printStackTrace() } } } 


  • Kami menggunakan JavaMailSender yang dikonfigurasi secara otomatis oleh Spring untuk mengirim email
  • Mengirim surat biasa sangat sederhana - Anda hanya perlu menambahkan teks ke badan surat dan mengirimkannya
  • Email HTML didefinisikan sebagai pesan Mime Type, dan isinya sebagai text/html
  • Untuk memproses template pesan HTML, kami menggunakan Spring Template Engine

Mari kita membuat template sederhana untuk menulis menggunakan kerangka Thymeleaf dengan menempatkannya di src / main / resources / templates / :

emailTemplate.html
 <!DOCTYPE html> <html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>Hello</title> </head> <body style="font-family: Arial, Helvetica, sans-serif;"> <h3>Hello!</h3> <div style="margin-top: 20px; margin-bottom: 30px; margin-left: 20px;"> <p>Hello, dear: <b><span th:text="${addresseeName}"></span></b></p> </div> <div> <img th:src="${signatureImage}" width="200px;"/> </div> </body> </html> 


Elemen perubahan templat (dalam kasus kami, nama penerima dan jalur gambar untuk tanda tangan) dinyatakan menggunakan placeholder.

Sekarang buat atau perbarui pengontrol yang akan mengirim surat:

BackendController.kt
 import com.kotlinspringvue.backend.email.EmailServiceImpl import com.kotlinspringvue.backend.web.response.ResponseMessage import org.springframework.beans.factory.annotation.Value import org.springframework.http.ResponseEntity import org.springframework.http.HttpStatus … @Autowired lateinit var emailService: EmailService @Value("\${spring.mail.username}") lateinit var addressee: String … @GetMapping("/sendSimpleEmail") @PreAuthorize("hasRole('USER')") fun sendSimpleEmail(): ResponseEntity<*> { try { emailService.sendSimpleMessage(addressee, "Simple Email", "Hello! This is simple email") } catch (e: Exception) { return ResponseEntity(ResponseMessage("Error while sending message"), HttpStatus.BAD_REQUEST) } return ResponseEntity(ResponseMessage("Email has been sent"), HttpStatus.OK) } @GetMapping("/sendTemplateEmail") @PreAuthorize("hasRole('USER')") fun sendTemplateEmail(): ResponseEntity<*> { try { var params:MutableMap<String, Any> = mutableMapOf() params["addresseeName"] = addressee params["signatureImage"] = "https://coderlook.com/wp-content/uploads/2019/07/spring-by-pivotal.png" emailService.sendSimpleMessageUsingTemplate(addressee, "Template Email", "emailTemplate", params) } catch (e: Exception) { return ResponseEntity(ResponseMessage("Error while sending message"), HttpStatus.BAD_REQUEST) } return ResponseEntity(ResponseMessage("Email has been sent"), HttpStatus.OK) } @GetMapping("/sendHtmlEmail") @PreAuthorize("hasRole('USER')") fun sendHtmlEmail(): ResponseEntity<*> { try { emailService.sendHtmlMessage(addressee, "HTML Email", "<h1>Hello!</h1><p>This is HTML email</p>") } catch (e: Exception) { return ResponseEntity(ResponseMessage("Error while sending message"), HttpStatus.BAD_REQUEST) } return ResponseEntity(ResponseMessage("Email has been sent"), HttpStatus.OK) } 


Catatan: Untuk memastikan semuanya bekerja, pertama kami akan mengirim surat kepada diri kami sendiri.

Selain itu, untuk memastikan semuanya bekerja, kita dapat membuat antarmuka web sederhana yang hanya akan menarik metode layanan web:

Email.vue
 <template> <div id="email"> <b-button v-on:click="sendSimpleMessage" variant="primary">Simple Email</b-button><br/> <b-button v-on:click="sendEmailUsingTemplate" variant="primary">Template Email</b-button><br/> <b-button v-on:click="sendHTMLEmail" variant="primary">HTML Email</b-button><br/> </div> </template> <script> import {AXIOS} from './http-common' export default { name: 'EmailPage', data() { return { counter: 0, username: '', header: {'Authorization': 'Bearer ' + this.$store.getters.getToken} } }, methods: { sendSimpleMessage() { AXIOS.get('/sendSimpleEmail', { headers: this.$data.header }) .then(response => { console.log(response); alert("OK"); }) .catch(error => { console.log('ERROR: ' + error.response.data); }) }, sendEmailUsingTemplate() { AXIOS.get('/sendTemplateEmail', { headers: this.$data.header }) .then(response => { console.log(response); alert("OK") }) .catch(error => { console.log('ERROR: ' + error.response.data); }) }, sendHTMLEmail() { AXIOS.get('/sendHtmlEmail', { headers: this.$data.header }) .then(response => { console.log(response); alert("OK") }) .catch(error => { console.log('ERROR: ' + error.response.data); }) } } } </script> <style> #email { margin-left: 38%; margin-top: 50px; } button { width: 150px; } </style> 


Catatan: jangan lupa untuk memperbarui router.js dan menambahkan tautan ke App.vue navigasi App.vue jika Anda membuat komponen baru.


Migrasi Gradle


Saya akan mengklarifikasi segera: jika item ini dianggap sebagai peningkatan, biarkan semua orang memutuskan untuk proyeknya sendiri. Kami hanya melihat bagaimana melakukan ini.

Secara umum, Anda dapat menggunakan Pindah dari Maven ke Gradle dalam waktu kurang dari 5 menit , tetapi hasilnya tidak sesuai dengan harapan. Saya masih merekomendasikan melakukan migrasi secara manual, tidak akan memakan waktu lebih lama.

Hal pertama yang perlu kita lakukan adalah menginstal Gradle .

Maka kita perlu melakukan prosedur berikut untuk kedua subproyek - backend dan fronted :

# 1 Hapus file Maven - pom.xml , .mvn .

# 2 Dalam direktori sub proyek, jalankan gradle init dan jawab pertanyaan:

  • Pilih jenis proyek untuk menghasilkan: dasar
  • Pilih bahasa implementasi: Kotlin
  • Pilih skrip bangun DSL: Kotlin (karena kami sedang menulis proyek di Kotlin)

# 3 Hapus settings.gradle.kts - file ini hanya diperlukan untuk proyek root.

# 4 Jalankan gradle wrapper .

Sekarang mari kita beralih ke proyek root kami. Untuk itu, Anda harus mengikuti langkah 1, 2 dan 4 yang dijelaskan di atas untuk sub-proyek - semuanya sama kecuali menghapus settings.gradle.kts .

Konfigurasi build untuk proyek backend akan terlihat seperti ini:

build.gradle.kts
 import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { id("org.springframework.boot") version "2.1.3.RELEASE" id("io.spring.dependency-management") version "1.0.8.RELEASE" kotlin("jvm") version "1.3.50" kotlin("plugin.spring") version "1.3.50" id("org.jetbrains.kotlin.plugin.jpa") version "1.3.50" } group = "com.kotlin-spring-vue" version = "0.0.1-SNAPSHOT" java.sourceCompatibility = JavaVersion.VERSION_1_8 repositories { mavenCentral() maven { url = uri("https://plugins.gradle.org/m2/") } } dependencies { runtimeOnly(project(":frontend")) implementation("org.springframework.boot:spring-boot-starter-actuator:2.1.3.RELEASE") implementation("org.springframework.boot:spring-boot-starter-web:2.1.3.RELEASE") implementation("org.springframework.boot:spring-boot-starter-data-jpa:2.1.3.RELEASE") implementation("org.springframework.boot:spring-boot-starter-mail:2.1.3.RELEASE") implementation("org.springframework.boot:spring-boot-starter-security:2.1.3.RELEASE") implementation("org.postgresql:postgresql:42.2.5") implementation("org.springframework.boot:spring-boot-starter-thymeleaf:2.1.3.RELEASE") implementation("commons-io:commons-io:2.4") implementation("io.jsonwebtoken:jjwt:0.9.0") implementation("io.jsonwebtoken:jjwt-api:0.10.6") implementation("com.mashape.unirest:unirest-java:1.4.9") implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.9.8") runtimeOnly("org.springframework.boot:spring-boot-devtools:2.1.3.RELEASE") implementation("org.jetbrains.kotlin:kotlin-reflect") implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") implementation("org.jetbrains.kotlin:kotlin-noarg:1.3.50") testImplementation("org.springframework.boot:spring-boot-starter-test") { exclude(group = "org.junit.vintage", module = "junit-vintage-engine") } } tasks.withType<KotlinCompile> { kotlinOptions { freeCompilerArgs = listOf("-Xjsr305=strict") jvmTarget = "1.8" } } 


  • Semua plugin Kotlin dan Spring yang diperlukan harus ditentukan.
  • Jangan lupa tentang plugin org.jetbrains.kotlin.plugin.jpa - perlu terhubung ke database
  • Dalam dependensi, Anda harus menentukan runtimeOnly(project(":frontend")) - kita perlu membangun proyek frontend terlebih dahulu

Membangun konfigurasi untuk proyek frontend :

build.gradle.kts
 plugins { id("org.siouan.frontend") version "1.2.1" id("java") } group = "com.kotlin-spring-vue" version = "0.0.1-SNAPSHOT" java { targetCompatibility = JavaVersion.VERSION_1_8 } buildscript { repositories { mavenCentral() maven { url = uri("https://plugins.gradle.org/m2/") } } } frontend { nodeVersion.set("10.16.0") cleanScript.set("run clean") installScript.set("install") assembleScript.set("run build") } tasks.named("jar", Jar::class) { dependsOn("assembleFrontend") from("$buildDir/dist") into("static") } 


  • Dalam contoh saya, plugin org.siouan.frontend digunakan untuk membangun proyek
  • Di bagian frontend {...} harus menunjukkan versi Node.js, serta perintah yang menjalankan skrip pembersihan, instalasi dan perakitan yang ditentukan dalam package.json
  • Sekarang kita kemas sub proyek frontend kita menjadi file JAR dan menggunakannya sebagai dependensi ( runtimeOnly(project(":frontend")) di backend ), jadi kita perlu menggambarkan tugas yang menyalin file dari direktori assembly ke / publik dan membuat file jar

Catatan:

  • Edit vue.config.js , ubah direktori build menjadi build / dist .
  • Dalam file skrip build package.json , tentukan build vue-cli-service build atau pastikan itu ditentukan

File settings.gradle.kts dalam proyek root harus berisi kode berikut: ...

 rootProject.name = "demo" include(":frontend", ":backend") 

... adalah nama proyek dan sub-proyek.

Dan sekarang kita dapat membangun proyek dengan menjalankan perintah: ./gradlew build

Catatan: jika untuk placeholder ditentukan dalam application.properties (misalnya, ${SPRING_DATASOURCE_URL} ) tidak ada variabel lingkungan yang sesuai, perakitan akan gagal. Untuk menghindarinya, gunakan /gradlew build -x

Anda dapat memeriksa struktur proyek menggunakan perintah gradle -q projects , hasilnya akan terlihat mirip dengan ini:

 Root project 'demo' +--- Project ':backend' \--- Project ':frontend' 

Dan akhirnya, untuk menjalankan aplikasi, Anda harus menjalankan ./gradlew bootRun .

.gitignore


File dan folder berikut harus ditambahkan ke file .gitignore :

  • backend / build /
  • frontend / bangun /
  • membangun
  • .gradle

Penting: Anda tidak harus menambahkan file gradlew ke .gitignore - tidak ada yang berbahaya di dalamnya, tetapi mereka diperlukan untuk perakitan yang sukses di server jauh.

Penempatan di Heroku


Mari kita lihat perubahan yang perlu kita lakukan agar aplikasi dapat digunakan dengan aman ke Heroku.

Procfile # 1

Kita perlu bertanya pada Heroku instruksi baru untuk meluncurkan aplikasi:

 web: java -Dserver.port=$PORT -jar backend/build/libs/backend-0.0.1-SNAPSHOT.jar 

Variabel lingkungan # 2

Heroku dapat membuat kembali jenis aplikasi (misalnya, aplikasi Spring Boot) dan mengikuti instruksi perakitan yang sesuai. Tetapi aplikasi kami (proyek root) tidak terlihat seperti aplikasi Spring Boot untuk Heroku. Jika kita meninggalkan semuanya apa adanya, Heroku akan meminta kita untuk menentukan stage . Jujur, saya tidak tahu di mana jalan ini berakhir, karena saya tidak mengikutinya. Lebih mudah untuk mendefinisikan variabel GRADLE_TASK dengan nilai build :



# 3 reCAPTCHA

Saat menempatkan aplikasi di domain baru, jangan lupa untuk memperbarui captcha, variabel lingkungan GOOGLE_RECAPTCHA_KEY_SITE dan GOOGLE_RECAPTCHA_KEY_SECRET , serta memperbarui Kunci Situs di subproyek frontend.


Menyimpan Token JWT di Cookie


Pertama-tama, saya sangat menyarankan Anda membaca artikel Harap Berhenti Menggunakan Penyimpanan Lokal , terutama Mengapa Penyimpanan Lokal Tidak Aman dan Anda Tidak Harus Menggunakannya untuk Menyimpan bagian Data Sensitif .

Mari kita lihat bagaimana Anda dapat menyimpan token JWT di tempat yang lebih aman - cookie di bendera httpOnly , yang tidak tersedia untuk dibaca / diubah menggunakan JavaScript.

# 1 Menghapus semua logika terkait JWT dari frontend:
Karena token masih tidak dapat diakses dengan JavaScript dengan metode penyimpanan baru, Anda dapat dengan aman menghapus semua referensi dari sub-proyek kami.

Tetapi peran pengguna tanpa referensi ke data lain adalah informasi yang tidak begitu penting, masih dapat disimpan di Penyimpanan Lokal dan ditentukan apakah pengguna berwenang atau tidak, tergantung pada apakah peran ini didefinisikan.

store / index.js
 import Vue from 'vue'; import Vuex from 'vuex'; Vue.use(Vuex); const state = { role: localStorage.getItem('user-role') || '', username: localStorage.getItem('user-name') || '', authorities: localStorage.getItem('authorities') || '', }; const getters = { isAuthenticated: state => { if (state.role != null && state.role != '') { 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; } }; const mutations = { auth_login: (state, user) => { localStorage.setItem('user-name', user.username); localStorage.setItem('user-authorities', user.roles); 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-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 }); 


Berhati-hatilah saat refactoring store/index.js: jika otorisasi dan deauthorisasi tidak berfungsi dengan benar, pesan kesalahan akan terus-menerus masuk ke konsol.

# 2 Kembalikan JWT sebagai cookie di pengontrol otorisasi ( bukan di badan respons ):

AuthController.kt
 @Value("\${ksvg.app.authCookieName}") lateinit var authCookieName: String @Value("\${ksvg.app.isCookieSecure}") var isCookieSecure: Boolean = true @PostMapping("/signin") fun authenticateUser(@Valid @RequestBody loginRequest: LoginUser, response: HttpServletResponse): ResponseEntity<*> { val userCandidate: Optional <User> = userRepository.findByUsername(loginRequest.username!!) if (!captchaService.validateCaptcha(loginRequest.recaptchaToken!!)) { return ResponseEntity(ResponseMessage("Validation failed (ReCaptcha v2)"), HttpStatus.BAD_REQUEST) } else 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 cookie: Cookie = Cookie(authCookieName, jwt) cookie.maxAge = jwtProvider.jwtExpiration!! cookie.secure = isCookieSecure cookie.isHttpOnly = true cookie.path = "/" response.addCookie(cookie) val authorities: List<GrantedAuthority> = user.roles!!.stream().map({ role -> SimpleGrantedAuthority(role.name)}).collect(Collectors.toList<GrantedAuthority>()) return ResponseEntity.ok(SuccessfulSigninResponse(user.username, authorities)) } else { return ResponseEntity(ResponseMessage("User not found!"), HttpStatus.BAD_REQUEST) } } 


Penting : Silakan dicatat bahwa saya menempatkan pilihan authCookieNamedan isCookieSecuredi application.properties - mengirimkan cookies untuk bendera securehanya dapat https, yang membuatnya sangat sulit untuk debug dengan localhost. TETAPI dalam produksi, tentu saja, lebih baik menggunakan cookie dengan bendera ini.

Selain itu, disarankan untuk tanggapan controller untuk menggunakan entitas tanpa bidang khusus untuk JWT. Pembaruan

# 3JwtAuthTokenFilter :
Kami dulu mengambil token dari header permintaan, sekarang kami mengambilnya dari cookie:

JwtAuthTokenFilter.kt
 @Value("\${ksvg.app.authCookieName}") lateinit var authCookieName: String ... private fun getJwt(request: HttpServletRequest): String? { for (cookie in request.cookies) { if (cookie.name == authCookieName) { return cookie.value } } return null } 


# 4 Mengaktifkan CORS
Jika dalam artikel saya sebelumnya Anda masih bisa dengan tenang melewati pertanyaan ini, sekarang akan aneh untuk melindungi token JWT tanpa harus mengaktifkan CORS di sisi backend.

Anda dapat memperbaikinya dengan mengedit WebSecurityConfig.kt:

WebSecurityConfig.kt
 @Bean fun corsConfigurationSource(): CorsConfigurationSource { val configuration = CorsConfiguration() configuration.allowedOrigins = Arrays.asList("http://localhost:8080", "http://localhost:8081", "https://kotlin-spring-vue-gradle-demo.herokuapp.com") configuration.allowedHeaders = Arrays.asList("*") configuration.allowedMethods = Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS") configuration.allowCredentials = true configuration.maxAge = 3600 val source = UrlBasedCorsConfigurationSource() source.registerCorsConfiguration("/**", configuration) return source } @Throws(Exception::class) override fun configure(http: HttpSecurity) { http .cors().and() .csrf().disable().authorizeRequests() .antMatchers("/**").permitAll() .anyRequest().authenticated() .and() .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter::class.java) http.headers().cacheControl().disable() } 


Dan sekarang Anda dapat menghapus semua anotasi @CrossOrigindari pengontrol.

Penting: parameter AllowCredentials diperlukan untuk mengirim permintaan dari frontend. Baca lebih lanjut tentang ini di sini .

# 5 Memperbarui tajuk di sisi front-end:

http-commons.js
 export const AXIOS = axios.create({ baseURL: `/api`, headers: { 'Access-Control-Allow-Origin': ['http://localhost:8080', 'http://localhost:8081', 'https://kotlin-spring-vue-gradle-demo.herokuapp.com'], 'Access-Control-Allow-Methods': 'GET,POST,DELETE,PUT,OPTIONS', 'Access-Control-Allow-Headers': '*', 'Access-Control-Allow-Credentials': true } }) 


Periksa


Mari kita coba masuk ke aplikasi dengan masuk dari host yang tidak ada dalam daftar yang diizinkan WebSecurityConfig.kt. Untuk melakukan ini, jalankan backend di port 8080, dan frontend, misalnya, aktif 8082dan coba masuk:

Hasil


Permintaan otorisasi ditolak oleh kebijakan CORS.

Sekarang mari kita lihat bagaimana flag cookies bekerja secara umum httpOnly. Untuk melakukan ini, mari kita pergi, misalnya, ke situs https://kotlinlang.org dan jalankan di konsol browser:

 document.cookie 

Hasil


Non- httpOnlycookie yang terkait dengan situs ini akan muncul di konsol , yang, seperti yang kita lihat, dapat diakses melalui JavaScript.

Sekarang mari kita masuk ke aplikasi kita, login (sehingga browser menyimpan cookie dengan JWT) dan ulangi hal yang sama:

Hasil


Catatan: metode penyimpanan token JWT ini lebih andal daripada menggunakan Penyimpanan Lokal, tetapi Anda harus memahami bahwa ini bukan obat mujarab.


Konfirmasi Pendaftaran Email


Algoritma singkat untuk melakukan tugas ini adalah sebagai berikut:

  1. Untuk semua pengguna baru, atribut isEnableddalam database diatur kefalse
  2. Token string dihasilkan dari karakter arbitrer, yang akan berfungsi sebagai kunci untuk mengonfirmasi pendaftaran
  3. Token dikirimkan kepada pengguna melalui pos sebagai bagian dari tautan
  4. Atribut isEnableddisetel ke true jika pengguna mengikuti tautan untuk periode waktu yang ditentukan.

Sekarang pertimbangkan proses ini secara lebih rinci.

Kami membutuhkan meja untuk menyimpan token untuk mengonfirmasi pendaftaran:

 CREATE TABLE public.verification_token ( id serial NOT NULL, token character varying, expiry_date timestamp without time zone, user_id integer, PRIMARY KEY (id) ); ALTER TABLE public.verification_token ADD CONSTRAINT verification_token_users_fk FOREIGN KEY (user_id) REFERENCES public.users (id) MATCH SIMPLE ON UPDATE CASCADE ON DELETE CASCADE; 

Dan, karenanya, entitas baru untuk pemetaan objek-relasional ...:

VerificationToken.kt
 package com.kotlinspringvue.backend.jpa import java.sql.* import javax.persistence.* import java.util.Calendar @Entity @Table(name = "verification_token") data class VerificationToken( @Id @GeneratedValue(strategy = GenerationType.AUTO) val id: Long? = 0, @Column(name = "token") var token: String? = null, @Column(name = "expiry_date") val expiryDate: Date, @OneToOne(targetEntity = User::class, fetch = FetchType.EAGER, cascade = [CascadeType.PERSIST]) @JoinColumn(nullable = false, name = "user_id") val user: User ) { constructor(token: String?, user: User) : this(0, token, calculateExpiryDate(1440), user) } private fun calculateExpiryDate(expiryTimeInMinutes: Int): Date { val cal = Calendar.getInstance() cal.time = Timestamp(cal.time.time) cal.add(Calendar.MINUTE, expiryTimeInMinutes) return Date(cal.time.time) } 


... dan repositori:

VerificationTokenRepository.kt
 package com.kotlinspringvue.backend.repository import com.kotlinspringvue.backend.jpa.VerificationToken import org.springframework.data.jpa.repository.JpaRepository import java.util.* interface VerificationTokenRepository : JpaRepository<VerificationToken, Long> { fun findByToken(token: String): Optional<VerificationToken> } 


Sekarang kita perlu mengimplementasikan alat untuk mengelola token - membuat, memverifikasi dan mengirim melalui email. Untuk melakukan ini, kami memodifikasi UserDetailsServiceImpldengan menambahkan metode untuk membuat dan memverifikasi token:

UserDetailsServiceImpl.kt
 override fun createVerificationTokenForUser(token: String, user: User) { tokenRepository.save(VerificationToken(token, user)) } override fun validateVerificationToken(token: String): String { val verificationToken: Optional<VerificationToken> = tokenRepository.findByToken(token) if (verificationToken.isPresent) { val user: User = verificationToken.get().user val cal: Calendar = Calendar.getInstance() if ((verificationToken.get().expiryDate.time - cal.time.time) <= 0) { tokenRepository.delete(verificationToken.get()) return TOKEN_EXPIRED } user.enabled = true tokenRepository.delete(verificationToken.get()) userRepository.save(user) return TOKEN_VALID } else { return TOKEN_INVALID } } 


Sekarang tambahkan metode untuk mengirim email dengan tautan konfirmasi ke EmailServiceImpl:

EmailServiceImpl.kt
  @Value("\${host.url}") lateinit var hostUrl: String @Autowired lateinit var userDetailsService: UserDetailsServiceImpl ... override fun sendRegistrationConfirmationEmail(user: User) { val token = UUID.randomUUID().toString() userDetailsService.createVerificationTokenForUser(token, user) val link = "$hostUrl/?token=$token&confirmRegistration=true" val msg = "<p>Please, follow the link to complete your registration:</p><p><a href=\"$link\">$link</a></p>" user.email?.let{sendHtmlMessage(user.email!!, "KSVG APP: Registration Confirmation", msg)} } 


Catatan:

  • Saya akan merekomendasikan menyimpan URL host di application.properties
  • Di tautan kami, kami meneruskan dua parameter GET ( tokendan confirmRegistration) ke alamat tempat aplikasi tersebut digunakan. Nanti saya akan jelaskan kenapa.

Kami memodifikasi pengontrol pendaftaran sebagai berikut:

  • Semua pengguna baru akan menetapkan nilai falseuntuk bidang tersebutisEnabled
  • Setelah membuat akun baru, kami akan mengirim email untuk mengonfirmasi pendaftaran
  • Buat pengontrol validasi token yang terpisah
  • Penting: selama otorisasi kami akan memeriksa apakah akun diverifikasi:

AuthController.kt
 package com.kotlinspringvue.backend.controller import com.kotlinspringvue.backend.email.EmailService import javax.validation.Valid import java.util.* import java.util.stream.Collectors 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.crypto.password.PasswordEncoder import org.springframework.security.core.GrantedAuthority import org.springframework.security.core.authority.SimpleGrantedAuthority import org.springframework.ui.Model import com.kotlinspringvue.backend.model.LoginUser import com.kotlinspringvue.backend.model.NewUser import com.kotlinspringvue.backend.web.response.SuccessfulSigninResponse 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 import com.kotlinspringvue.backend.service.ReCaptchaService import com.kotlinspringvue.backend.service.UserDetailsService import org.springframework.beans.factory.annotation.Value import org.springframework.context.ApplicationEventPublisher import org.springframework.web.bind.annotation.* import org.springframework.web.context.request.WebRequest import java.io.UnsupportedEncodingException import javax.servlet.http.Cookie import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletResponse import com.kotlinspringvue.backend.service.UserDetailsServiceImpl.Companion.TOKEN_VALID import com.kotlinspringvue.backend.service.UserDetailsServiceImpl.Companion.TOKEN_INVALID import com.kotlinspringvue.backend.service.UserDetailsServiceImpl.Companion.TOKEN_EXPIRED @RestController @RequestMapping("/api/auth") class AuthController() { @Value("\${ksvg.app.authCookieName}") lateinit var authCookieName: String @Value("\${ksvg.app.isCookieSecure}") var isCookieSecure: Boolean = true @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 @Autowired lateinit var captchaService: ReCaptchaService @Autowired lateinit var userService: UserDetailsService @Autowired lateinit var emailService: EmailService @PostMapping("/signin") fun authenticateUser(@Valid @RequestBody loginRequest: LoginUser, response: HttpServletResponse): ResponseEntity<*> { val userCandidate: Optional <User> = userRepository.findByUsername(loginRequest.username!!) if (!captchaService.validateCaptcha(loginRequest.recaptchaToken!!)) { return ResponseEntity(ResponseMessage("Validation failed (ReCaptcha v2)"), HttpStatus.BAD_REQUEST) } else if (userCandidate.isPresent) { val user: User = userCandidate.get() if (!user.enabled) { return ResponseEntity(ResponseMessage("Account is not verified yet! Please, follow the link in the confirmation email."), HttpStatus.UNAUTHORIZED) } val authentication = authenticationManager.authenticate( UsernamePasswordAuthenticationToken(loginRequest.username, loginRequest.password)) SecurityContextHolder.getContext().setAuthentication(authentication) val jwt: String = jwtProvider.generateJwtToken(user.username!!) val cookie: Cookie = Cookie(authCookieName, jwt) cookie.maxAge = jwtProvider.jwtExpiration!! cookie.secure = isCookieSecure cookie.isHttpOnly = true cookie.path = "/" response.addCookie(cookie) val authorities: List<GrantedAuthority> = user.roles!!.stream().map({ role -> SimpleGrantedAuthority(role.name)}).collect(Collectors.toList<GrantedAuthority>()) return ResponseEntity.ok(SuccessfulSigninResponse(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 (!captchaService.validateCaptcha(newUser.recaptchaToken!!)) { return ResponseEntity(ResponseMessage("Validation failed (ReCaptcha v2)"), HttpStatus.BAD_REQUEST) } else 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) } try { // Creating user's account val user = User( 0, newUser.username!!, newUser.firstName!!, newUser.lastName!!, newUser.email!!, encoder.encode(newUser.password), false ) user.roles = Arrays.asList(roleRepository.findByName("ROLE_USER")) val registeredUser = userRepository.save(user) emailService.sendRegistrationConfirmationEmail(registeredUser) } catch (e: Exception) { return ResponseEntity(ResponseMessage("Server error. Please, contact site owner"), HttpStatus.SERVICE_UNAVAILABLE) } return ResponseEntity(ResponseMessage("Please, follow the link in the confirmation email to complete the registration."), HttpStatus.OK) } else { return ResponseEntity(ResponseMessage("User already exists!"), HttpStatus.BAD_REQUEST) } } @PostMapping("/registrationConfirm") @CrossOrigin(origins = ["*"]) @Throws(UnsupportedEncodingException::class) fun confirmRegistration(request: HttpServletRequest, model: Model, @RequestParam("token") token: String): ResponseEntity<*> { when(userService.validateVerificationToken(token)) { TOKEN_VALID -> return ResponseEntity.ok(ResponseMessage("Registration confirmed")) TOKEN_INVALID -> return ResponseEntity(ResponseMessage("Token is invalid!"), HttpStatus.BAD_REQUEST) TOKEN_EXPIRED -> return ResponseEntity(ResponseMessage("Token is invalid!"), HttpStatus.UNAUTHORIZED) } return ResponseEntity(ResponseMessage("Server error. Please, contact site owner"), HttpStatus.SERVICE_UNAVAILABLE) } @PostMapping("/logout") fun logout(response: HttpServletResponse): ResponseEntity<*> { val cookie: Cookie = Cookie(authCookieName, null) cookie.maxAge = 0 cookie.secure = isCookieSecure cookie.isHttpOnly = true cookie.path = "/" response.addCookie(cookie) return ResponseEntity.ok(ResponseMessage("Successfully logged")) } private fun emailExists(email: String): Boolean { return userRepository.findByUsername(email).isPresent } private fun usernameExists(username: String): Boolean { return userRepository.findByUsername(username).isPresent } } 


Sekarang mari kita bekerja di frontend:

# 1 Buat komponen RegistrationConfirmPage.vue

# 2 Tambahkan jalur baru router.jsdengan parameter :token:

 { path: '/registration-confirm/:token', name: 'RegistrationConfirmPage', component: RegistrationConfirmPage } 

Pembaruan # 3SignUp.vue - setelah berhasil mengirim data dari formulir, kami akan memberi tahu mereka bahwa untuk menyelesaikan pendaftaran, Anda harus mengikuti tautan dalam surat itu.

# 4 Penting: sayangnya, kami tidak dapat memberikan tautan tetap ke komponen terpisah yang akan memvalidasi token dan melaporkan keberhasilan atau kegagalan. Tautan dengan garis miring semuanya akan membawa kita ke halaman asli aplikasi. Tetapi kami dapat memberi tahu aplikasi kami tentang perlunya mengonfirmasi pendaftaran menggunakan parameter GET yang dikirimkan confirmRegistration:

 methods: { confirmRegistration() { if (this.$route.query.confirmRegistration === 'true' && this.$route.query.token != null) { this.$router.push({name: 'RegistrationConfirmPage', params: { token: this.$route.query.token}}); } }, ... mounted() { this.confirmRegistration(); } 

# 5 Mari kita buat komponen yang melakukan validasi token dan melaporkan hasil validasi:

RegistrasiConfirmPage.vue
 <template> <div id="registration-confirm"> <div class="confirm-form"> <b-card title="Confirmation" tag="article" style="max-width: 20rem;" class="mb-2" > <div v-if="isSuccess"> <p class="success">Account is successfully verified!</p> <router-link to="/login"> <b-button variant="primary">Login</b-button> </router-link> </div> <div v-if="isError"> <p class="fail">Verification failed:</p> <p>{{ errorMessage }}</p> </div> </b-card> </div> </div> </template> <script> import {AXIOS} from './http-common' export default { name: 'RegistrationConfirmPage', data() { return { isSuccess: false, isError: false, errorMessage: '' } }, methods: { executeVerification() { AXIOS.post(`/auth/registrationConfirm`, null, {params: { 'token': this.$route.params.token}}) .then(response => { this.isSuccess = true; console.log(response); }, error => { this.isError = true; this.errorMessage = error.response.data.message; }) .catch(e => { console.log(e); this.errorMessage = 'Server error. Please, report this error website owners'; }) } }, mounted() { this.executeVerification(); } } </script> <style scoped> .confirm-form { margin-left: 38%; margin-top: 50px; } .success { color: green; } .fail { color: red; } </style> 


Hasil


Alih-alih sebuah kesimpulan


Sebagai kesimpulan dari materi ini, saya ingin melakukan penyimpangan dan mengatakan bahwa konsep aplikasi yang dibahas dalam artikel ini dan sebelumnya tidak baru pada saat penulisan. Tugas dengan cepat membuat tumpukan penuh aplikasi pada musim semi Boot menggunakan yang modern JavaScript-kerangka sudut / Bereaksi / Vue.js elegan memecahkan Hipster .

Namun, ide-ide yang dijelaskan dalam artikel ini dapat diimplementasikan bahkan menggunakan JHipster, jadi saya berharap para pembaca yang datang ke tempat ini akan menemukan bahan ini berguna bahkan sebagai bahan pertimbangan.


Tautan yang bermanfaat


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


All Articles