Webanwendung auf Kotlin + Spring Boot + Vue.js (Add-On)

Guten Tag, liebe Einwohner von Habr!

Wie der Name schon sagt, handelt es sich bei diesem Artikel um eine Erweiterung der zuvor auf Kotlin + Spring Boot + Vue.js geschriebenen Webanwendung , mit der wir das Grundgerüst einer zukünftigen Anwendung verbessern und die Arbeit damit einfacher gestalten können.

Bevor ich mit der Geschichte beginne, möchte ich mich bei allen bedanken, die im vorherigen Artikel einen Kommentar abgegeben haben.

Inhalt




CI / CD-Setup (Heroku)


Betrachten Sie die Implementierung einer kontinuierlichen Integration und Bereitstellung am Beispiel der Heroku- Cloud-PaaS-Plattform.

Als erstes müssen wir den Anwendungscode in das Repository von GitHub stellen . Damit im Repository nichts überflüssig ist, empfehle ich den folgenden Inhalt der .gitignore- Datei:

.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 


Wichtig: Bevor Sie mit Heroku arbeiten, fügen Sie dem Stammverzeichnis eine Datei namens Procfile (ohne Erweiterung) mit der folgenden Zeile hinzu:
web: java -Dserver.port=$PORT -jar backend/target/backend-0.0.1-SNAPSHOT.jar , wobei backend-0.0.1-SNAPSHOT.jar der Name der Assembly- JAR-Datei ist. Und seien Sie sicher, sich zu verpflichten und zu drücken .

Hinweis: Sie können die Datei travis.yaml auch zum Stammverzeichnis hinzufügen, um die Erstellungs- und Bereitstellungszeit der Anwendung auf Heroku zu verkürzen:

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


Dann:

# 1 Registrieren Sie sich bei Heroku .

# 2 Erstellen Sie eine neue Anwendung:

Erstellen Sie eine neue Anwendung


# 3 Mit Heroku können Sie zusätzliche Ressourcen mit der Anwendung verbinden, z. B. die PostreSQL-Datenbank. Gehen Sie dazu wie folgt vor: Anwendung -> Ressourcen -> Add-Ons -> Heroku Postgres :

Heroku Postgres


# 4 Wähle einen Plan:

Planauswahl


# 5 Jetzt können Sie die verbundene Ressource sehen:

Verbundene Ressource


# 6 Sehen Sie sich die Anmeldeinformationen an. Sie werden zum Einrichten von Umgebungsvariablen benötigt: Einstellungen -> Anmeldeinformationen anzeigen :

Anmeldeinformationen anzeigen


# 7 Umgebungsvariablen festlegen: Anwendung -> Einstellungen -> Konfigurationsvariablen anzeigen :

Umgebungsvariablen


# 8 Legen Sie die Umgebungsvariablen für die Verbindung im folgenden Format fest:

 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> 

Wie sieht es aus?


# 9 Erstellen Sie alle erforderlichen Tabellen in der neuen Datenbank.

# 10 Die Datei application.properties sollte ungefähr so ​​aussehen:

application.properties
 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 Neue Pipeline erstellen - Neue Pipeline erstellen :

Neue Pipeline erstellen


# 12 Bereitstellungsmethode - GitHub (klicken Sie auf Connect to GitHub und folgen Sie den Anweisungen in einem neuen Fenster).

# 13 Automatische Bereitstellungen aktivieren :

Aktivieren Sie die automatische Bereitstellung


# 14 Manuelle Bereitstellung - Klicken Sie für die erste Bereitstellung auf Zweig bereitstellen . Direkt im Browser sehen Sie die Ausgabe der Kommandozeile.

Manuelle Bereitstellung


# 15 Klicken Sie nach erfolgreicher Erstellung auf Anzeigen, um die bereitgestellte Anwendung zu öffnen:

Ansehen



Bot-Schutz (reCAPTCHA)


Der erste Schritt, um die reCAPTCHA-Überprüfung in unserer Anwendung zu aktivieren, ist das Erstellen eines neuen reCAPTCH im Google-Administrationsbereich . Dort legen wir eine neue Site an (Add new site / Create) und nehmen folgende Einstellungen vor:

ReCAPTCHA-Einstellungen


Im Abschnitt Domains sollten Sie zusätzlich zu der Adresse, unter der die Anwendung localhost , localhost angeben, damit Sie beim Debuggen Probleme vermeiden, wenn Sie sich nicht bei Ihrer Anwendung anmelden können.

Backend

Speichern Sie den Site-Schlüssel und den geheimen Schlüssel ...

Standortschlüssel / geheimer Schlüssel


... um sie dann Umgebungsvariablen zuzuweisen und die Variablennamen wiederum neuen application.properties- Eigenschaften zuzuweisen:

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

Fügen Sie eine neue Abhängigkeit in pom.xml zur Überprüfung von reCAPTCHA-Token auf der Google-Seite hinzu, die der Client an uns sendet:

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

Jetzt ist es an der Zeit, die Entitäten zu aktualisieren, mit denen wir Benutzer autorisieren und registrieren, indem wir ihnen ein Zeichenfolgenfeld für dasselbe reCAPTCHA-Token hinzufügen:

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 } } 


Fügen Sie einen kleinen Dienst hinzu, der das reCAPTCHA-Token an einen speziellen Google-Dienst sendet, und geben Sie als Antwort, ob das Token die Überprüfung bestanden hat:

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) } } 


Dieser Dienst muss im Benutzerregistrierungs- und Berechtigungscontroller verwendet werden:

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

Der erste Schritt besteht darin, das reCAPTHA Paket zu installieren und zu speichern:

 $ npm install --save vue-recaptcha 

Stellen Sie dann eine Verbindung zum Skript in index.html her :

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

Fügen Sie ein beliebiges Captcha zu einem beliebigen freien Platz auf der Seite hinzu:

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

Und die Schaltfläche für die Zielaktion (Autorisierung oder Registrierung) ruft jetzt zuerst die Validierungsmethode auf:

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

Fügen Sie den Komponenten die Abhängigkeit hinzu:

 import VueRecaptcha from 'vue-recaptcha' 

Exportvorgabe bearbeiten:

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

Und füge neue Methoden hinzu:

  • validateCaptcha() - wird durch Klicken auf die Schaltfläche aufgerufen
  • onCapthcaVerified(recaptchaToken) onCaptchaExpired() - das Captcha selbst aufruft

Neue Methoden
 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() } 


Ergebnis



E-Mail senden


Ziehen Sie die Möglichkeit in Betracht, Briefe über einen öffentlichen Mailserver wie Google oder Mail.ru an unsere Anwendung zu senden.

Der erste Schritt besteht darin, ein Konto auf dem ausgewählten Mailserver zu erstellen, sofern dies noch nicht geschehen ist.

Im zweiten Schritt müssen wir die folgenden Abhängigkeiten in pom.xml hinzufügen:

Abhängigkeiten
 <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> 


Außerdem müssen Sie application.properties neue Eigenschaften hinzufügen:

SMTP-Eigenschaften
 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 


Sie können die SMTP-Einstellungen hier festlegen: Google und Mail.ru

Erstellen Sie eine Schnittstelle, in der wir mehrere Methoden deklarieren:

  • So senden Sie eine normale SMS
  • So senden Sie eine HTML-E-Mail
  • So senden Sie eine E-Mail mithilfe einer Vorlage

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) } 


Jetzt erstellen wir eine Implementierung dieser Schnittstelle - den Dienst zum Versenden von E-Mails:

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() } } } 


  • Wir verwenden den automatisch konfigurierten JavaMailSender von Spring, um E-Mails zu versenden
  • Das Versenden von normalen Briefen ist denkbar einfach: Sie müssen lediglich Text in den Briefkörper einfügen und ihn senden
  • HTML-E-Mails werden als Mime Type-Nachrichten definiert und ihr Inhalt als text/html
  • Zur Verarbeitung der HTML-Nachrichtenvorlage verwenden wir die Spring Template Engine

Erstellen wir eine einfache Vorlage zum Schreiben mit dem Thymeleaf- Framework, indem wir sie in src / main / resources / templates / platzieren :

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> 


Die sich ändernden Elemente der Vorlage (in unserem Fall der Name des Empfängers und der Pfad des Bildes zur Signatur) werden mit Platzhaltern deklariert.

Erstellen oder aktualisieren Sie nun einen Controller, der Briefe sendet:

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) } 


Hinweis: Um sicherzustellen, dass alles funktioniert, senden wir uns zunächst Briefe.

Um sicherzustellen, dass alles funktioniert, können wir auch eine bescheidene Webschnittstelle erstellen, die einfach die Webdienstmethoden abruft:

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> 


Hinweis: Vergessen Sie nicht, router.js zu aktualisieren und den Link zur App.vue Navigationsleiste hinzuzufügen, wenn Sie eine neue Komponente erstellen.


Gradle Migration


Ich werde gleich klarstellen: Sollte dieser Punkt als Verbesserung angesehen werden, sollte jeder für sein eigenes Projekt entscheiden. Wir schauen uns nur an, wie das geht.

Im Allgemeinen können Sie den Umzug von Maven nach Gradle in weniger als 5 Minuten durchführen , aber es ist unwahrscheinlich, dass das Ergebnis den Erwartungen entspricht. Ich würde weiterhin empfehlen, die Migration manuell durchzuführen, da dies nicht viel länger dauert.

Als erstes müssen wir Gradle installieren .

Dann müssen wir das folgende Verfahren für beide Teilprojekte ausführen - backend und fronted :

# 1 Maven-Dateien löschen - pom.xml , .mvn .

# 2 Führen Sie im Unterprojektverzeichnis gradle init aus und beantworten Sie die Fragen:

  • Wählen Sie den zu generierenden Projekttyp aus: basic
  • Implementierungssprache auswählen: Kotlin
  • Wähle Build Script DSL: Kotlin (da wir ein Projekt in Kotlin schreiben)

# 3 Lösche settings.gradle.kts - Diese Datei wird nur für das Root-Projekt benötigt.

# 4 Führen Sie gradle wrapper .

Wenden wir uns nun unserem Stammprojekt zu. Dazu müssen Sie die oben beschriebenen Schritte 1, 2 und 4 für Unterprojekte ausführen - bis auf das Löschen von settings.gradle.kts ist alles gleich.

Die Build-Konfiguration für das Backend- Projekt sieht folgendermaßen aus:

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" } } 


  • Alle erforderlichen Kotlin- und Spring-Plugins sollten angegeben werden.
  • Vergessen Sie nicht das Plugin org.jetbrains.kotlin.plugin.jpa - es ist notwendig, eine Verbindung zur Datenbank herzustellen
  • In Abhängigkeiten müssen Sie runtimeOnly(project(":frontend")) angeben - wir müssen zuerst das Frontend- Projekt erstellen

Erstellen Sie die Konfiguration für das Frontend- Projekt:

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") } 


  • In meinem Beispiel wird das org.siouan.frontend Plugin zum Erstellen des Projekts verwendet
  • Im frontend {...} Abschnitt frontend {...} sollten frontend {...} die Version von Node.js sowie die Befehle angeben, die die in package.json angegebenen Reinigungs-, Installations- und Assemblierungsskripten package.json
  • Jetzt packen wir unser Frontend-Unterprojekt in eine JAR-Datei und verwenden es als Abhängigkeit ( runtimeOnly(project(":frontend")) im Backend ). runtimeOnly(project(":frontend")) müssen wir eine Aufgabe beschreiben, die die Dateien aus dem Assembly-Verzeichnis nach / kopiert. public und erstellt eine JAR-Datei

Hinweis:

  • Bearbeiten Sie vue.config.js und ändern Sie das Erstellungsverzeichnis in build / dist .
  • package.json in der package.json Build für vue-cli-service build oder stellen Sie sicher, dass dieser angegeben ist

Die Datei settings.gradle.kts im Stammprojekt sollte den folgenden Code enthalten: ...

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

... ist der Name des Projekts und der Teilprojekte.

Und jetzt können wir das Projekt ./gradlew build indem wir den Befehl ./gradlew build

Hinweis: Wenn für in application.properties angegebene Platzhalter (z. B. ${SPRING_DATASOURCE_URL} ) keine entsprechenden Umgebungsvariablen vorhanden sind, ${SPRING_DATASOURCE_URL} die Assembly fehl. Um dies zu vermeiden, verwenden Sie /gradlew build -x

Sie können die Struktur von Projekten mit dem Befehl gradle -q projects überprüfen. Das Ergebnis sollte gradle -q projects so aussehen:

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

Und schließlich müssen ./gradlew bootRun , um die Anwendung ./gradlew bootRun .

.gitignore


Die folgenden Dateien und Ordner sollten der .gitignore- Datei hinzugefügt werden:

  • backend / build /
  • Frontend / Build /
  • bauen
  • .gradle

Wichtig: Sie sollten gradlew Dateien nicht zu .gitignore hinzufügen - sie gradlew keine gradlew Dateien, sind jedoch für eine erfolgreiche Assemblierung auf einem Remoteserver erforderlich.

Einsatz auf Heroku


Sehen wir uns die Änderungen an, die erforderlich sind, damit die App sicher in Heroku bereitgestellt werden kann.

# 1 Procfile

Wir müssen Heroku nach neuen Anweisungen fragen, um die Anwendung zu starten:

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

# 2 Umgebungsvariablen

Heroku kann den Anwendungstyp (z. B. die Spring Boot-Anwendung) neu erstellen und die entsprechenden Montageanweisungen befolgen. Unsere Anwendung (das Root-Projekt) sieht für Heroku jedoch nicht wie eine Spring Boot-Anwendung aus. Wenn wir alles so lassen, wie es ist, werden wir von Heroku gebeten, die stage zu definieren. Ehrlich gesagt, ich weiß nicht, wo dieser Weg endet, weil ich ihm nicht gefolgt bin. Es ist einfacher, eine GRADLE_TASK Variable mit einem build Wert zu definieren:



# 3 reCAPTCHA

Vergessen Sie beim Platzieren der Anwendung in einer neuen Domäne nicht, das Captcha, die Umgebungsvariablen GOOGLE_RECAPTCHA_KEY_SITE und GOOGLE_RECAPTCHA_KEY_SECRET sowie den Site-Schlüssel im Frontend-Teilprojekt zu aktualisieren.


Speichern von JWT-Token in Cookies


Zuallererst empfehle ich dringend, dass Sie den Artikel Bitte verwenden Sie keinen lokalen Speicher mehr lesen, insbesondere den Abschnitt Warum lokaler Speicher unsicher ist und Sie ihn nicht zum Speichern vertraulicher Daten verwenden sollten .

Schauen wir uns an, wie Sie das JWT-Token an einem sichereren Ort speichern können - Cookies in der httpOnly , wo es nicht zum Lesen / Ändern mit JavaScript verfügbar ist.

# 1 Entfernen der gesamten JWT-bezogenen Logik vom Frontend:
Da das Token mit der neuen Speichermethode immer noch nicht mit JavaScript zugänglich ist, können Sie alle Verweise darauf sicher aus unserem Teilprojekt entfernen.

Die Rolle des Benutzers ohne Bezug zu anderen Daten ist jedoch keine so wichtige Information. Sie kann dennoch im lokalen Speicher gespeichert und bestimmt werden, ob der Benutzer berechtigt ist oder nicht, je nachdem, ob diese Rolle definiert ist.

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 }); 


Seien Sie beim Refactoring vorsichtig store/index.js: Wenn Autorisierung und Deaktivierung nicht ordnungsgemäß funktionieren, werden Fehlermeldungen dauerhaft in der Konsole belästigt.

# 2 Geben Sie das JWT als Cookie im Berechtigungscontroller zurück ( nicht im Antworttext ):

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) } } 


Wichtig : Bitte beachten Sie, dass ich Put - Optionen authCookieNameund isCookieSecurein application.properties - Cookies zur Flagge Senden securekann nur https, die es extrem schwierig zu debuggen mit localhost macht. ABER in der Produktion ist es natürlich besser, Cookies mit dieser Flagge zu verwenden.

Außerdem ist es jetzt ratsam, dass die Antworten des Controllers eine Entität ohne ein spezielles Feld für JWT verwenden.

# 3 Update JwtAuthTokenFilter:
Früher haben wir ein Token aus dem Anforderungsheader genommen, jetzt nehmen wir es aus Cookies:

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 Aktivieren von CORS
Wenn Sie diese Frage in meinem vorherigen Artikel noch im Hintergrund auslassen könnten, wäre es jetzt seltsam, das JWT-Token zu schützen, ohne CORS auf der Back-End-Seite aktivieren zu müssen.

Sie können dies beheben, indem Sie Folgendes bearbeiten 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() } 


Und jetzt können Sie alle Anmerkungen @CrossOriginvon den Steuerungen entfernen .

Wichtig: Der Parameter AllowCredentials ist erforderlich, um Anforderungen vom Frontend aus zu senden. Weitere Informationen dazu finden Sie lesen hier .

# 5 Aktualisieren der Header auf der Front-End-Seite:

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 } }) 


Überprüfen Sie


Versuchen Sie, sich bei der Anwendung anzumelden, indem Sie sich von einem Host aus anmelden, der nicht in der Liste der zulässigen Hosts aufgeführt ist WebSecurityConfig.kt. Führen Sie dazu das Backend auf dem Port 8080und das Frontend aus, 8082und versuchen Sie, sich anzumelden:

Ergebnis


Autorisierungsanfrage von CORS-Richtlinie abgelehnt.

Nun wollen wir sehen, wie Flag-Cookies im Allgemeinen funktionieren httpOnly. Gehen wir dazu zum Beispiel zur Site https://kotlinlang.org und führen Sie sie in der Browserkonsole aus:

 document.cookie 

Ergebnis


In der Konsole werden Nicht- httpOnlyCookies angezeigt, die mit dieser Site in Verbindung stehen. Wie wir sehen, können Sie über JavaScript darauf zugreifen.

Gehen wir jetzt in unsere Anwendung, melden uns an (damit der Browser den Cookie mit JWT speichert ) und wiederholen dasselbe:

Ergebnis


Hinweis: Diese Methode zum Speichern eines JWT-Tokens ist zuverlässiger als die Verwendung des lokalen Speichers. Sie sollten jedoch verstehen, dass dies kein Allheilmittel ist.


Bestätigung der E-Mail-Registrierung


Ein kurzer Algorithmus zum Ausführen dieser Aufgabe lautet wie folgt:

  1. Für alle neuen Benutzer wird das Attribut isEnabledin der Datenbank auf festgelegtfalse
  2. Ein Zeichenfolgentoken wird aus beliebigen Zeichen generiert, die als Schlüssel zur Bestätigung der Registrierung dienen
  3. Das Token wird als Teil des Links an den Benutzer in der E-Mail gesendet
  4. Das Attribut isEnabledwird auf true gesetzt, wenn der Benutzer dem Link für einen festgelegten Zeitraum folgt.

Betrachten Sie diesen Vorgang nun genauer.

Wir benötigen eine Tabelle zum Speichern des Tokens, um die Registrierung zu bestätigen:

 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; 

Und dementsprechend eine neue Entität für das objektrelationale Mapping ...:

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) } 


... und das Repository:

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> } 


Jetzt müssen wir Tools zum Verwalten von Tokens implementieren - Erstellen, Überprüfen und Senden per E-Mail. Dazu ändern wir, UserDetailsServiceImplindem wir Methoden zum Erstellen und Überprüfen des Tokens hinzufügen:

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 } } 


Fügen Sie nun eine Methode zum Senden einer E-Mail mit einem Bestätigungslink an folgende Adresse hinzu 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)} } 


Hinweis:

  • Ich würde empfehlen, die Host-URL in application.properties zu speichern
  • In unserem Link übergeben wir zwei GET-Parameter ( tokenund confirmRegistration) an die Adresse, an der die Anwendung bereitgestellt wird. Ich werde später erklären, warum.

Wir modifizieren den Registrierungscontroller wie folgt:

  • Alle neuen Benutzer legen den Wert falsefür das Feld festisEnabled
  • Nach dem Erstellen eines neuen Kontos senden wir eine E-Mail, um die Registrierung zu bestätigen
  • Erstellen Sie einen separaten Token-Validierungs-Controller
  • Wichtig: Während der Autorisierung prüfen wir, ob das Konto verifiziert ist:

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 } } 


Nun arbeiten wir am Frontend:

# 1 Erstellen Sie die Komponente RegistrationConfirmPage.vue

# 2 Fügen Sie einen neuen Pfad router.jsmit dem Parameter hinzu :token:

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

# 3 Update SignUp.vue- Nach erfolgreichem Versand der Daten aus den Formularen werden wir Sie darüber informieren, dass Sie dem Link im Brief folgen müssen, um die Registrierung abzuschließen.

# 4 Wichtig: Leider können wir keine feste Verknüpfung zu einer separaten Komponente herstellen, die das Token validiert und Erfolg oder Misserfolg meldet. Links mit Schrägstrichen führen uns auf die ursprüngliche Seite der Anwendung. Wir können unserer Anwendung jedoch mitteilen, dass die Registrierung mithilfe des übermittelten GET-Parameters bestätigt werden muss 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 Erstellen Sie eine Komponente, die das Token überprüft und das Ergebnis der Überprüfung meldet:

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


Ergebnis


Anstelle einer Schlussfolgerung


Abschließend möchte ich einen Exkurs machen und sagen, dass das Konzept der Anwendung, die in diesem und dem vorherigen Artikel erörtert wurde, zum Zeitpunkt des Schreibens nicht neu war. Aufgabe schnell einen vollen Stapel von Anwendungen , die auf Frühling Stiefel erstellen mit modernem JavaScript-Frameworks Angular / React / Vue.js elegant löst Hipster .

Die in diesem Artikel beschriebenen Ideen können jedoch auch mit JHipster umgesetzt werden. Daher hoffe ich, dass Leser, die an diesen Ort kommen, dieses Material selbst als Denkanstoß nützlich finden.


Nützliche Links


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


All Articles