Bonjour, chers habitants de Habr!
Il n'y a pas si longtemps, j'ai eu l'opportunité de réaliser un petit projet sans exigences techniques particulières. Autrement dit, j'étais libre de choisir la pile technologique à ma discrétion. C’est pourquoi je n’ai pas manqué l’occasion de «sentir» le
Kotlin et
Vue.js à la
mode, jeune, prometteur, mais que je ne connais pas dans la pratique, en ajoutant le
Spring Boot déjà familier et en essayant le tout sur une application Web simple.
Quand j'ai commencé, je croyais imprudemment qu'il y aurait de nombreux articles et manuels sur le sujet sur Internet. Les matériaux sont vraiment suffisants, et ils sont tous bons, mais seulement jusqu'au premier contrôleur REST. Les difficultés de la contradiction commencent alors. Mais même dans une application simple, j'aimerais avoir une logique plus complexe que de dessiner sur la page le texte renvoyé par le serveur.
Après l'avoir trié, j'ai décidé d'écrire mon propre manuel, qui, je l'espère, sera utile à quelqu'un.
Quoi et pour qui est l'article
Ce matériel est un guide pour le «démarrage rapide» du développement d'une application web avec un backend sur
Kotlin +
Spring Boot et un frontend sur
Vue.js. Je dois dire tout de suite que je ne me «noie pas» pour eux et ne parle pas des avantages sans équivoque de cette pile. Le but de cet article est de partager l'expérience.
Le matériel est conçu pour les développeurs ayant une expérience avec Java, Spring Framework / Spring Boot, React / Angular, ou au moins JavaScript pur. Convient à ceux qui n'ont pas une telle expérience - par exemple, les programmeurs novices, mais, je le crains, vous devrez alors trouver vous-même certains détails. En général, certains aspects de ce guide devraient être examinés plus en détail, mais je pense qu'il vaut mieux le faire dans d'autres publications, afin de ne pas trop s'écarter du sujet et de ne pas alourdir l'article.
Peut-être que cela aidera quelqu'un à se faire une idée du développement du backend sur Kotlin sans avoir à plonger dans ce sujet, et quelqu'un d'autre - pour réduire le temps passé sur la base d'un squelette d'application déjà préparé.
Malgré la description des étapes pratiques spécifiques, en général, à mon avis, l'article a un caractère de revue expérimentale. Maintenant, cette approche, et la question elle-même est vue, est plus susceptible d'être une idée hipster - de rassembler autant de mots à la mode en un seul endroit. Mais à l'avenir, il occupera peut-être sa niche dans le développement des entreprises. Il y a peut-être des programmeurs débutants (et continus) parmi nous qui doivent vivre et travailler à un moment où Kotlin et Vue.js seront aussi populaires et demandés que Java et React le sont maintenant. Après tout, Kotlin et Vue.js ont vraiment des attentes élevées.
Pendant la période où j'ai rédigé ce guide, des publications similaires, comme celle-ci, ont déjà commencé à apparaître sur le réseau. Je répète, il y a suffisamment de matériaux où l'ordre des actions vers le premier contrôleur REST est compris, mais il serait intéressant de voir une logique plus complexe - par exemple, la mise en œuvre de l'authentification avec séparation par rôles, qui est une fonctionnalité plutôt nécessaire. C'est ce que j'ai ajouté à ma propre direction.
Table des matières
Référence rapide
Kotlin est un langage de programmation qui s'exécute sur la
JVM et est développé par la société internationale
JetBrains .
Vue.js est un framework
JavaScript pour développer des applications de style réactif sur une seule page.
Outils de développement
En tant qu'environnement de développement, je recommanderais d'utiliser
IntelliJ IDEA - l'environnement de développement de
JetBrains , qui a gagné en popularité dans la communauté Java, car il dispose d'outils et de fonctionnalités pratiques pour travailler avec Kotlin jusqu'à la conversion de code Java en code Kotlin. Cependant, vous ne devez pas vous attendre à ce que vous puissiez migrer l'intégralité du projet, et soudain, tout fonctionnera de lui-même.
Les heureux propriétaires d'
IntelliJ IDEA Ultimate Edition peuvent installer le
plug-in approprié pour la commodité de travailler avec Vue.js. Si vous recherchez un compromis entre le prix de la
gratification et la commodité, je recommande fortement d'utiliser
Microsoft Visual Code avec le plugin
Vetur .
Je suppose que cela est évident pour beaucoup, mais juste au cas où, je vous rappelle que le
gestionnaire de paquets
npm est requis pour fonctionner avec Vue.js. Les instructions d'installation de Vue.js se trouvent sur le site Web
Vue CLI .
Maven est utilisé comme collecteur de projets Java dans ce guide,
PostgreSQL est utilisé comme serveur de base de données.
Initialisation du projet
Créez un répertoire de projet par nom, par exemple,
kotlin-spring-vue . Notre projet comprendra deux modules -
backend et
frontend . Tout d'abord, l'interface sera collectée. Ensuite, lors de l'assemblage, le backend se copiera lui-même index.html, favicon.ico et tous les fichiers statiques (* .js, * .css, images, etc.).
Ainsi, dans le répertoire racine, nous aurons deux sous-dossiers -
/ backend et
/ frontend . Cependant, ne vous précipitez pas pour les créer manuellement.
Il existe plusieurs façons d'initialiser le module principal:
- manuellement (chemin samouraï)
- Projet d'application Spring Boot généré à l'aide de Spring Tool Suite ou IntelliJ IDEA Ultimate Edition
- Utilisation de Spring Initializr , spécification des paramètres nécessaires - c'est peut-être le moyen le plus courant
Dans notre cas, la configuration principale est la suivante:
Configuration du module backend- Projet: Maven Project
- Langue: Kotlin
- Spring Boot: 2.1.6
- Métadonnées du projet: Java 8, empaquetage JAR
- Dépendances: Spring Web Starter, Spring Boot Actuator, Spring Boot DevTools

pom.xml devrait ressembler à ceci:
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>
Faites attention:
- Le nom de la classe principale se termine par Kt
- Copie des ressources de project_root / frontend / target / dist vers src / main / resources / public
- Projet parent (parent) représenté par spring-boot-starter-parent déplacé vers le niveau principal pom.xml
Pour initialiser le module frontal, accédez au répertoire racine du projet et exécutez la commande:
$ vue create frontend
Ensuite, vous pouvez sélectionner tous les paramètres par défaut - dans notre cas, cela suffira.
Par défaut, le module sera collecté dans le sous-dossier
/ dist , cependant nous devons voir les fichiers collectés dans le dossier / target. Pour ce faire, créez le fichier
vue.config.js directement dans
/ frontend avec les paramètres suivants:
module.exports = { outputDir: 'target/dist', assetsDir: 'static' }
Placez le fichier
pom.xml du formulaire
suivant dans le module
frontal :
pom.xml - frontend <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <artifactId>frontend</artifactId> <parent> <groupId>com.kotlin-spring-vue</groupId> <artifactId>demo</artifactId> <version>0.0.1-SNAPSHOT</version> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> <frontend-maven-plugin.version>1.6</frontend-maven-plugin.version> </properties> <build> <plugins> <plugin> <groupId>com.github.eirslett</groupId> <artifactId>frontend-maven-plugin</artifactId> <version>${frontend-maven-plugin.version}</version> <executions> <execution> <id>install node and npm</id> <goals> <goal>install-node-and-npm</goal> </goals> <configuration> <nodeVersion>v11.8.0</nodeVersion> </configuration> </execution> <execution> <id>npm install</id> <goals> <goal>npm</goal> </goals> <phase>generate-resources</phase> <configuration> <arguments>install</arguments> </configuration> </execution> <execution> <id>npm run build</id> <goals> <goal>npm</goal> </goals> <configuration> <arguments>run build</arguments> </configuration> </execution> </executions> </plugin> </plugins> </build> </project>
Et enfin, mettez
pom.xml dans le répertoire racine du projet:
pom.xml <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.kotlin-spring-vue</groupId> <artifactId>demo</artifactId> <packaging>pom</packaging> <version>0.0.1-SNAPSHOT</version> <name>kotlin-spring-vue</name> <description>Kotlin + Spring Boot + Vue.js</description> <modules> <module>frontend</module> <module>backend</module> </modules> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.3.RELEASE</version> <relativePath/> </parent> <properties> <main.basedir>${project.basedir}</main.basedir> </properties> <build> <plugins> <plugin> <groupId>org.jacoco</groupId> <artifactId>jacoco-maven-plugin</artifactId> <executions> <execution> <id>pre-unit-test</id> <goals> <goal>prepare-agent</goal> </goals> </execution> <execution> <id>post-unit-test</id> <phase>test</phase> <goals> <goal>report</goal> </goals> </execution> </executions> </plugin> <plugin> <groupId>org.eluder.coveralls</groupId> <artifactId>coveralls-maven-plugin</artifactId> </plugin> <plugin> <groupId>org.jetbrains.kotlin</groupId> <artifactId>kotlin-maven-plugin</artifactId> <version>${kotlin.version}</version> <executions> <execution> <id>compile</id> <phase>compile</phase> <goals> <goal>compile</goal> </goals> </execution> <execution> <id>test-compile</id> <phase>test-compile</phase> <goals> <goal>test-compile</goal> </goals> </execution> </executions> <configuration> <jvmTarget>1.8</jvmTarget> </configuration> </plugin> </plugins> </build> </project>
où nous voyons nos deux modules -
frontend et
backend , et aussi parent -
spring-boot-starter-parent .
Important: les modules doivent être assemblés dans cet ordre - d'abord le frontend, puis le backend.
Maintenant, nous pouvons construire le projet:
$ mvn install
Et, si tout est assemblé, lancez l'application:
$ mvn --projects backend spring-boot:run
La page Vue.js par défaut sera disponible sur
http: // localhost: 8080 / :

API REST
Créons maintenant un service REST simple. Par exemple, «Bonjour, [nom d'utilisateur]!» (la valeur par défaut est World), qui compte le nombre de fois où nous l'avons tiré.
Pour cela, nous avons besoin d'une structure de données composée d'un nombre et d'une chaîne - une classe dont le seul but est de stocker des données. Kotlin a
des classes de données pour cela . Et notre classe ressemblera à ceci:
data class Greeting(val id: Long, val content: String)
C’est tout. Maintenant, nous pouvons écrire le service directement.
Remarque: pour plus de commodité, tous les services seront
acheminés vers une route
/ API distincte à l'aide de l'annotation
@RequestMapping avant de déclarer la classe:
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") }
Maintenant, redémarrez l'application et voyez le résultat
http: // localhost: 8080 / api / salutation? Name = Vadim :
{"id":1,"content":"Hello, Vadim"}
Nous rafraîchirons la page et nous assurerons que le compteur fonctionne:
{"id":2,"content":"Hello, Vadim"}
Maintenant, travaillons sur le frontend pour dessiner magnifiquement le résultat sur la page.
Installez
vue-router afin d'implémenter la navigation sur les «pages» (en fait - sur les routes et les composants, puisque nous n'avons qu'une seule page) dans notre application:
$ npm install --save vue-router
Ajoutez
router.js à
/ src - ce composant sera responsable du routage:
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 } ] })
Remarque: la route racine ("/") sera disponible pour nous le composant Greeting.vue, que nous écrirons un peu plus tard.
Maintenant, nous allons importer notre routeur. Pour ce faire, modifiez
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')
Alors
App.vue <template> <div id="app"> <router-view></router-view> </div> </template> <script> export default { name: 'app' } </script> <style> </style>
Pour exécuter les demandes du serveur, utilisez le client HTTP AXIOS:
$ npm install --save axios
Afin de ne pas écrire les mêmes paramètres à chaque fois (par exemple, la route de demande est "/ api") dans chaque composant, je recommande de les mettre dans
le composant http-common.js distinct :
import axios from 'axios' export const AXIOS = axios.create({ baseURL: `/api` })
Remarque: pour éviter les avertissements lors de la sortie vers la console (
console.log () ), je recommande d'écrire cette ligne dans
package.json :
"rules": { "no-console": "off" }
Maintenant, enfin, créez le composant (dans
/ 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>
Remarque:- Les paramètres de requête sont codés en dur pour voir comment fonctionne la méthode
- La fonction de chargement et de rendu des données (
loadGreeting()
) est appelée immédiatement après le chargement de la page ( monté () ) - nous avons déjà importé des axios avec nos paramètres personnalisés depuis http-common

Connexion à la base de données
Voyons maintenant le processus d'interaction avec une base de données en utilisant l'exemple de
PostgreSQL et
Spring Data .
Créez d'abord une plaque d'essai:
CREATE TABLE public."person" ( id serial NOT NULL, name character varying, PRIMARY KEY (id) );
et remplissez-le de données:
INSERT INTO person (name) VALUES ('John'), ('Griselda'), ('Bobby');
Complétez le pom.xml du module backend: <properties> ... <postgresql.version>42.2.5</postgresql.version> ... </properties> ... <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <version>${postgresql.version}</version> </dependency> ... <plugin> <groupId>org.jetbrains.kotlin</groupId> <artifactId>kotlin-maven-plugin</artifactId> <configuration> <args> <arg>-Xjsr305=strict</arg> </args> <compilerPlugins> <plugin>spring</plugin> <plugin>jpa</plugin> </compilerPlugins> </configuration> ... <dependency> <groupId>org.jetbrains.kotlin</groupId> <artifactId>kotlin-maven-noarg</artifactId> <version>${kotlin.version}</version> </dependency>
Nous allons maintenant compléter le fichier
application.properties du module backend avec les paramètres de connexion à la base de données:
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
Remarque: sous cette forme, les trois premiers paramètres font référence aux variables d'environnement. Je recommande fortement de passer des paramètres sensibles via des variables d'environnement ou des paramètres de démarrage. Mais, si vous êtes sûr qu'ils ne tomberont pas entre les mains d'attaquants insidieux, alors vous pouvez leur demander explicitement.
Créons une entité (entité-classe) pour un mappage relationnel objet:
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 )
Et un référentiel CRUD pour travailler avec notre table:
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> {}
Remarque: Nous utiliserons la méthode
findAll()
, qui n'a pas besoin d'être redéfinie, nous laisserons donc le corps vide.
Et enfin, nous mettrons à jour notre contrôleur pour voir comment travailler avec la base de données en action:
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()
Exécutez l'application, suivez le lien
https: // localhost: 8080 / api / persons pour vous assurer que tout fonctionne:
[{"id":1,"name":"John"},{"id":2,"name":"Griselda"},{"id":3,"name":"Bobby"}]
Authentification
Nous pouvons maintenant passer à l'authentification - également l'une des fonctions de base des applications où l'accès aux données est différencié.
Envisagez d'implémenter votre propre serveur d'autorisation à l'aide de
JWT (JSON Web Token).
Pourquoi pas l'authentification de base?- À mon avis, l'authentification de base ne répond pas au défi des menaces modernes, même dans un environnement d'utilisation relativement sûr.
- Vous pouvez trouver beaucoup plus d'informations sur ce sujet.
Pourquoi ne pas sortir OAuth de la sécurité Spring OAuth?- Parce que OAuth a plus de choses.
- Cette approche peut être dictée par des circonstances externes: exigences du client, caprice de l'architecte, etc.
- Si vous êtes un développeur novice, dans une perspective stratégique, il sera utile de creuser plus en détail la fonctionnalité de sécurité.
Backend
En plus des invités, il y aura deux groupes d'utilisateurs dans notre application - les utilisateurs ordinaires et les administrateurs. Créons trois tables:
utilisateurs - pour stocker les données utilisateur,
rôles - pour stocker des informations sur les rôles et
utilisateurs_rôles - pour lier les deux premières tables.
Créer des tables, ajouter des contraintes et remplir la table des rôles 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');
Créons des classes d'entité:
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 )
Remarque: les tables d'
utilisateurs et de
rôles sont dans une relation plusieurs-à-plusieurs - un utilisateur peut avoir plusieurs rôles (par exemple, un utilisateur ordinaire et un administrateur), et plusieurs utilisateurs peuvent se voir attribuer un rôle.
Informations à prendre en considération: selon une approche, les utilisateurs se voient attribuer des pouvoirs (autorités) distincts, tandis qu'un rôle implique un groupe de pouvoirs. Vous pouvez en savoir plus sur la différence entre les rôles et les autorisations ici:
Autorité accordée par rapport au rôle dans 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 )
Créez des référentiels pour travailler avec des tables:
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 }
Ajouter de nouvelles dépendances à
module backend pom.xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>com.fasterxml.jackson.module</groupId> <artifactId>jackson-module-kotlin</artifactId> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.0</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.10.6</version> </dependency>
Et ajoutez de nouveaux paramètres pour travailler avec des jetons dans
application.properties :
assm.app.jwtSecret=jwtAssmSecretKey assm.app.jwtExpiration=86400
Nous allons maintenant créer des classes pour stocker les données provenant des formulaires d'autorisation et d'enregistrement:
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 } }
Créons des classes spéciales pour les réponses du serveur - renvoyant le jeton d'authentification et universel (chaîne):
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?)
Nous aurons également besoin de l'exception «L'utilisateur existe déjà».
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 } }
Pour définir les rôles d'utilisateur, nous avons besoin d'un service supplémentaire qui implémente l'interface
UserDetailsService :
UserDetailsServiceImpl.kt import com.kotlinspringvue.backend.repository.UserRepository import org.springframework.beans.factory.annotation.Autowired import org.springframework.security.core.userdetails.UserDetails import org.springframework.security.core.userdetails.UserDetailsService import org.springframework.security.core.userdetails.UsernameNotFoundException import org.springframework.stereotype.Service import org.springframework.security.core.GrantedAuthority import org.springframework.security.core.authority.SimpleGrantedAuthority import java.util.stream.Collectors @Service class UserDetailsServiceImpl: UserDetailsService { @Autowired lateinit var userRepository: UserRepository @Throws(UsernameNotFoundException::class) override fun loadUserByUsername(username: String): UserDetails { val user = userRepository.findByUsername(username).get() ?: throw UsernameNotFoundException("User '$username' not found") val authorities: List<GrantedAuthority> = user.roles!!.stream().map({ role -> SimpleGrantedAuthority(role.name)}).collect(Collectors.toList<GrantedAuthority>()) return org.springframework.security.core.userdetails.User .withUsername(username) .password(user.password) .authorities(authorities) .accountExpired(false) .accountLocked(false) .credentialsExpired(false) .disabled(false) .build() } }
Pour travailler avec JWT, nous avons besoin de trois classes:
JwtAuthEntryPoint - pour gérer les erreurs d'autorisation et pour une utilisation ultérieure dans les paramètres de sécurité Web:
JwtAuthEntryPoint.kt import javax.servlet.ServletException import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletResponse import org.slf4j.Logger import org.slf4j.LoggerFactory import org.springframework.security.core.AuthenticationException import org.springframework.security.web.AuthenticationEntryPoint import org.springframework.stereotype.Component @Component class JwtAuthEntryPoint : AuthenticationEntryPoint { @Throws(IOException::class, ServletException::class) override fun commence(request: HttpServletRequest, response: HttpServletResponse, e: AuthenticationException) { logger.error("Unauthorized error. Message - {}", e!!.message) response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid credentials") } companion object { private val logger = LoggerFactory.getLogger(JwtAuthEntryPoint::class.java) } }
JwtProvider - pour générer et valider des jetons, ainsi que déterminer l'utilisateur par son jeton:
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 - pour authentifier les utilisateurs et filtrer les demandes:
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) } }
Nous pouvons maintenant configurer le bean responsable de la sécurité Web:
WebSecurityConfig.kt import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.beans.factory.annotation.Autowired import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter import org.springframework.security.config.http.SessionCreationPolicy import org.springframework.security.authentication.AuthenticationManager import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder import com.kotlinspringvue.backend.jwt.JwtAuthEntryPoint import com.kotlinspringvue.backend.jwt.JwtAuthTokenFilter import com.kotlinspringvue.backend.service.UserDetailsServiceImpl @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) class WebSecurityConfig : WebSecurityConfigurerAdapter() { @Autowired internal var userDetailsService: UserDetailsServiceImpl? = null @Autowired private val unauthorizedHandler: JwtAuthEntryPoint? = null @Bean fun bCryptPasswordEncoder(): BCryptPasswordEncoder { return BCryptPasswordEncoder() } @Bean fun authenticationJwtTokenFilter(): JwtAuthTokenFilter { return JwtAuthTokenFilter() } @Throws(Exception::class) override fun configure(authenticationManagerBuilder: AuthenticationManagerBuilder) { authenticationManagerBuilder .userDetailsService(userDetailsService) .passwordEncoder(bCryptPasswordEncoder()) } @Bean @Throws(Exception::class) override fun authenticationManagerBean(): AuthenticationManager { return super.authenticationManagerBean() } @Throws(Exception::class) override protected fun configure(http: HttpSecurity) { http.csrf().disable().authorizeRequests() .antMatchers("/**").permitAll() .anyRequest().authenticated() .and() .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter::class.java) } }
Créez un contrôleur pour l'enregistrement et l'autorisation: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) }
Nous avons mis en œuvre deux méthodes:- signin - vérifie si l'utilisateur existe et, dans l'affirmative, renvoie le jeton généré, le nom d'utilisateur et ses rôles (ou plutôt, autorités - autorisations)
- inscription - vérifie si l'utilisateur existe et, sinon, crée un nouvel enregistrement dans la table des utilisateurs avec un lien externe vers le rôle ROLE_USER
Et enfin, nous complétons le BackendController avec deux méthodes: l'une retournera des données accessibles uniquement à l'administrateur (un utilisateur avec les privilèges ROLE_USER et ROLE_ADMIN) et un utilisateur ordinaire (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
Créons de nouveaux composants:- Accueil
- Connexion
- Inscription
- Page d'administration
- Page utilisateur
Avec le contenu du modèle (pour un démarrage pratique du copier-coller ):Modèle de composant <template> <div> </div> </template> <script> </script> <style> </style>
Ajoutez id = " nom_composant " à chaque div à l'intérieur du modèle et exportez {nom: "[nom_composant") par défaut} dans le script .Ajoutez maintenant de nouveaux itinéraires: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 } ] })
Nous utiliserons Vuex pour stocker des jetons et les utiliser lors de l'interrogation du serveur . Vuex est un modèle de gestion d'état + bibliothèque Vue.js. Il sert d'entrepôt de données centralisé pour tous les composants d'application avec des règles pour garantir que l'état ne peut être modifié que de manière prévisible. $ npm install --save vuex
Ajoutez un magasin en tant que fichier distinct à src / store :index.js import Vue from 'vue'; import Vuex from 'vuex'; Vue.use(Vuex); const state = { token: localStorage.getItem('user-token') || '', role: localStorage.getItem('user-role') || '', username: localStorage.getItem('user-name') || '', authorities: localStorage.getItem('authorities') || '', }; const getters = { isAuthenticated: state => { if (state.token != null && state.token != '') { return true; } else { return false; } }, isAdmin: state => { if (state.role === 'admin') { return true; } else { return false; } }, getUsername: state => { return state.username; }, getAuthorities: state => { return state.authorities; }, getToken: state => { return state.token; } }; const mutations = { auth_login: (state, user) => { localStorage.setItem('user-token', user.token); localStorage.setItem('user-name', user.name); localStorage.setItem('user-authorities', user.roles); state.token = user.token; state.username = user.username; state.authorities = user.roles; var isUser = false; var isAdmin = false; for (var i = 0; i < user.roles.length; i++) { if (user.roles[i].authority === 'ROLE_USER') { isUser = true; } else if (user.roles[i].authority === 'ROLE_ADMIN') { isAdmin = true; } } if (isUser) { localStorage.setItem('user-role', 'user'); state.role = 'user'; } if (isAdmin) { localStorage.setItem('user-role', 'admin'); state.role = 'admin'; } }, auth_logout: () => { state.token = ''; state.role = ''; state.username = ''; state.authorities = []; localStorage.removeItem('user-token'); localStorage.removeItem('user-role'); localStorage.removeItem('user-name'); localStorage.removeItem('user-authorities'); } }; const actions = { login: (context, user) => { context.commit('auth_login', user) }, logout: (context) => { context.commit('auth_logout'); } }; export const store = new Vuex.Store({ state, getters, mutations, actions });
Voyons ce que nous avons ici:- store — , — , , ( — (authorities): — , , admin user —
- getters —
- mutations —
- actions — ,
: (mutations) — .
Nous apporterons les modifications appropriées àmain.js import { store } from './store'; ... new Vue({ router, store, render: h => h(App) }).$mount('#app')
Afin de rendre l'interface immédiatement belle et soignée, même dans une application expérimentale que j'utilise. Mais cela, comme on dit, est une question de goût et n'affecte pas les fonctionnalités de base: $ npm install --save bootstrap bootstrap-vue
Bootstrap dans main.js import BootstrapVue from 'bootstrap-vue' import 'bootstrap/dist/css/bootstrap.css' import 'bootstrap-vue/dist/bootstrap-vue.css' … Vue.use(BootstrapVue)
Maintenant, travaillons sur le composant App:- Ajouter la possibilité de "se déconnecter" pour tous les utilisateurs autorisés
- Ajouter une redirection automatique vers la page d'accueil après la déconnexion
- Nous afficherons les boutons du menu de navigation «Utilisateur» et «Déconnexion» pour tous les utilisateurs autorisés et «Connexion» pour les utilisateurs non autorisés
- Nous afficherons le bouton «Admin» du menu de navigation uniquement aux administrateurs autorisés
Pour ce faire:ajouter la méthode logout () methods: { logout() { this.$store.dispatch('logout'); this.$router.push('/') } }
et éditez le modèle <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>
Remarque:- store , . , , («v-if»)
- Kotlin, Spring Boot Vue.js, /assets/img/ . , ( )
Mettre à jour les composants: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>
:
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 .
Lancement de l'application
Nous enregistrerons notre premier administrateur:
Important: par défaut, tous les nouveaux utilisateurs sont réguliers. Donnons au premier administrateur son autorité: INSERT INTO users_roles (user_id, role_id) VALUES (1, 2);
Ensuite:- Connectons-nous en tant qu'administrateur
- Consultez la page utilisateur:

- Consultez la page d'administration:

- Déconnectez-vous du compte administrateur
- Enregistrer un compte utilisateur ordinaire
- Vérifier la disponibilité de la page Utilisateur
- Essayons d'obtenir des données d'administration à l'aide de l'API REST: 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
Comment s'améliorer
D'une manière générale, il y en a toujours beaucoup dans toute entreprise. Je vais énumérer les plus évidents:- Utilisez pour construire Gradle (si vous considérez cela comme une amélioration)
- Couvrir immédiatement le code avec des tests unitaires (c'est, sans aucun doute, une bonne pratique)
- Dès le début, construisez CI / CD Pipeline: placez le code dans le référentiel, contenez l'application, automatisez l'assemblage et le déploiement
- Ajouter des demandes PUT et DELETE (par exemple, mettre à jour les données utilisateur et supprimer des comptes)
- Mettre en œuvre l'activation / la désactivation du compte
- N'utilisez pas le stockage local pour stocker le jeton - ce n'est pas sûr
- Utilisez OAuth
- Vérifier les adresses e-mail lors de l'enregistrement d'un nouvel utilisateur
- Utilisez la protection anti-spam, par exemple reCAPTCHA
Liens utiles
Ajout à ce matériel ici.