Aplicación web en Kotlin + Spring Boot + Vue.js

¡Buenas tardes, queridos habitantes de Habr!

No hace mucho tiempo, tuve la oportunidad de implementar un pequeño proyecto sin requisitos técnicos especiales. Es decir, tenía la libertad de elegir la pila de tecnología a mi discreción. Es por eso que no perdí la oportunidad de "sentir" a Kotlin y Vue.js a la moda, juveniles, prometedores pero desconocidos para mí en la práctica, agregando el Spring Boot ya familiar y probándolo todo en una aplicación web sin complicaciones.

Cuando comencé, creí imprudentemente que habría muchos artículos y manuales sobre el tema en Internet. Los materiales son realmente suficientes, y todos son buenos, pero solo hasta el primer controlador REST. Entonces comienzan las dificultades de contradicción. Pero incluso en una aplicación simple, me gustaría tener una lógica más compleja que dibujar en la página el texto devuelto por el servidor.

Después de resolverlo de alguna manera, decidí escribir mi propio manual, que espero sea de utilidad para alguien.

Qué y para quién es el artículo


Este material es una guía para el "inicio rápido" del desarrollo de una aplicación web con un backend en Kotlin + Spring Boot y una interfaz en Vue.js. Debo decir de inmediato que no me estoy "ahogando" por ellos y no estoy hablando de ninguna ventaja inequívoca de esta pila. El propósito de este artículo es compartir experiencias.

El material está diseñado para desarrolladores con experiencia en Java, Spring Framework / Spring Boot, React / Angular, o al menos JavaScript puro. Adecuado para aquellos que no tienen esa experiencia, por ejemplo, programadores novatos, pero me temo que tendrás que descubrir algunos detalles por tu cuenta. En general, algunos aspectos de esta guía deben considerarse con más detalle, pero creo que es mejor hacerlo en otras publicaciones, para no desviarse mucho del tema y no hacer que el artículo sea engorroso.

Tal vez ayude a alguien a formarse una idea del desarrollo del backend en Kotlin sin la necesidad de profundizar en este tema, y ​​a alguien, para reducir el tiempo dedicado a un esqueleto de aplicación ya preparado.

A pesar de la descripción de pasos prácticos específicos, en general, en mi opinión, el artículo tiene un carácter de revisión experimental. Ahora, este enfoque, y se ve la pregunta en sí, es más probable como una idea inconformista: recopilar tantas palabras de moda en un solo lugar. Pero en el futuro, tal vez, ocupará su nicho en el desarrollo empresarial. Quizás hay programadores principiantes (y continuos) entre nosotros que tienen que vivir y trabajar en un momento en que Kotlin y Vue.js serán tan populares y demandados como lo son ahora Java y React. Después de todo, Kotlin y Vue.js realmente tienen altas expectativas.

Durante el tiempo que escribí esta guía, publicaciones similares, como esta, ya han comenzado a aparecer en la red. Repito, hay suficientes materiales donde se entiende el orden de las acciones para el primer controlador REST, pero sería interesante ver una lógica más compleja, por ejemplo, la implementación de autenticación con separación por roles, que es una funcionalidad bastante necesaria. Eso es lo que agregué a mi propio liderazgo.

Contenido




Referencia rápida


Kotlin es un lenguaje de programación que se ejecuta sobre JVM y está desarrollado por la compañía internacional JetBrains .
Vue.js es un marco de JavaScript para desarrollar aplicaciones de estilo reactivo de una sola página.


Herramientas de desarrollo


Como entorno de desarrollo, recomendaría usar IntelliJ IDEA , el entorno de desarrollo de JetBrains , que ha ganado una gran popularidad en la comunidad Java, porque tiene herramientas y funciones convenientes para trabajar con Kotlin hasta convertir el código Java en código Kotlin. Sin embargo, no debe esperar que de esta manera pueda migrar todo el proyecto, y de repente todo funcionará solo.

Los propietarios felices de IntelliJ IDEA Ultimate Edition pueden instalar el complemento apropiado para la conveniencia de trabajar con Vue.js. Si está buscando un compromiso entre el precio gratuito y la conveniencia, le recomiendo usar Microsoft Visual Code con el complemento Vetur .

Supongo que esto es obvio para muchos, pero por si acaso, les recuerdo que el administrador de paquetes npm debe trabajar con Vue.js. Las instrucciones de instalación para Vue.js se pueden encontrar en el sitio web de Vue CLI .

Maven se usa como el recopilador de proyectos Java en esta guía, PostgreSQL se usa como el servidor de base de datos.


Inicializacion del proyecto


Cree un directorio de proyecto por nombre, por ejemplo, kotlin-spring-vue . Nuestro proyecto tendrá dos módulos: backend y frontend . Primero, se recogerá la interfaz. Luego, durante el ensamblaje, el backend se copiará index.html, favicon.ico y todos los archivos estáticos (* .js, * .css, imágenes, etc.).

Por lo tanto, en el directorio raíz tendremos dos subcarpetas: / backend y / frontend . Sin embargo, no se apresure a crearlos manualmente.

Hay varias formas de inicializar el módulo de fondo:

  • manualmente (ruta samurai)
  • Proyecto de aplicación Spring Boot generado con Spring Tool Suite o IntelliJ IDEA Ultimate Edition
  • Usando Spring Initializr , especificando las configuraciones necesarias, esta es quizás la forma más común

En nuestro caso, la configuración primaria es la siguiente:

Configuración del módulo de fondo
  • Proyecto: Proyecto Maven
  • Idioma: Kotlin
  • Spring Boot: 2.1.6
  • Metadatos del proyecto: Java 8, empaquetado JAR
  • Dependencias: Spring Web Starter, Spring Boot Actuator, Spring Boot DevTools



pom.xml debería verse así:

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> 

Presta atención:

  • El nombre de la clase principal termina en Kt
  • Copiar recursos de project_root / frontend / target / dist a src / main / resources / public
  • El proyecto padre (padre) representado por spring-boot-starter-parent se movió al nivel principal pom.xml


Para inicializar el módulo frontend, vaya al directorio raíz del proyecto y ejecute el comando:

 $ vue create frontend 

Luego puede seleccionar todas las configuraciones predeterminadas; en nuestro caso, esto será suficiente.

De manera predeterminada, el módulo se recopilará en la subcarpeta / dist , sin embargo, necesitamos ver los archivos recopilados en la carpeta / target. Para hacer esto, cree el archivo vue.config.js directamente en / frontend con la siguiente configuración:

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

Coloque el archivo pom.xml del siguiente formulario en el módulo de interfaz :

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> 


Y finalmente, coloque pom.xml en el directorio raíz del proyecto:
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> 

donde vemos nuestros dos módulos - frontend y backend , y también parent - spring-boot-starter-parent .

Importante: los módulos deben ensamblarse en este orden: primero el frontend, luego el backend.

Ahora podemos construir el proyecto:

 $ mvn install 

Y, si todo está ensamblado, ejecute la aplicación:

 $ mvn --projects backend spring-boot:run 

La página predeterminada de Vue.js estará disponible en http: // localhost: 8080 / :




API REST


Ahora creemos un servicio REST simple. Por ejemplo, "¡Hola, [nombre de usuario]!" (el valor predeterminado es Mundo), que cuenta cuántas veces lo sacamos.
Para hacer esto, necesitamos una estructura de datos que consista en un número y una cadena, una clase cuyo único propósito es almacenar datos. Kotlin tiene clases de datos para esto . Y nuestra clase se verá así:

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

Eso es todo. Ahora podemos escribir el servicio directamente.

Nota: por conveniencia, llevará todos los servicios a una ruta / api separada utilizando la anotación @RequestMapping antes de declarar la clase:

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

Ahora reinicie la aplicación y vea el resultado http: // localhost: 8080 / api / greeting? Name = Vadim :

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

Actualizaremos la página y nos aseguraremos de que el contador funcione:

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

Ahora trabajemos en la interfaz para dibujar bellamente el resultado en la página.
Instale vue-router para implementar la navegación en las "páginas" (de hecho, en las rutas y componentes, ya que solo tenemos una página) en nuestra aplicación:

 $ npm install --save vue-router 

Agregue router.js a / src : este componente será responsable del enrutamiento:

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


Nota: la ruta raíz ("/") estará disponible para nosotros, el componente Greeting.vue, que escribiremos un poco más adelante.

Ahora importaremos nuestro enrutador. Para hacer esto, realice cambios en
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') 


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


Para ejecutar solicitudes del servidor, use el cliente HTTP AXIOS:

 $ npm install --save axios 

Para no escribir la misma configuración cada vez (por ejemplo, la ruta de solicitud es "/ api") en cada componente, recomiendo ponerlos en el componente http-common.js separado :

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

Nota: para evitar advertencias al enviar a la consola ( console.log () ), recomiendo escribir esta línea en package.json :

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

Ahora, finalmente, cree el componente (en / 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> 


Nota:

  • Los parámetros de consulta están codificados para ver cómo funciona el método
  • La función de cargar y representar datos ( loadGreeting() ) se llama inmediatamente después de cargar la página ( montado () )
  • ya importamos axios con nuestra configuración personalizada de http-common



Conexión de base de datos


Ahora veamos el proceso de interacción con una base de datos usando el ejemplo de PostgreSQL y Spring Data .

Primero, cree una placa de prueba:

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

y llenarlo con datos:

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

Complemente el pom.xml del módulo de fondo:
 <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> 


Ahora complementaremos el archivo application.properties del módulo de fondo con la configuración de conexión de la base de datos:

 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 

Nota: en este formulario, los primeros tres parámetros se refieren a variables de entorno. Recomiendo pasar parámetros sensibles a través de variables de entorno o parámetros de inicio. Pero, si está seguro de que no caerán en manos de atacantes insidiosos, puede preguntarles explícitamente.

Creemos una entidad (entidad-clase) para un mapeo relacional de objetos:

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 ) 



Y un repositorio CRUD para trabajar con nuestra tabla:

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

Nota: Utilizaremos el método findAll() , que no necesita ser redefinido, por lo que dejaremos el cuerpo vacío.

Y finalmente, actualizaremos nuestro controlador para ver cómo trabajar con la base de datos en acción:

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


Ejecute la aplicación, siga el enlace https: // localhost: 8080 / api / persons para asegurarse de que todo funcione:

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


Autenticación


Ahora podemos pasar a la autenticación, también una de las funciones básicas de las aplicaciones en las que se diferencia el acceso a los datos.

Considere implementar su propio servidor de autorización utilizando JWT (JSON Web Token).

¿Por qué no la autenticación básica?

  • En mi opinión, la autenticación básica no cumple con el desafío de la amenaza moderna, incluso en un entorno de uso relativamente seguro.
  • Puedes encontrar mucho más material sobre este tema.

¿Por qué no OAuth fuera de la caja de Spring OAuth Security?
  • Porque OAuth tiene más cosas.
  • Este enfoque puede ser dictado por circunstancias externas: requisitos del cliente, capricho del arquitecto, etc.
  • Si es un desarrollador novato, entonces, desde una perspectiva estratégica, será útil profundizar más en la funcionalidad de seguridad con más detalle.

Backend


Además de los invitados, habrá dos grupos de usuarios en nuestra aplicación: usuarios comunes y administradores. Creemos tres tablas: usuarios - para almacenar datos de usuario, roles - para almacenar información sobre roles y users_roles - para vincular las dos primeras tablas.

Cree tablas, agregue restricciones y complete la tabla de roles
 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'); 


Creemos clases de entidad:
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 ) 

Nota: las tablas de usuarios y roles están en una relación de muchos a muchos: un usuario puede tener varios roles (por ejemplo, un usuario normal y un administrador), y a varios usuarios se les puede asignar un rol.

Información a tener en cuenta: existe un enfoque en el que los usuarios reciben poderes separados (autoridades), mientras que un rol implica un grupo de poderes. Puede leer más sobre la diferencia entre roles y permisos aquí: Autoridad otorgada versus rol en 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 ) 


Crear repositorios para trabajar con tablas:

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 } 


Agregar nuevas dependencias a
módulo 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> 


Y agregue nuevos parámetros para trabajar con tokens en application.properties :
 assm.app.jwtSecret=jwtAssmSecretKey assm.app.jwtExpiration=86400 

Ahora crearemos clases para almacenar datos provenientes de formularios de autorización y registro:

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


Hagamos clases especiales para las respuestas del servidor, devolviendo el token de autenticación y universal (cadena):

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


También necesitaremos la excepción "El usuario ya existe".
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 } } 


Para definir roles de usuario, necesitamos un servicio adicional que implemente la interfaz 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() } } 


Para trabajar con JWT, necesitamos tres clases:
JwtAuthEntryPoint : para manejar errores de autorización y uso posterior en configuraciones de seguridad 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 : para generar y validar tokens, así como determinar el usuario por su token:

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 : para autenticar usuarios y filtrar solicitudes:

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


Ahora podemos configurar el bean responsable de la seguridad 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) } } 


Cree un controlador para el registro y la autorización:

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


Y, por último, complementamos el BackendController con dos métodos: uno devolverá datos a los que solo puede acceder el administrador (un usuario con privilegios ROLE_USER y ROLE_ADMIN) y un usuario común (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


Creemos algunos componentes nuevos:

  • Inicio
  • Registrarse
  • Registrarse
  • Adminpage
  • Página de usuario

Con contenido de plantilla (para un conveniente inicio de copiar y pegar ):

Plantilla de componentes
 <template> <div> </div> </template> <script> </script> <style> </style> 


Agregue id = " nombre_componente " a cada div dentro de la plantilla y exporte {nombre: '[nombre_componente]'} predeterminado en el script .

Ahora agregue nuevas rutas:

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


Usaremos Vuex para almacenar tokens y usarlos al consultar el servidor . Vuex es un patrón de gestión de estado + biblioteca Vue.js. Sirve como un almacén de datos centralizado para todos los componentes de la aplicación con reglas para garantizar que el estado solo se pueda cambiar de una manera predecible.

 $ npm install --save vuex 

Agregue la tienda como un archivo separado a 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 }); 

, :

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

: (mutations) — .

Haremos los cambios apropiados a

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


Para que la interfaz se vea hermosa y ordenada de inmediato, incluso en una aplicación experimental que uso. Pero esto, como dicen, es cuestión de gustos y no afecta la funcionalidad básica:

 $ npm install --save bootstrap bootstrap-vue 

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


Ahora trabajemos en el componente de la aplicación:

  • Agregue la capacidad de "cerrar sesión" para todos los usuarios autorizados
  • Agregue una redirección automática a la página de inicio después de cerrar sesión
  • Mostraremos los botones del menú de navegación "Usuario" y "Cerrar sesión" para todos los usuarios autorizados y "Iniciar sesión" para usuarios no autorizados.
  • Mostraremos el botón "Admin" del menú de navegación solo a administradores autorizados

Para hacer esto:

agregar el método logout ()
 methods: { logout() { this.$store.dispatch('logout'); this.$router.push('/') } } 


y edite la plantilla
 <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/ . , ( )


Actualizar componentes:

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 .

Lanzamiento de la aplicación


Registraremos a nuestro primer administrador:





Importante: de forma predeterminada, todos los usuarios nuevos son regulares. Demos al primer administrador su autoridad:

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

Entonces:

  1. Iniciemos sesión como administrador
  2. Consulte la página del usuario:

  3. Consulta la página de administración:

  4. Cerrar sesión en la cuenta de administrador
  5. Registrar una cuenta de usuario ordinaria
  6. Verifique la disponibilidad de la página de usuario
  7. Intentemos obtener datos de administrador utilizando la 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 


Formas de mejorar


En términos generales, siempre hay muchos de ellos en cualquier negocio. Voy a enumerar los más obvios:

  • Úselo para construir Gradle (si considera que esto es una mejora)
  • Cubra inmediatamente el código con pruebas unitarias (esto es, sin duda, una buena práctica)
  • Desde el principio, cree la canalización de CI / CD: coloque el código en el repositorio, contenga la aplicación, automatice el ensamblaje y la implementación
  • Agregar solicitudes PUT y DELETE (por ejemplo, actualizar datos de usuario y eliminar cuentas)
  • Implementar activación / desactivación de cuenta
  • No utilice el almacenamiento local para almacenar el token: no es seguro
  • Use OAuth
  • Verifique las direcciones de correo electrónico cuando registre un nuevo usuario
  • Use protección contra correo no deseado, por ejemplo, reCAPTCHA


Enlaces utiles




Además de este material aquí.

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


All Articles