Now we have enough knowledge
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 users_roles:
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');
<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>
assm.app.jwtSecret=jwtAssmSecretKey
assm.app.jwtExpiration=86400
import org.springframework.security.core.GrantedAuthority
class JwtResponse(var accessToken: String?, var username: String?, val authorities:
JwtAuthEntryPoint:
JwtProvider:
JwtAuthTokenFilter:
We have allowed access to all routes (#56) - the access control will be implemented specifically for every route.
We have definŠµd two methods, accessible in /api/auth root route:
Let's add some modifications to our BackendController - create two methods:
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"
}
<template>
<div>
</div>
</template>
<script>
</script>
<style>
</style>
$ npm install --save vuex
import { store } from './store';
...
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
$ npm install --save bootstrap bootstrap-vue
Import Bootstrap in main.js:
import BootstrapVue from 'bootstrap-vue'
import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap-vue/dist/bootstrap-vue.css'
…
Vue.use(BootstrapVue)
methods: {
logout() {
this.$store.dispatch('logout');
this.$router.push('/')
}
}
Update the template - add the navigation bar:
<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>
ATTENTION! Add ROLE_ADMIN role to admin user before login:
INSERT INTO users_roles (user_id, role_id) VALUES (1, 2);
Check:
SELECT *
FROM users
INNER JOIN users_roles ON users.id = users_roles.user_id
INNER JOIN roles ON users_roles.role_id = roles.id;
Then:
#1 Login using admin credentials
#2 Check out User page
to implement critical important feature of any enterprise(and not only) application - authentication. We will use JWT authentication method.
JSON Web Token (JWT) is an Internet standard for creating JSON-based access tokens that assert some number of claims. For example, a server could generate a token that has the claim "logged in as admin" and provide that to a client. The client could then use that token to prove that it is logged in as admin. The tokens are signed by one party's private key (usually the server's), so that both parties (the other already being, by some suitable and trustworthy means, in possession of the corresponding public key) are able to verify that the token is legitimate. The tokens are designed to be compact, URL-safe, and usable especially in a web-browser single-sign-on (SSO) context. JWT claims can be typically used to pass identity of authenticated users between an identity provider and a service provider, or any other type of claims as required by business processes.
JWT relies on other JSON-based standards: JSON Web Signature and JSON Web Encryption.
Wikipedia
Objectives
- Ability to register
- Ability to authorize
- Role-based access to data
- Role-based acmes to pages
- Recognize user by request
- Different navbars for authorized and non-authorized users
- Different navbars for admins and regular users
Why JWT?
- Why JWT not Basic Authentication?
- Basic authentication does not meet the current threat level and is too vulnerable
- You can find much more info about Basic Authentication implementation in the Internet
- Why JWT not OAuth?
- Sometimes you may need to implement custom authentication logic, and it's more convenient to do that using JWT
- You can find much more info about OAuth Authentication implementation in the Internet
- If you understand how to implement JWT authentication, implementing OAuth authentication will not cause you any difficulties
Backend
Database
Create table users:
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 roles:
CREATE TABLE public.roles
(
id serial NOT NULL,
name character varying,
PRIMARY KEY (id)
);
Create table users_roles:
CREATE TABLE public.users_roles
(
id serial NOT NULL,
user_id integer,
role_id integer,
PRIMARY KEY (id)
);
Add constraints:
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;
Add roles entries into roles:
INSERT INTO roles (name) VALUES ('ROLE_USER'), ('ROLE_ADMIN');
We don't add users entries right now. We will register them using our application.
JPA
Entities
Create two classes inside the jpa package:
User:
Role:
Repositories
Create two classes inside the repository package:
UsersRepository:
RolesRepository:
pom.xml
Add the following dependencies to your backend module:
<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
Models
We need to create special classes to process data coming from the frontend: for authorizing the existing users and for registering the new users. Let's create package model and these models:
LoginUser:
NewUser:
LoginUser:
NewUser:
Responses
Create a package web.response and two small classes in there:
JwtResponse:
import org.springframework.security.core.GrantedAuthority
class JwtResponse(var accessToken: String?, var username: String?, val authorities:
Collection<GrantedAuthority>) {
var type = "Bearer"
}
class ResponseMessage(var message: String?)
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
}
}
We need information about user roles to differentiate access to data and services. So, let's create package service and UserDetailsServiceImpl - the implementation of UserDetailsService Spring interface:var type = "Bearer"
}
ResponseMessage:
class ResponseMessage(var message: String?)
We also need an error response "User already exists", so create it in web.error package:
UserAlreadyExistException:
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
}
}
User Service
JWT
Create package jwt with three following classes:
- JwtAuthEntryPoint - to handle authorization errors and use it in web security config
- JwtProvider - to generate and validate tokens and also to get user's data by token provided
- JwtAuthTokenFilter - to authenticate users and filter the requests
JwtAuthEntryPoint:
JwtProvider:
JwtAuthTokenFilter:
Security Configuration
Create package config and add WebSecurityConfig bean:
Controllers
Let's create new controller called AuthController:
- signin - we check if user exists and if so, we return the generated token, username and his authorities (roles)
- signup - we check if user doesn't exist and if so, we create new record in users table with ROLE_USER role
- Return data available for regular users (ROLE_USERS) and admins (ROLE_ADMIN)
- Return data available for admins only (ROLE_ADMIN)
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"
}
Build project to ensure everything is OK. That's all for backend.
Frontend
Create new components:
- Home
- SignIn
- SignUp
- UserPage
- AdminPage
<template>
<div>
</div>
</template>
<script>
</script>
<style>
</style>
Add id="component_name" to every <div> inside the <template> and export default {name: ‘[component_name]’} to <script>.
Having so many pages now we need to use router for describing and managing routes: add file router.js to /src:
Having so many pages now we need to use router for describing and managing routes: add file router.js to /src:
Vuex
Vuex is a state management pattern + library for Vue.js applications. It serves as a centralized store for all the components in an application, with rules ensuring that the state can only be mutated in a predictable fashion. It also integrates with Vue's official devtools extension to provide advanced features such as zero-config time-travel debugging and state snapshot export / import.
We will use Vuex for storing and using authentication token in our requests.
Execute command to install Vuex and add it to your project:
$ npm install --save vuex
Store
Add package src/store and file index.js:
This small module allows us to manage the state using the store:
NOTE: Mutations is the only correct way to change the state
- store - the goal data we need to use across the components
- getters - functions to define specific aspects of state
- mutations - functions to mutate the state
- actions - functions to commit the mutations, they can contain asynchronous operations
NOTE: Mutations is the only correct way to change the state
main.js
Add some changes to main.js:
import { store } from './store';
...
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
Bootstrap
Install Bootstrap:
$ npm install --save bootstrap bootstrap-vue
Import Bootstrap in main.js:
import BootstrapVue from 'bootstrap-vue'
import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap-vue/dist/bootstrap-vue.css'
…
Vue.use(BootstrapVue)
App.vue
- Logout function should be available from everywhere
- After logout user should be redirected to the start page
- We should show "Logout" button and "User" tab if user is authenticated and "Login" button if is not
- We should show "Admin" tab for authenticated admins only
methods: {
logout() {
this.$store.dispatch('logout');
this.$router.push('/')
}
}
Update the template - add the navigation bar:
<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>
Home.vue
Here we should show "Login" button if user is not authenticated
SignIn.vue
- Send login request to server
- Get token from server and save it to storage
- Show "beautiful" error using Bootstrap (<b-alert>)
- If logging in is successful, redirect to /home
SignUp.vue
- Send SignUp request to server
- Show "beautiful" error using Bootstrap (<b-alert>)
- Validate input fields
UserPage.vue
Here we get user/admin content from server
AdminPage.vue
Here we get admin only content
Login and Start
ATTENTION! Add ROLE_ADMIN role to admin user before login:
Check:
SELECT *
FROM users
INNER JOIN users_roles ON users.id = users_roles.user_id
INNER JOIN roles ON users_roles.role_id = roles.id;
Then:
#1 Login using admin credentials
#2 Check out User page
#3 Check out Admin page
#4 Logout
#5 Register regular user account
#6 Check out User page
#7 Try to get admin content by regular user using REST API: http://localhost:8080/api/admincontent
ERROR 77100 --- [nio-8080-exec-2] c.k.backend.jwt.JwtAuthEntryPoint : Unauthorized error. Message - Full authentication is required to access this resource
#5 Register regular user account
#6 Check out User page
#7 Try to get admin content by regular user using REST API: http://localhost:8080/api/admincontent
ERROR 77100 --- [nio-8080-exec-2] c.k.backend.jwt.JwtAuthEntryPoint : Unauthorized error. Message - Full authentication is required to access this resource
Ways to Improve
- Don't user local storage! That's not secure
- Use OAuth
- Use email verification
- Use reCAPTCHA
Comments
Post a Comment