Prerequisites
Before we start, ensure you have the following:
- Java Development Kit (JDK) installed
- Apache Maven installed
- Node.js and npm installed
- Angular CLI installed (
npm install -g @angular/cli
) - An IDE (such as IntelliJ IDEA, Eclipse, or VS Code) installed
Step 1: Setting Up the Spring Boot Backend
1.1 Create a Spring Boot Project
-
Open Spring Initializr:
- Go to Spring Initializr in your web browser.
-
Configure Project Metadata:
- Project: Maven Project
- Language: Java
- Spring Boot: Select the latest version of Spring Boot 3.3
- Group: com.example
- Artifact: jwt-auth
- Name: jwt-auth
- Description: JWT Authentication Service
- Package Name: com.example.jwtauth
- Packaging: Jar
- Java Version: 17 (or your preferred version)
- Click
Next
.
-
Select Dependencies:
- On the
Dependencies
screen, select:- Spring Web
- Spring Security
- Spring Data JPA
- H2 Database
- Spring Boot DevTools
- Click
Next
.
- On the
-
Generate the Project:
- Click
Generate
to download the project zip file. - Extract the zip file to your desired location.
- Click
-
Open the Project in Your IDE:
- Open your IDE and import the project as a Maven project.
1.2 Update application.properties
Open the application.properties
file located in the src/main/resources
directory and add the following configuration:
server.port=8080
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.h2.console.enabled=true
spring.jpa.hibernate.ddl-auto=update
jwt.secret=your_jwt_secret_key
Explanation:
- Configures the server port to 8080.
- Sets up an in-memory H2 database.
- Enables the H2 console for database inspection.
- Automatically updates the database schema based on JPA entities.
- Adds a secret key for signing JWT tokens.
1.3 Create User Entity
Create a User
entity class in the com.example.jwtauth.model
package:
package com.example.jwtauth.model;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String password;
private String email;
// Getters and Setters
}
Explanation:
@Entity
: Marks the class as a JPA entity.@Id
: Marks theid
field as the primary key.@GeneratedValue(strategy = GenerationType.IDENTITY)
: Configures auto-increment for theid
field.username
,password
,email
: Fields representing user attributes.- Getters and Setters: Methods to access and modify the fields.
1.4 Create User Repository
Create a UserRepository
interface in the com.example.jwtauth.repository
package:
package com.example.jwtauth.repository;
import com.example.jwtauth.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
User findByUsername(String username);
}
Explanation:
@Repository
: Marks the interface as a Spring Data repository.- Extends
JpaRepository<User, Long>
: Provides CRUD operations for theUser
entity. findByUsername(String username)
: Custom query method to find a user by username.
1.5 Create User Service
Create a UserService
class in the com.example.jwtauth.service
package that implements UserDetailsService
:
package com.example.jwtauth.service;
import com.example.jwtauth.model.User;
import com.example.jwtauth.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.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.Optional;
@Service
public class UserService implements UserDetailsService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
@Autowired
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
this.passwordEncoder = new BCryptPasswordEncoder();
}
public User saveUser(User user) {
user.setPassword(passwordEncoder.encode(user.getPassword()));
return userRepository.save(user);
}
public Optional<User> findByUsername(String username) {
return Optional.ofNullable(userRepository.findByUsername(username));
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("User not found");
}
return org.springframework.security.core.userdetails.User
.withUsername(user.getUsername())
.password(user.getPassword())
.authorities("USER")
.build();
}
}
Explanation:
@Service
: Marks the class as a service component.- Implements
UserDetailsService
: Required by Spring Security to load user-specific data. loadUserByUsername(String username)
: Loads user details for authentication.saveUser(User user)
: Saves a new user with an encoded password.findByUsername(String username)
: Finds a user by username.
1.6 Create JWT Utility Class
Create a JwtUtils
class in the com.example.jwtauth.util
package:
package com.example.jwtauth.util;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.function.Function;
@Component
public class JwtUtils {
@Value("${jwt.secret}")
private String secret;
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
public Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}
private Claims extractAllClaims(String token) {
return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
}
private Boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
public String generateToken(UserDetails userDetails) {
return createToken(userDetails.getUsername());
}
private String createToken(String subject) {
return Jwts.builder()
.setSubject(subject)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 10))
.signWith(SignatureAlgorithm.HS256, secret)
.compact();
}
public Boolean validateToken(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
}
Explanation:
@Component
: Marks the class as a Spring bean.- Methods for generating and validating JWT tokens.
- Uses the secret key from the application properties for signing tokens.
1.7 Create Security Configuration
Create a SecurityConfig
class in the com.example.jwtauth.config
package:
package com.example.jwtauth.config;
import com.example.jwtauth.service.UserService;
import com.example.jwtauth.util.JwtUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final UserService userService;
private
final JwtUtils jwtUtils;
@Autowired
public SecurityConfig(UserService userService, JwtUtils jwtUtils) {
this.userService = userService;
this.jwtUtils = jwtUtils;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/auth/**").permitAll()
.anyRequest().authenticated()
)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.addFilterBefore(new JwtAuthenticationFilter(userService, jwtUtils), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Autowired
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService).passwordEncoder(passwordEncoder());
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception {
return http.getSharedObject(AuthenticationManagerBuilder.class).build();
}
}
Explanation:
@Configuration
: Marks the class as a configuration component.@EnableWebSecurity
: Enables Spring Security.securityFilterChain(HttpSecurity http)
: Configures HTTP security, disabling CSRF and permitting access to/auth/**
endpoints.configure(AuthenticationManagerBuilder auth)
: Configures the authentication manager with user details service and password encoder.passwordEncoder()
: Configures the password encoder.authenticationManager(HttpSecurity http)
: Provides the authentication manager.
1.8 Create JWT Authentication Filter
Create a JwtAuthenticationFilter
class in the com.example.jwtauth.filter
package:
package com.example.jwtauth.filter;
import com.example.jwtauth.service.UserService;
import com.example.jwtauth.util.JwtUtils;
import org.springframework.beans.factory.annotation.Autowired;
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 javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final UserService userService;
private final JwtUtils jwtUtils;
@Autowired
public JwtAuthenticationFilter(UserService userService, JwtUtils jwtUtils) {
this.userService = userService;
this.jwtUtils = jwtUtils;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
final String authorizationHeader = request.getHeader("Authorization");
String username = null;
String jwt = null;
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
jwt = authorizationHeader.substring(7);
username = jwtUtils.extractUsername(jwt);
}
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userService.loadUserByUsername(username);
if (jwtUtils.validateToken(jwt, userDetails)) {
var usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
usernamePasswordAuthenticationToken.setDetails(
new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
}
}
filterChain.doFilter(request, response);
}
}
Explanation:
extends OncePerRequestFilter
: Ensures that the filter is executed once per request.doFilterInternal(...)
: Extracts the JWT token from the Authorization header, validates it, and sets the authentication in the security context.
1.9 Create Authentication Controller
Create an AuthController
class in the com.example.jwtauth.controller
package:
package com.example.jwtauth.controller;
import com.example.jwtauth.model.User;
import com.example.jwtauth.service.UserService;
import com.example.jwtauth.util.JwtUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/auth")
@CrossOrigin(origins = "http://localhost:4200")
public class AuthController {
private final AuthenticationManager authenticationManager;
private final UserService userService;
private final JwtUtils jwtUtils;
@Autowired
public AuthController(AuthenticationManager authenticationManager, UserService userService, JwtUtils jwtUtils) {
this.authenticationManager = authenticationManager;
this.userService = userService;
this.jwtUtils = jwtUtils;
}
@PostMapping("/register")
public ResponseEntity<?> registerUser(@RequestBody User user) {
if (userService.findByUsername(user.getUsername()).isPresent()) {
return ResponseEntity.badRequest().body("Username is already taken");
}
userService.saveUser(user);
return ResponseEntity.ok("User registered successfully");
}
@PostMapping("/login")
public ResponseEntity<?> loginUser(@RequestBody User user) {
try {
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword()));
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
String jwt = jwtUtils.generateToken(userDetails);
return ResponseEntity.ok(jwt);
} catch (AuthenticationException e) {
return ResponseEntity.status(401).body("Invalid username or password");
}
}
}
Explanation:
@RestController
: Marks the class as a REST controller.@RequestMapping("/auth")
: Maps requests to/auth
URL.@CrossOrigin(origins = "http://localhost:4200")
: Enables CORS for requests from the Angular frontend running onlocalhost:4200
.registerUser(@RequestBody User user)
: Handles user registration.loginUser(@RequestBody User user)
: Handles user login and returns a JWT token.
Step 2: Setting Up the Angular Frontend
2.1 Create an Angular Project
- Open a terminal and run the following command to create a new Angular project:
ng new jwt-auth-client
- Navigate to the project directory:
cd jwt-auth-client
2.2 Install Dependencies
Install Bootstrap for styling:
npm install bootstrap
Add Bootstrap to angular.json
:
"styles": [
"src/styles.css",
"node_modules/bootstrap/dist/css/bootstrap.min.css"
],
2.3 Create Angular Services and Components
2.3.1 Create Auth Service
Generate the AuthService:
ng generate service services/auth
Edit auth.service.ts
:
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class AuthService {
private baseUrl = 'http://localhost:8080/auth';
constructor(private http: HttpClient) { }
register(user: any): Observable<any> {
return this.http.post(`${this.baseUrl}/register`, user);
}
login(credentials: any): Observable<any> {
return this.http.post(`${this.baseUrl}/login`, credentials);
}
}
Explanation:
@Injectable({ providedIn: 'root' })
: Marks the service as injectable and available throughout the app.HttpClient
: Service for making HTTP requests.register(user: any)
: Sends a POST request to register a new user.login(credentials: any)
: Sends a POST request to log in a user.
2.3.2 Create Components
Generate the components for registration and login:
ng generate component components/register
ng generate component components/login
Edit register.component.ts
:
import { Component } from '@angular/core';
import { AuthService } from '../../services/auth.service';
import { Router } from '@angular/router';
@Component({
selector: 'app-register',
templateUrl: './register.component.html',
styleUrls: ['./register.component.css']
})
export class RegisterComponent {
user: any = { username: '', password: '', email: '' };
constructor(private authService: AuthService, private router: Router) { }
register() {
this.authService.register(this.user).subscribe(() => {
this.router.navigate(['/login']);
}, error => {
console.error('Registration error: ', error);
});
}
}
Edit register.component.html
:
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card">
<div class="card-header">Register</div>
<div class="card-body">
<form (ngSubmit)="register()">
<div class="form-group">
<label for="username">Username</label>
<input type="text" class="form-control" id="username" [(ngModel)]="user.username" name="username" required>
</div>
<div class="form-group">
<label for="email">Email</label>
<input type
="email" class="form-control" id="email" [(ngModel)]="user.email" name="email" required>
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" class="form-control" id="password" [(ngModel)]="user.password" name="password" required>
</div>
<button type="submit" class="btn btn-primary">Register</button>
</form>
</div>
</div>
</div>
</div>
</div>
Edit login.component.ts
:
import { Component } from '@angular/core';
import { AuthService } from '../../services/auth.service';
import { Router } from '@angular/router';
@Component({
selector: 'app-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.css']
})
export class LoginComponent {
credentials: any = { username: '', password: '' };
constructor(private authService: AuthService, private router: Router) { }
login() {
this.authService.login(this.credentials).subscribe((response) => {
localStorage.setItem('token', response);
this.router.navigate(['/home']);
}, error => {
console.error('Login error: ', error);
});
}
}
Edit login.component.html
:
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card">
<div class="card-header">Login</div>
<div class="card-body">
<form (ngSubmit)="login()">
<div class="form-group">
<label for="username">Username</label>
<input type="text" class="form-control" id="username" [(ngModel)]="credentials.username" name="username" required>
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" class="form-control" id="password" [(ngModel)]="credentials.password" name="password" required>
</div>
<button type="submit" class="btn btn-primary">Login</button>
</form>
</div>
</div>
</div>
</div>
</div>
2.4 Update Angular Routing
Edit app-routing.module.ts
:
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { RegisterComponent } from './components/register/register.component';
import { LoginComponent } from './components/login/login.component';
const routes: Routes = [
{ path: 'register', component: RegisterComponent },
{ path: 'login', component: LoginComponent },
{ path: '', redirectTo: '/register', pathMatch: 'full' }
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
Explanation:
- Defines routes for the registration and login components.
- Redirects the root path to the registration component.
2.5 Update Angular App Module
Edit app.module.ts
:
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { RegisterComponent } from './components/register/register.component';
import { LoginComponent } from './components/login/login.component';
@NgModule({
declarations: [
AppComponent,
RegisterComponent,
LoginComponent
],
imports: [
BrowserModule,
AppRoutingModule,
FormsModule,
HttpClientModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Explanation:
- Imports necessary modules for the Angular app.
- Declares the components used in the app.
- Sets up the app's root module.
2.6 Run the Angular Application
Open a terminal in the Angular project directory and run the application:
ng serve
Visit http://localhost:4200
in your web browser to see the application.
Conclusion
In this tutorial, we created a JWT authentication system using Spring Boot 3.3 for the backend and Angular 18 for the frontend. We handled CORS issues to ensure smooth communication between the Angular frontend and the Spring Boot backend. By following this structure, you can extend and customize the application as needed.
Comments
Post a Comment
Leave Comment