Webanwendung auf Kotlin + Spring Boot + Vue.js

Guten Tag, liebe Einwohner von Habr!

Vor nicht allzu langer Zeit hatte ich die Möglichkeit, ein kleines Projekt ohne besondere technische Anforderungen umzusetzen. Das heißt, ich konnte den Technologie-Stack nach eigenem Ermessen auswählen. Deshalb habe ich die Gelegenheit nicht verpasst, die modischen, jugendlichen, vielversprechenden, aber mir in der Praxis unbekannten Kotlin und Vue.js zu "fühlen", den bereits bekannten Spring Boot hinzuzufügen und alles in einer einfachen Webanwendung auszuprobieren.

Als ich anfing, glaubte ich rücksichtslos, dass es im Internet viele Artikel und Handbücher zu diesem Thema geben würde. Die Materialien sind wirklich genug und sie sind alle gut, aber nur bis zum ersten REST-Controller. Dann beginnen die Schwierigkeiten des Widerspruchs. Aber selbst in einer einfachen Anwendung möchte ich eine komplexere Logik haben, als auf der Seite den vom Server zurückgegebenen Text zu zeichnen.

Nachdem ich es irgendwie geklärt hatte, beschloss ich, mein eigenes Handbuch zu schreiben, das hoffentlich für jemanden nützlich sein wird.

Was und für wen ist der Artikel


Dieses Material ist eine Anleitung für den „schnellen Start“ der Entwicklung einer Webanwendung mit einem Backend auf Kotlin + Spring Boot und einem Frontend auf Vue.js. Ich muss sofort sagen, dass ich für sie nicht „ertrinke“ und nicht über eindeutige Vorteile dieses Stapels spreche. Der Zweck dieses Artikels ist es, Erfahrungen auszutauschen.

Das Material richtet sich an Entwickler mit Erfahrung mit Java, Spring Framework / Spring Boot, React / Angular oder zumindest reinem JavaScript. Auch für diejenigen geeignet, die keine solche Erfahrung haben - zum Beispiel für unerfahrene Programmierer, aber ich fürchte, dann müssen Sie einige Details selbst herausfinden. Im Allgemeinen sollten einige Aspekte dieses Handbuchs genauer betrachtet werden, aber ich denke, es ist besser, dies in anderen Veröffentlichungen zu tun, um nicht viel vom Thema abzuweichen und den Artikel nicht umständlich zu machen.

Vielleicht hilft es jemandem, sich ein Bild von der Backend-Entwicklung auf Kotlin zu machen, ohne sich mit diesem Thema befassen zu müssen, und jemandem, die Arbeitszeit auf der Grundlage des vorgefertigten Anwendungsskeletts zu verkürzen.

Trotz der Beschreibung spezifischer praktischer Schritte hat der Artikel meiner Meinung nach im Allgemeinen einen experimentellen Überprüfungscharakter. Jetzt ist dieser Ansatz und die Frage selbst eher eine Hipster-Idee - so viele modische Wörter an einem Ort zu sammeln. Aber vielleicht wird es in Zukunft seine Nische in der Unternehmensentwicklung besetzen. Vielleicht gibt es unter uns Anfänger (und Fortgeschrittene), die in einer Zeit leben und arbeiten müssen, in der Kotlin und Vue.js genauso beliebt und gefragt sind wie Java und React. Immerhin haben Kotlin und Vue.js wirklich hohe Erwartungen.

Während der Zeit, in der ich diesen Leitfaden geschrieben habe, sind ähnliche Veröffentlichungen wie diese bereits im Netzwerk erschienen. Ich wiederhole, es gibt genügend Materialien, in denen die Reihenfolge der Aktionen für den ersten REST-Controller verstanden wird, aber es wäre interessant, eine komplexere Logik zu sehen - zum Beispiel die Implementierung der Authentifizierung mit Rollentrennung, was eine ziemlich notwendige Funktionalität ist. Das habe ich meiner eigenen Führung hinzugefügt.

Inhalt




Kurzanleitung


Kotlin ist eine Programmiersprache, die auf der JVM läuft und von der internationalen Firma JetBrains entwickelt wird .
Vue.js ist ein JavaScript- Framework für die Entwicklung von reaktiven Anwendungen für einzelne Seiten.


Entwicklungswerkzeuge


Als Entwicklungsumgebung würde ich die Verwendung von IntelliJ IDEA empfehlen, einer Entwicklungsumgebung von JetBrains , die in der Java-Community große Popularität erlangt hat, da sie über praktische Tools und Funktionen für die Arbeit mit Kotlin bis hin zur Konvertierung von Java-Code in Kotlin-Code verfügt. Sie sollten jedoch nicht erwarten, dass Sie auf diese Weise das gesamte Projekt migrieren können, und plötzlich funktioniert alles von selbst.

Zufriedene Besitzer der IntelliJ IDEA Ultimate Edition können das entsprechende Plugin installieren, um bequem mit Vue.js arbeiten zu können. Wenn Sie nach einem Kompromiss zwischen Werbegeschenkpreis und Komfort suchen, empfehle ich dringend, Microsoft Visual Code mit dem Vetur- Plugin zu verwenden.

Ich glaube, dass dies für viele offensichtlich ist, aber für alle Fälle erinnere ich Sie daran, dass der npm- Paketmanager für die Arbeit mit Vue.js erforderlich ist. Installationsanweisungen für Vue.js finden Sie auf der Vue CLI- Website.

Maven wird in diesem Handbuch als Java-Projektkollektor verwendet, PostgreSQL als Datenbankserver.


Projektinitialisierung


Erstellen Sie ein Projektverzeichnis mit Namen, z. B. kotlin-spring-vue . Unser Projekt wird zwei Module haben - Backend und Frontend . Zunächst wird das Frontend gesammelt. Während der Montage kopiert sich das Backend dann selbst index.html, favicon.ico und alle statischen Dateien (* .js, * .css, Bilder usw.).

Daher haben wir im Stammverzeichnis zwei Unterordner - / backend und / frontend . Beeilen Sie sich jedoch nicht, sie manuell zu erstellen.

Es gibt verschiedene Möglichkeiten, das Backend-Modul zu initialisieren:

  • manuell (Samurai-Pfad)
  • Spring Boot-Anwendungsprojekt, das mit Spring Tool Suite oder IntelliJ IDEA Ultimate Edition erstellt wurde
  • Verwenden des Spring Initializr , Festlegen der erforderlichen Einstellungen - dies ist möglicherweise der häufigste Weg

In unserem Fall lautet die primäre Konfiguration wie folgt:

Konfiguration des Backend-Moduls
  • Projekt: Maven-Projekt
  • Sprache: Kotlin
  • Spring Boot: 2.1.6
  • Projektmetadaten: Java 8, JAR-Verpackung
  • Abhängigkeiten: Spring Web Starter, Spring Boot Actuator, Spring Boot DevTools



pom.xml sollte folgendermaßen aussehen:

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

Achten Sie darauf:

  • Der Name der Hauptklasse endet in Kt
  • Kopieren von Ressourcen von project_root / frontend / target / dist nach src / main / resources / public
  • Das übergeordnete Projekt (übergeordnetes Projekt), das durch das übergeordnete Spring-Boot-Starter- Projekt dargestellt wird, wurde in die Hauptstufe pom.xml verschoben


Um das Frontend-Modul zu initialisieren, wechseln Sie in das Projektstammverzeichnis und führen Sie den folgenden Befehl aus:

 $ vue create frontend 

Dann können Sie alle Standardeinstellungen auswählen - in unserem Fall reicht dies aus.

Standardmäßig wird das Modul im Unterordner / dist gesammelt. Die gesammelten Dateien müssen jedoch im Ordner / target angezeigt werden. Erstellen Sie dazu die Datei vue.config.js direkt im / frontend mit den folgenden Einstellungen:

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

Fügen Sie die Datei pom.xml des folgenden Formulars in das Frontend- Modul ein:

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


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

Hier sehen wir unsere beiden Module - Frontend und Backend sowie Parent - Spring-Boot-Starter-Parent .

Wichtig: Die Module müssen in dieser Reihenfolge zusammengebaut werden - zuerst das Frontend, dann das Backend.

Jetzt können wir das Projekt erstellen:

 $ mvn install 

Und wenn alles zusammengebaut ist, führen Sie die Anwendung aus:

 $ mvn --projects backend spring-boot:run 

Die Standardseite Vue.js ist unter http: // localhost: 8080 / : verfügbar.




REST-API


Lassen Sie uns nun einen einfachen REST-Service erstellen. Zum Beispiel "Hallo, [Benutzername]!" (Die Standardeinstellung ist "Welt"). Dies zählt, wie oft wir es gezogen haben.
Dazu benötigen wir eine Datenstruktur, die aus einer Zahl und einer Zeichenfolge besteht - eine Klasse, deren einziger Zweck darin besteht, Daten zu speichern. Kotlin hat dafür Datenklassen . Und unsere Klasse wird so aussehen:

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

Das ist alles. Jetzt können wir den Service direkt schreiben.

Hinweis: Der Einfachheit halber werden alle Dienste mithilfe der Annotation @RequestMapping auf eine separate Route / API übertragen , bevor die Klasse deklariert wird:

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

Starten Sie nun die Anwendung neu und sehen Sie das Ergebnis http: // localhost: 8080 / api / greeting? Name = Vadim :

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

Wir werden die Seite aktualisieren und sicherstellen, dass der Zähler funktioniert:

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

Lassen Sie uns nun am Frontend arbeiten, um das Ergebnis wunderschön auf die Seite zu zeichnen.
Installieren Sie den Vue-Router , um die Navigation auf den "Seiten" (tatsächlich - auf den Routen und Komponenten, da wir nur eine Seite haben) in unserer Anwendung zu implementieren:

 $ npm install --save vue-router 

Fügen Sie router.js zu / src hinzu - diese Komponente ist für das Routing verantwortlich:

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


Hinweis: Die Root-Route ("/") steht uns als Komponente Greeting.vue zur Verfügung, die wir etwas später schreiben werden.

Jetzt werden wir unseren Router importieren. Nehmen Sie dazu Änderungen an vor
main.js
 import Vue from 'vue' import App from './App.vue' import router from './router' Vue.config.productionTip = false new Vue({ router, render: h => h(App), }).$mount('#app') 


Dann
App.vue
 <template> <div id="app"> <router-view></router-view> </div> </template> <script> export default { name: 'app' } </script> <style> </style> 


Verwenden Sie zum Ausführen von Serveranforderungen den AXIOS HTTP-Client:

 $ npm install --save axios 

Um nicht jedes Mal dieselben Einstellungen zu schreiben (z. B. lautet die Anforderungsroute "/ api"), empfehle ich, sie in die separate Komponente http-common.js einzufügen :

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

Hinweis: Um Warnungen bei der Ausgabe an die Konsole ( console.log () ) zu vermeiden, empfehle ich, diese Zeile in package.json zu schreiben :

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

Erstellen Sie nun endlich die Komponente (in / src / components )

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


Hinweis:

  • Abfrageparameter sind fest codiert, um zu sehen, wie die Methode funktioniert
  • Die Funktion zum Laden und Rendern von Daten ( loadGreeting() ) wird unmittelbar nach dem Laden der Seite aufgerufen ( loadGreeting() () )
  • Wir haben Axios bereits mit unseren benutzerdefinierten Einstellungen von http-common importiert



Datenbankverbindung


Betrachten wir nun den Prozess der Interaktion mit einer Datenbank am Beispiel von PostgreSQL und Spring Data .

Erstellen Sie zunächst eine Testplatte:

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

und fülle es mit Daten:

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

Ergänzen Sie die pom.xml des Backend-Moduls:
 <properties> ... <postgresql.version>42.2.5</postgresql.version> ... </properties> ... <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <version>${postgresql.version}</version> </dependency> ... <plugin> <groupId>org.jetbrains.kotlin</groupId> <artifactId>kotlin-maven-plugin</artifactId> <configuration> <args> <arg>-Xjsr305=strict</arg> </args> <compilerPlugins> <plugin>spring</plugin> <plugin>jpa</plugin> </compilerPlugins> </configuration> ... <dependency> <groupId>org.jetbrains.kotlin</groupId> <artifactId>kotlin-maven-noarg</artifactId> <version>${kotlin.version}</version> </dependency> 


Jetzt ergänzen wir die Datei application.properties des Backend-Moduls mit den Einstellungen für die Datenbankverbindung:

 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 

Hinweis: In diesem Formular beziehen sich die ersten drei Parameter auf Umgebungsvariablen. Ich empfehle dringend, vertrauliche Parameter über Umgebungsvariablen oder Startparameter zu übergeben. Wenn Sie jedoch sicher sind, dass sie nicht in die Hände heimtückischer Angreifer fallen, können Sie sie explizit fragen.

Erstellen wir eine Entität (Entitätsklasse) für eine objektrelationale Zuordnung:

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



Und ein CRUD-Repository für die Arbeit mit unserer Tabelle:

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

Hinweis: Wir verwenden die findAll() -Methode, die nicht neu definiert werden muss, sodass der Körper leer bleibt.

Und schließlich werden wir unseren Controller aktualisieren, um zu sehen, wie mit der Datenbank in Aktion gearbeitet wird:

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


Führen Sie die Anwendung aus und folgen Sie dem Link https: // localhost: 8080 / api / personen , um sicherzustellen, dass alles funktioniert:

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


Authentifizierung


Jetzt können wir zur Authentifizierung übergehen - auch eine der Grundfunktionen von Anwendungen, bei denen der Datenzugriff differenziert ist.

Erwägen Sie die Implementierung eines eigenen Autorisierungsservers mit JWT (JSON Web Token).

Warum nicht die Standardauthentifizierung?

  • Meiner Meinung nach erfüllt die Standardauthentifizierung die moderne Bedrohungsherausforderung selbst in einer relativ sicheren Nutzungsumgebung nicht.
  • Zu diesem Thema finden Sie viel mehr Material.

Warum nicht OAuth aus der Box Spring OAuth Security?
  • Weil OAuth mehr Zeug hat.
  • Dieser Ansatz kann durch äußere Umstände bestimmt werden: Kundenanforderungen, Laune des Architekten usw.
  • Wenn Sie ein unerfahrener Entwickler sind, ist es aus strategischer Sicht hilfreich, die Sicherheitsfunktionen genauer zu betrachten.

Backend


Zusätzlich zu den Gästen gibt es in unserer Anwendung zwei Benutzergruppen - normale Benutzer und Administratoren. Erstellen wir drei Tabellen: Benutzer - zum Speichern von Benutzerdaten, Rollen - zum Speichern von Informationen zu Rollen und Benutzerrollen - zum Verknüpfen der ersten beiden Tabellen.

Erstellen Sie Tabellen, fügen Sie Einschränkungen hinzu und füllen Sie die Rollentabelle
 CREATE TABLE public.users ( id serial NOT NULL, username character varying, first_name character varying, last_name character varying, email character varying, password character varying, enabled boolean, PRIMARY KEY (id) ); CREATE TABLE public.roles ( id serial NOT NULL, name character varying, PRIMARY KEY (id) ); CREATE TABLE public.users_roles ( id serial NOT NULL, user_id integer, role_id integer, PRIMARY KEY (id) ); ALTER TABLE public.users_roles ADD CONSTRAINT users_roles_users_fk FOREIGN KEY (user_id) REFERENCES public.users (id) MATCH SIMPLE ON UPDATE CASCADE ON DELETE CASCADE; ALTER TABLE public.users_roles ADD CONSTRAINT users_roles_roles_fk FOREIGN KEY (role_id) REFERENCES public.roles (id) MATCH SIMPLE ON UPDATE CASCADE ON DELETE CASCADE; INSERT INTO roles (name) VALUES ('ROLE_USER'), ('ROLE_ADMIN'); 


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

Hinweis: Die Benutzer- und Rollentabellen stehen in einer Viele-zu-Viele-Beziehung. Ein Benutzer kann mehrere Rollen haben (z. B. ein normaler Benutzer und ein Administrator), und mehreren Benutzern kann eine Rolle zugewiesen werden.

Zu berücksichtigende Informationen: Es gibt einen Ansatz, bei dem Benutzern separate Befugnisse (Behörden) zugewiesen werden, während eine Rolle eine Gruppe von Befugnissen impliziert. Weitere Informationen zum Unterschied zwischen Rollen und Berechtigungen finden Sie hier: Erteilte Berechtigung versus Rolle in Spring Security .

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


Erstellen Sie Repositorys für die Arbeit mit Tabellen:

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


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


Fügen Sie neue Abhängigkeiten hinzu
Backend-Modul pom.xml
 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>com.fasterxml.jackson.module</groupId> <artifactId>jackson-module-kotlin</artifactId> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.0</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.10.6</version> </dependency> 


Fügen Sie in application.properties neue Parameter für die Arbeit mit Token hinzu:
 assm.app.jwtSecret=jwtAssmSecretKey assm.app.jwtExpiration=86400 

Jetzt erstellen wir Klassen zum Speichern von Daten aus Autorisierungs- und Registrierungsformularen:

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


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


Lassen Sie uns spezielle Klassen für Serverantworten erstellen - Authentifizierungstoken und Universal (Zeichenfolge) zurückgeben:

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


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


Wir benötigen auch die Ausnahme "User Already Exists".
UserAlreadyExistException.kt
 class UserAlreadyExistException : RuntimeException { constructor() : super() {} constructor(message: String, cause: Throwable) : super(message, cause) {} constructor(message: String) : super(message) {} constructor(cause: Throwable) : super(cause) {} companion object { private val serialVersionUID = 5861310537366287163L } } 


Zum Definieren von Benutzerrollen benötigen wir einen zusätzlichen Dienst, der die UserDetailsService- Schnittstelle implementiert:

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


Um mit JWT arbeiten zu können, benötigen wir drei Klassen:
JwtAuthEntryPoint - zur Behandlung von Autorisierungsfehlern und zur weiteren Verwendung in Web-Sicherheitseinstellungen:

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


JwtProvider - um Token zu generieren und zu validieren sowie den Benutzer anhand seines Tokens zu bestimmen:

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


JwtAuthTokenFilter - um Benutzer zu authentifizieren und Anforderungen zu filtern:

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


Jetzt können wir die Bean konfigurieren, die für die Websicherheit verantwortlich ist:

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


:

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

:

  • signin — , , , , (, authorities — )
  • signup — , , , users ROLE_USER


, , BackendController : , ( ROLE_USER ROLE_ADMIN) (ROLE_USER).

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


Frontend


:

  • Zuhause
  • SignIn
  • SignUp
  • AdminPage
  • UserPage

( ):

 <template> <div> </div> </template> <script> </script> <style> </style> 


id=«_» div template export default {name: '[component_name]'} script .

:

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


Wir werden Vuex verwenden , um Token zu speichern und sie bei der Abfrage des Servers zu verwenden . Vuex ist ein Zustandsverwaltungsmuster + eine Vue.js.-Bibliothek Es dient als zentrales Data Warehouse für alle Anwendungskomponenten mit Regeln, um sicherzustellen, dass der Status nur auf vorhersehbare Weise geändert werden kann.

 $ npm install --save vuex 

Fügen Sie store als separate Datei zu src / store hinzu :

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

, :

  • store — , — , , ( — (authorities): — , , admin user
  • getters
  • mutations
  • actions — ,

: (mutations) — .

Wir werden die entsprechenden Änderungen an vornehmen

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


Damit die Oberfläche auch in einer experimentellen Anwendung, die ich verwende, sofort schön und ordentlich aussieht. Dies ist jedoch, wie sie sagen, Geschmackssache und hat keinen Einfluss auf die Grundfunktionalität:

 $ npm install --save bootstrap bootstrap-vue 

Bootstrap in main.js
 import BootstrapVue from 'bootstrap-vue' import 'bootstrap/dist/css/bootstrap.css' import 'bootstrap-vue/dist/bootstrap-vue.css' … Vue.use(BootstrapVue) 


Lassen Sie uns nun an der App-Komponente arbeiten:

  • Fügen Sie die Möglichkeit hinzu, sich für alle autorisierten Benutzer abzumelden
  • Fügen Sie nach dem Abmelden eine automatische Weiterleitung zur Startseite hinzu
  • Wir zeigen die Navigationsmenü-Schaltflächen "Benutzer" und "Abmelden" für alle autorisierten Benutzer und "Anmelden" für nicht autorisierte Benutzer
  • Die Schaltfläche "Admin" des Navigationsmenüs wird nur autorisierten Administratoren angezeigt

Dafür:

logout () -Methode hinzufügen
 methods: { logout() { this.$store.dispatch('logout'); this.$router.push('/') } } 


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

:

  • store , . , , («v-if»)
  • Kotlin, Spring Boot Vue.js, /assets/img/ . , ( )


Komponenten aktualisieren:

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


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

:

  • POST-
  • storage
  • «» Bootstrap
  • , /home


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

:

  • POST-
  • Bootstrap
  • , Bootstrap-


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

:
  • , storage


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

, UserPage .

Anwendungsstart


Wir werden unseren ersten Administrator registrieren:





Wichtig: Standardmäßig sind alle neuen Benutzer regulär. Geben wir dem ersten Administrator seine Autorität:

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

Dann:

  1. Melden wir uns als Administrator an
  2. Überprüfen Sie die Benutzerseite:

  3. Überprüfen Sie die Admin-Seite:

  4. Melden Sie sich vom Administratorkonto ab
  5. Registrieren Sie ein normales Benutzerkonto
  6. Überprüfen Sie die Verfügbarkeit der Benutzerseite
  7. Versuchen wir, Administratordaten mithilfe der REST-API abzurufen : http: // localhost: 8080 / api / admincontent

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


Möglichkeiten zur Verbesserung


Im Allgemeinen gibt es in jedem Unternehmen immer viele davon. Ich werde das offensichtlichste auflisten:

  • Verwenden Sie diese Option, um Gradle zu erstellen (wenn Sie dies als Verbesserung betrachten).
  • Decken Sie den Code sofort mit Unit-Tests ab (dies ist zweifellos eine gute Praxis).
  • Erstellen Sie von Anfang an eine CI / CD-Pipeline: Platzieren Sie den Code im Repository, enthalten Sie die Anwendung, automatisieren Sie die Montage und Bereitstellung
  • Hinzufügen von PUT- und DELETE-Anforderungen (z. B. Aktualisieren von Benutzerdaten und Löschen von Konten)
  • Implementieren Sie die Aktivierung / Deaktivierung des Kontos
  • Verwenden Sie keinen lokalen Speicher zum Speichern von Token - dies ist nicht sicher
  • Verwenden Sie OAuth
  • Überprüfen Sie die E-Mail-Adressen, wenn Sie einen neuen Benutzer registrieren
  • Verwenden Sie Spam-Schutz, z. B. reCAPTCHA


Nützliche Links




Ergänzung zu diesem Material hier.

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


All Articles