JWT Authentication and Authorization with Spring Boot and Vue.js: Complete Guide


Here's a brief explanation of the diagram's flow:

Actors:

  • User (U): Represents the end user interacting with the application.
  • LoginComponent (LC): The component in the frontend (Vue.js) where the user enters login credentials.
  • AuthService (AS): A service in the frontend responsible for handling authentication, such as calling the backend to log in and storing the JWT token and user roles in local storage.
  • LocalStorage (LS): A storage mechanism for saving the JWT token and user roles in the frontend.
  • AuthController (AC): The backend controller responsible for handling authentication requests (e.g., login).
  • JwtTokenUtil (JTU): A utility in the backend that generates and validates JWT tokens.
  • UserController (UC): A backend controller that handles user-related routes.
  • SecurityConfig (SC): A backend configuration that defines security rules and role-based access control.

Flow:

  1. User Login:

    • The User enters their credentials into the LoginComponent (LC).
    • The LoginComponent sends the credentials to the AuthService (AS).
    • AuthService calls the AuthController (AC) with the login request.
    • The AuthController uses JwtTokenUtil (JTU) to generate a JWT token and returns it to AuthService.
    • AuthService stores the JWT token and the user's roles in LocalStorage (LS).
  2. User Accessing Protected Routes:

    • The User navigates to protected routes, such as the user or admin dashboard.
    • AuthService checks if the JWT token is valid by retrieving it from LocalStorage and validating it with JwtTokenUtil.
    • If the token is valid, the User can access the routes.
  3. Admin Role Validation:

    • When accessing an AdminComponent, AuthService verifies that the user has the admin role by checking the stored roles in LocalStorage.
    • If the role is valid, access is granted; otherwise, access is denied.
  4. Backend Security:

    • The UserController checks if the user is authenticated and authorized to access routes like /user/home.
    • For routes requiring admin privileges (e.g., /admin/home), SecurityConfig ensures the user has the correct role (admin) before granting access.

Complete Solution: JWT Authentication & Authorization


1. Backend: Spring Boot 3.x (JWT Authentication & Authorization)

Step 1: Create the Spring Boot Project

Create a Spring Boot project with the necessary dependencies.

In pom.xml:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt</artifactId>
        <version>0.11.5</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <scope>runtime</scope>
    </dependency>
</dependencies>

Step 2: Configure application.properties

Configure the H2 database for testing.

spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=password
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=update
spring.h2.console.enabled=true

Step 3: Create JWT Utility Class

The JwtTokenUtil class handles generating, validating, and extracting information from JWT tokens.

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.List;

@Component
public class JwtTokenUtil {

    private String secretKey = "secret";  // Use a stronger key for production

    public String generateToken(String username, List<String> roles) {
        return Jwts.builder()
                .setSubject(username)
                .claim("roles", roles)  // Include roles in the token
                .setExpiration(new Date(System.currentTimeMillis() + 600000)) // 10 minutes
                .signWith(SignatureAlgorithm.HS512, secretKey)
                .compact();
    }

    public Claims extractClaims(String token) {
        return Jwts.parser()
                .setSigningKey(secretKey)
                .parseClaimsJws(token)
                .getBody();
    }

    public boolean isTokenExpired(String token) {
        return extractClaims(token).getExpiration().before(new Date());
    }

    public String getUsernameFromToken(String token) {
        return extractClaims(token).getSubject();
    }

    public List<String> getRolesFromToken(String token) {
        return (List<String>) extractClaims(token).get("roles");
    }

    public boolean validateToken(String token, String username) {
        return username.equals(getUsernameFromToken(token)) && !isTokenExpired(token);
    }
}

Step 4: Security Configuration with Role-Based Access

The SecurityConfig class configures role-based authorization in Spring Security.

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public HttpSecurity configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
            .authorizeRequests()
            .antMatchers("/api/auth/login", "/register").permitAll()
            .antMatchers("/admin/**").hasRole("ADMIN")  // Restrict admin endpoints
            .antMatchers("/user/**").hasRole("USER")   // Restrict user endpoints
            .anyRequest().authenticated()
            .and()
            .addFilter(new JwtAuthenticationFilter());  // Add JWT filter here
        return http;
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public UserDetailsService userDetailsService() {
        return username -> {
            if ("admin".equals(username)) {
                return User.withUsername(username)
                        .password(passwordEncoder().encode("admin"))
                        .roles("ADMIN")
                        .build();
            } else if ("user".equals(username)) {
                return User.withUsername(username)
                        .password(passwordEncoder().encode("user"))
                        .roles("USER")
                        .build();
            } else {
                throw new UsernameNotFoundException("User not found");
            }
        };
    }
}

Step 5: Authentication Controller

The AuthController generates a JWT token after successful login, including user roles.

import com.example.demo.security.JwtTokenUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.stream.Collectors;

@RestController
@RequestMapping("/api/auth")
public class AuthController {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    @PostMapping("/login")
    public String login(@RequestBody LoginRequest loginRequest) {
        var authenticationToken = new UsernamePasswordAuthenticationToken(
                loginRequest.getUsername(), loginRequest.getPassword());
        var authentication = authenticationManager.authenticate(authenticationToken);

        // Get roles for the authenticated user
        List<String> roles = authentication.getAuthorities().stream()
                .map(authority -> authority.getAuthority())
                .collect(Collectors.toList());

        // Generate and return JWT token
        return jwtTokenUtil.generateToken(authentication.getName(), roles);
    }
}

Step 6: User Controller with Role-Based Access

Define the UserController to restrict access to endpoints based on roles.

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UserController {

    @GetMapping("/user/home")
    public String userHome() {
        return "Welcome, User!";
    }

    @GetMapping("/admin/home")
    public String adminHome() {
        return "Welcome, Admin!";
    }
}

2. Frontend: Vue.js (JWT Authentication & Authorization)

Step 1: Create Vue.js Project

Start by creating a new Vue.js project using Vue CLI:

vue create jwt-auth-frontend

Install Axios and jwt-decode:

npm install axios jwt-decode

Step 2: Auth Service

Create an auth.js service to handle JWT login, logout, and storing roles.

// src/services/auth.js
import axios from 'axios';
import jwtDecode from 'jwt-decode';

const API_URL = 'http://localhost:8080/api/auth';

export default {
  login(credentials) {
    return axios.post(`${API_URL}/login`, credentials)
      .then(response => {
        const token = response.data.token;
        localStorage.setItem('jwt', token);  // Save JWT token

        // Decode the token to extract user roles
        const decodedToken = jwtDecode(token);
        localStorage.setItem('roles', JSON.stringify(decodedToken.roles));  // Store roles

        return response.data;
      });
  },

  logout() {
    localStorage.removeItem('jwt');
    localStorage.removeItem('roles');
  },

  getAuthHeader() {
    const token = localStorage.getItem('jwt');
    return token ? { Authorization: `Bearer ${token}` } : {};
  },

  isRole(role) {
    const roles = JSON.parse(localStorage.getItem('roles')) || [];
    return roles.includes(role);
  }
};

Step 3: Vue Router with Role-Based Access

Configure the Vue Router to protect routes based on roles.

// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router';
import Home from '../views/Home.vue';
import Login from '../views/Login.vue';
import auth from '../services/auth';

const routes = [
  { path: '/', component: Home, meta: { requiresAuth: true, requiredRole: 'USER' } },
  { path: '/admin', component: () => import('../views/Admin.vue'), meta: { requiresAuth: true, requiredRole: 'ADMIN' } },
  { path: '/login', component: Login },
];

const router = createRouter({
  history: createWebHistory(),
  routes,
});

router.beforeEach((to, from, next) => {
  if (to.meta.requiresAuth) {
    if (!localStorage.getItem('jwt')) {
      next('/login');
    } else if (to.meta.requiredRole && !auth.isRole(to.meta.requiredRole)) {
      next('/login');
    } else {
      next();
    }
  } else {
    next();
  }
});

export default router;

Step 4: Login Component

Create a login form in Login.vue.

<template>
  <div>
    <h1>Login</h1>
    <form @submit.prevent="handleLogin">
      <input v-model="username" type="text" placeholder="Username" required />
      <input v-model="password" type="password" placeholder="Password" required />
      <button type="submit">Login</button>
    </form>
  </div>
</template>

<script>
import auth from '../services/auth';

export default {
  data() {
    return {
      username: '',
      password: ''
    };
  },
  methods: {
    async handleLogin() {
      try {
        await auth.login({ username: this.username, password: this.password });
        this.$router.push('/');
      } catch (error) {
        alert('Login failed');
      }
    }
  }
};
</script>

Step 5: Admin and User Components

Create the protected pages for Admin and User.

Admin.vue:

<template>
  <div>
    <h1>Admin Dashboard</h1>
    <p>Welcome, Admin!</p>
  </div>
</template>

Home.vue (User Dashboard):

<template>
  <div>
    <h1>User Dashboard</h1>
    <p>Welcome, User!</p>
  </div>
</template>

Final Notes

This is a fully functional, end-to-end JWT Authentication and Authorization solution with Spring Boot and Vue.js:

  • Backend:
    • JWT Authentication is implemented, and the token contains user roles.
    • Role-based authorization restricts access to specific endpoints.
  • Frontend:
    • After logging in, the JWT is stored and decoded to access roles.
    • Vue.js routes are protected based on user roles.

This example can be expanded to include database-backed user authentication, custom error handling, and better security practices (e.g., refresh tokens).

Popular posts from this blog

Learn Java 8 streams with an example - print odd/even numbers from Array and List

Java Stream API - How to convert List of objects to another List of objects using Java streams?

Registration and Login with Spring Boot + Spring Security + Thymeleaf

Java, Spring Boot Mini Project - Library Management System - Download

ReactJS, Spring Boot JWT Authentication Example

Top 5 Java ORM tools - 2024

Java - Blowfish Encryption and decryption Example

Spring boot video streaming example-HTML5

Google Cloud Storage + Spring Boot - File Upload, Download, and Delete