تطبيق ويب على Kotlin + Spring Boot + Vue.js

مساء الخير أيها السكان الأعزاء في هبر!

منذ وقت ليس ببعيد ، أتيحت لي الفرصة لتنفيذ مشروع صغير دون متطلبات فنية خاصة. وهذا هو ، لقد كنت حرًا في اختيار كومة التقنية وفقًا لتقديري. لهذا السبب لم يفوتني الفرصة "لأشعر" بالأناقة ، والشباب ، والواعدة ، ولكن غير مألوف بالنسبة لي Kotlin و Vue.js في الممارسة العملية ، مضيفًا إقلاع Spring المألوف بالفعل وتجربته جميعًا على تطبيق ويب مباشر.

عندما بدأت ، اعتقدت بتهور أنه سيكون هناك العديد من المقالات والأدلة حول هذا الموضوع على الإنترنت. المواد كافية حقًا ، وكلها جيدة ، ولكن فقط حتى أول وحدة تحكم REST. ثم تبدأ صعوبات التناقض. لكن حتى في تطبيق بسيط ، أود أن يكون لديّ منطق أكثر تعقيدًا من الرسم على الصفحة التي أرجعها الخادم على الصفحة.

بعد فرزها بطريقة ما ، قررت أن أكتب دليلي الخاص ، والذي آمل أن يكون مفيدًا لشخص ما.

ماذا ولمن هذه المادة


هذه المواد هي دليل على "البداية السريعة" لتطوير تطبيق ويب مع خلفية على Kotlin + Spring Boot وواجهة أمامية على Vue.js. يجب أن أقول على الفور أنني لا "غرق" بالنسبة لهم ولا أتحدث عن أي مزايا لا لبس فيها لهذه المجموعة. الغرض من هذه المقالة هو تبادل الخبرات.

تم تصميم المواد للمطورين ذوي الخبرة في Java أو Spring Framework / Spring Boot أو React / Angular أو JavaScript على الأقل. مناسبة لأولئك الذين ليس لديهم مثل هذه الخبرة - على سبيل المثال ، المبرمجين المبتدئين ، لكنني أخشى ، فسيتعين عليك معرفة بعض التفاصيل الخاصة بك. بشكل عام ، ينبغي النظر في بعض جوانب هذا الدليل بمزيد من التفصيل ، لكنني أعتقد أنه من الأفضل القيام بذلك في منشورات أخرى ، حتى لا تنحرف كثيرًا عن الموضوع ولا تجعل المقالة مرهقة.

ربما ستساعد شخصًا ما على تكوين فكرة عن تطوير الواجهة الخلفية على Kotlin دون الاضطرار إلى الغوص في هذا الموضوع ، وشخص آخر - لتقليل وقت العمل ، مع الأخذ في الاعتبار الهيكل الأساسي للتطبيق الجاهزة.

على الرغم من وصف خطوات عملية محددة ، بشكل عام ، في رأيي ، تحتوي المقالة على شخصية مراجعة تجريبية. الآن هذا النهج ، والسؤال نفسه مطروح ، من المرجح أن يكون فكرة محب - لجمع أكبر عدد ممكن من الكلمات العصرية في مكان واحد. ولكن في المستقبل ، ربما ، سوف تحتل مكانتها في تطوير المشاريع. ربما يوجد مبرمجون مبتدئين (ومستمرون) بيننا يتعين عليهم أن يعيشوا ويعملوا في الوقت الذي ستصبح فيه Kotlin و Vue.js شائعة ومطلوبة مثل Java و React الآن. بعد كل شيء ، Kotlin و Vue.js لديهم بالفعل توقعات كبيرة.

خلال الوقت الذي كتبت فيه هذا الدليل ، بدأت بالفعل منشورات مماثلة ، مثل هذا المنشور ، في الظهور على الشبكة. أكرر ، هناك مواد كافية يتم فيها فهم ترتيب الإجراءات على أول وحدة تحكم REST ، ولكن سيكون من المثير رؤية منطق أكثر تعقيدًا - على سبيل المثال ، تنفيذ المصادقة مع الفصل بين الأدوار ، وهي وظيفة ضرورية إلى حد ما. هذا ما أضفته إلى قيادتي الخاصة.

محتوى




مرجع سريع


Kotlin هي لغة برمجة تعمل على قمة JVM ويتم تطويرها بواسطة شركة JetBrains الدولية.
Vue.js هو إطار جافا سكريبت لتطوير تطبيقات نمط رد الفعل من صفحة واحدة.


أدوات التطوير


كبيئة تطوير ، أوصي باستخدام IntelliJ IDEA - بيئة التطوير من JetBrains ، التي اكتسبت شعبية واسعة في مجتمع Java ، لأنها تحتوي على أدوات وميزات مناسبة للعمل مع Kotlin حتى تحويل كود Java إلى كود Kotlin. ومع ذلك ، يجب ألا تتوقع أنه بهذه الطريقة يمكنك ترحيل المشروع بأكمله ، وفجأة سيعمل كل شيء بنفسه.

يمكن لمالكي IntelliJ IDEA Ultimate Edition السعداء تثبيت المكوّن الإضافي المناسب لراحة العمل مع Vue.js. إذا كنت تبحث عن حل وسط بين سعر الهدية الترويجية والراحة ، فإنني أوصي بشدة باستخدام Microsoft Visual Code مع البرنامج المساعد Vetur .

أفترض أن هذا واضح للكثيرين ، لكن في حالة حدوث ذلك ، أذكركم بأن مدير حزمة npm مطلوب للعمل مع Vue.js. يمكن العثور على تعليمات التثبيت الخاصة بـ Vue.js على موقع Vue CLI الإلكتروني.

يتم استخدام Maven كمجمع لمشروع Java في هذا الدليل ، ويستخدم PostgreSQL كخادم قاعدة بيانات.


تهيئة المشروع


قم بإنشاء دليل مشروع بالاسم ، على سبيل المثال ، kotlin-spring-vue . سيتضمن مشروعنا وحدتين - الواجهة الخلفية والواجهة الأمامية . أولاً ، سيتم جمع الواجهة الأمامية. بعد ذلك ، أثناء التجميع ، ستنسخ الواجهة الخلفية index.html و favicon.ico وجميع الملفات الثابتة (* .js ، * .css ، الصور ، إلخ).

وبالتالي ، في الدليل الجذر ، سيكون لدينا مجلدان فرعيان - / backend و / frontend . ومع ذلك ، لا تتعجل في إنشائها يدويًا.

هناك عدة طرق لتهيئة الوحدة الخلفية:

  • يدويا (مسار الساموراي)
  • تم إنشاء مشروع تطبيق Spring Boot باستخدام Spring Tool Suite أو IntelliJ IDEA Ultimate Edition
  • باستخدام Spring Initializr ، وتحديد الإعدادات الضرورية - ربما تكون هذه هي الطريقة الأكثر شيوعًا

في حالتنا ، التكوين الأساسي هو كما يلي:

تكوين وحدة الواجهة الخلفية
  • المشروع: مشروع مخضرم
  • اللغة: كوتلين
  • الحذاء الربيعي: 2.1.6
  • بيانات المشروع الوصفية: Java 8، JAR packaging
  • التبعيات: Spring Web Starter ، و Spring Boot Actuator ، و Spring Boot DevTools



يجب أن يبدو pom.xml كالتالي:

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

انتبه:

  • ينتهي اسم الفئة الرئيسية بالكيلو
  • نسخ الموارد من project_root / frontend / target / dist إلى src / main / resources / public
  • انتقل المشروع الرئيسي (الأصل) ممثلاً بنابض بدء تشغيل الأصل إلى مستوى pom.xml الرئيسي


لتهيئة وحدة الواجهة الأمامية ، انتقل إلى دليل جذر المشروع وقم بتنفيذ الأمر:

 $ vue create frontend 

ثم يمكنك تحديد جميع الإعدادات الافتراضية - في حالتنا هذا سيكون كافياً.

بشكل افتراضي ، سيتم جمع الوحدة في المجلد الفرعي / dist ، ومع ذلك نحتاج إلى رؤية الملفات التي تم جمعها في المجلد / target. للقيام بذلك ، قم بإنشاء ملف vue.config.js مباشرة في الواجهة الأمامية بالإعدادات التالية:

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

ضع ملف pom.xml في النموذج التالي في الوحدة النمطية للواجهة الأمامية :

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


وأخيرًا ، ضع pom.xml في الدليل الجذر للمشروع:
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> 

حيث نرى وحدتين لدينا - الواجهة الأمامية والخلفية ، وكذلك الوالدين - الربيع التمهيد كاتب الأصل .

هام: يجب تجميع الوحدات النمطية بهذا الترتيب - أولاً الواجهة الأمامية ، ثم الواجهة الخلفية.

الآن يمكننا بناء المشروع:

 $ mvn install 

وإذا تم تجميع كل شيء ، قم بتشغيل التطبيق:

 $ mvn --projects backend spring-boot:run 

ستكون صفحة Vue.js الافتراضية متاحة على الموقع http: // localhost: 8080 / :




REST API


الآن لنقم بإنشاء خدمة REST بسيطة. على سبيل المثال ، "مرحبًا ، [اسم المستخدم]!" (الافتراضي هو العالم) ، الذي يحسب عدد المرات التي سحبناها.
للقيام بذلك ، نحتاج إلى بنية بيانات تتكون من رقم وسلسلة - فئة هدفها الوحيد هو تخزين البيانات. Kotlin لديها فئات البيانات لهذا الغرض . وسوف يبدو صفنا هكذا:

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

هذا كل شيء. الآن يمكننا كتابة الخدمة مباشرة.

ملاحظة: للراحة ، ستأخذ جميع الخدمات إلى مسار / api منفصل باستخدام التعليق التوضيحي RequestMapping قبل إعلان الفئة:

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

أعد تشغيل التطبيق الآن وشاهد النتيجة http: // localhost: 8080 / api / greeting؟ Name = Vadim :

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

سنقوم بتحديث الصفحة والتأكد من عمل العداد:

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

الآن دعونا نعمل على الواجهة الأمامية لرسم النتيجة بشكل جميل على الصفحة.
قم بتثبيت vue-router من أجل تطبيق التنقل على "الصفحات" (في الواقع - على الطرق والمكونات ، نظرًا لأن لدينا صفحة واحدة فقط) في تطبيقنا:

 $ npm install --save vue-router 

أضف router.js إلى / src - سيكون هذا المكون مسؤولاً عن التوجيه:

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


ملاحظة: سيكون مسار الجذر ("/") متاحًا لنا المكون Greeting.vue ، والذي سنكتبه لاحقًا.

الآن سوف نستورد جهاز التوجيه الخاص بنا. للقيام بذلك ، قم بإجراء تغييرات على
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') 


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


لتنفيذ طلبات الخادم ، استخدم عميل AXIOS HTTP:

 $ npm install --save axios 

من أجل عدم كتابة نفس الإعدادات في كل مرة (على سبيل المثال ، مسار الطلب هو "/ api") في كل مكون ، أوصي بوضعها في مكون http-common.js المنفصل :

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

ملاحظة: لتجنب التحذيرات عند الإخراج إلى وحدة التحكم ( console.log () ) ، أوصي بكتابة هذا السطر في package.json :

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

الآن ، أخيرًا ، قم بإنشاء المكون (في / 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> 


ملاحظة:

  • معلمات الاستعلام هي hardcoded لنرى كيف تعمل الطريقة
  • يتم استدعاء وظيفة تحميل وتقديم البيانات ( loadGreeting() ) مباشرة بعد تحميل الصفحة ( محمّلة () )
  • قمنا باستيراد axios بالفعل من خلال إعداداتنا المخصصة من http-common



اتصال قاعدة البيانات


الآن دعونا نلقي نظرة على عملية التفاعل مع قاعدة بيانات باستخدام مثال PostgreSQL و Spring Data .

أولاً ، قم بإنشاء لوحة اختبار:

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

وملء البيانات:

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

تكملة pom.xml من وحدة الواجهة الخلفية:
 <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> 


الآن سنكمل ملف application.properties الخاص بوحدة الواجهة الخلفية مع إعدادات اتصال قاعدة البيانات:

 spring.datasource.url=${SPRING_DATASOURCE_URL} spring.datasource.username=${SPRING_DATASOURCE_USERNAME} spring.datasource.password=${SPRING_DATASOURCE_PASSWORD} spring.jpa.generate-ddl=true spring.jpa.properties.hibernate.temp.use_jdbc_metadata_defaults = false spring.jpa.database-platform=org.hibernate.dialect.PostgreSQL9Dialect spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true 

ملاحظة: في هذا النموذج ، تشير المعلمات الثلاثة الأولى إلى متغيرات البيئة. أوصي بشدة بتمرير المعلمات الحساسة من خلال متغيرات البيئة أو معلمات بدء التشغيل. ولكن ، إذا كنت متأكدًا من أنهم لن يقعوا في أيدي المهاجمين الغدرين ، فيمكنك أن تطلب منهم صراحة.

لنقم بإنشاء كيان (فئة كيان) لتعيين الكائنات العلائقية:

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 ) 



ومستودع CRUD للعمل مع طاولتنا:

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

ملاحظة: سوف نستخدم طريقة findAll() ، والتي لا تحتاج إلى إعادة تعريف ، لذلك سنترك الجسم فارغًا.

وأخيرًا ، سنقوم بتحديث وحدة التحكم الخاصة بنا لمعرفة كيفية العمل مع قاعدة البيانات في العمل:

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


قم بتشغيل التطبيق ، اتبع الرابط https: // localhost: 8080 / api / الأشخاص للتأكد من أن كل شيء يعمل:

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


المصادقة


يمكننا الآن الانتقال إلى المصادقة - وهي أيضًا إحدى الوظائف الأساسية للتطبيقات حيث يكون الوصول إلى البيانات متباينًا.

النظر في تنفيذ خادم التخويل الخاص بك باستخدام JWT (JSON Web Token).

لماذا لا المصادقة الأساسية؟

  • في رأيي ، لا تفي المصادقة الأساسية بتحدي التهديد الحديث حتى في بيئة الاستخدام الآمن نسبيًا.
  • يمكنك العثور على المزيد من المواد حول هذا الموضوع.

لماذا لا تخرج OAuth من أمان Spring OAuth؟
  • لأن OAuth لديه المزيد من الأشياء.
  • يمكن إملاء هذا النهج وفقًا للظروف الخارجية: متطلبات العميل ، نزوة المهندس المعماري ، إلخ.
  • إذا كنت مطورًا مبتدئًا ، فمن المنظور الاستراتيجي سيكون من المفيد البحث بعمق مع وظيفة الأمان بمزيد من التفاصيل.

الخلفية


بالإضافة إلى الضيوف ، سيكون هناك مجموعتان من المستخدمين في تطبيقنا - المستخدمين العاديين والمسؤولين. لنقم بإنشاء ثلاثة جداول: المستخدمون - لتخزين بيانات المستخدم ، والأدوار - لتخزين المعلومات حول الأدوار و user_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'); 


لنقم بإنشاء فئات الكيان:
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 ) 

ملاحظة: ترتبط جداول المستخدمين والأدوار بعدة أطراف ، حيث يمكن أن يكون لدى مستخدم واحد عدة أدوار (على سبيل المثال ، مستخدم عادي ومسؤول) ، ويمكن تعيين عدة مستخدمين لدور واحد.

المعلومات الواجب مراعاتها: هناك طريقة يتم من خلالها منح المستخدمين صلاحيات (سلطات) منفصلة ، في حين أن الدور ينطوي على مجموعة من الصلاحيات. يمكنك قراءة المزيد حول الفرق بين الأدوار والأذونات هنا: السلطة الممنوحة مقابل الدور في أمان الربيع .

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 ) 


إنشاء مستودعات للعمل مع الجداول:

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 } 


إضافة تبعيات جديدة ل
وحدة الواجهة الخلفية 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> 


وإضافة معلمات جديدة للعمل مع الرموز في application.properties :
 assm.app.jwtSecret=jwtAssmSecretKey assm.app.jwtExpiration=86400 

سنقوم الآن بإنشاء فصول لتخزين البيانات الواردة من نماذج الترخيص والتسجيل:

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


دعنا نجعل فئات خاصة لاستجابات الخادم - إرجاع رمز المصادقة العالمي (سلسلة):

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


سنحتاج أيضًا إلى استثناء "المستخدم موجود بالفعل".
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 } } 


لتحديد أدوار المستخدم ، نحتاج إلى خدمة إضافية تنفذ واجهة 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() } } 


للعمل مع JWT ، نحتاج إلى ثلاثة فصول:
JwtAuthEntryPoint - لمعالجة أخطاء التخويل والاستخدام الإضافي في إعدادات أمان الويب:

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 - لإنشاء الرموز والتحقق من صحتها ، وكذلك تحديد المستخدم بواسطة رمزه المميز:

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 - لمصادقة المستخدمين وتصفية الطلبات:

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


الآن يمكننا تهيئة الفول المسؤول عن أمان الويب:

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


:

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

:

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


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

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


الواجهة


:

  • منزل
  • SignIn
  • SignUp
  • AdminPage
  • UserPage

( ):

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


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

:

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


Vuex . Vuex — + Vue.js. , , .

 $ npm install --save vuex 

store 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) — .



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


, . , , , :

 $ npm install --save bootstrap bootstrap-vue 

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


App:

  • «»
  • (logout)
  • «User» «Logout» «Login» —
  • «Admin»

:

logout()
 methods: { logout() { this.$store.dispatch('logout'); this.$router.push('/') } } 


(template)
 <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/ . , ( )


:

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 .


:





: — . :

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

:

  1. User:

  2. Admin:

  3. User
  4. , REST API: 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 



, . :

  • Gradle ( )
  • ( , , )
  • CI/CD Pipeline: , ,
  • PUT DELETE (, )
  • /
  • local storage —
  • OAuth
  • , , reCAPTCHA


روابط مفيدة




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


All Articles