Spring Boot Security: JWT Authentication & Authorization

Introduction
Security is a critical aspect of any web application. Spring Security provides comprehensive authentication and authorization support for Spring Boot applications, protecting your REST APIs from unauthorized access.
In this comprehensive guide, we'll cover:
- Understanding Spring Security architecture
- Implementing JWT-based authentication
- Password encryption with BCrypt
- Role-based authorization (RBAC)
- Securing REST API endpoints
- Handling authentication exceptions
- Best practices for production security
Prerequisites: This tutorial builds on Spring Boot Database Integration. Make sure you have a working Spring Boot application with JPA before proceeding.
Why Spring Security?
Spring Security offers:
- Industry-standard security: Battle-tested framework used by millions
- Flexible authentication: Support for various authentication mechanisms
- Fine-grained authorization: Control access at method and URL level
- CSRF protection: Built-in Cross-Site Request Forgery protection
- Session management: Stateless JWT or stateful session handling
- Integration: Seamless integration with Spring ecosystem
Understanding Authentication vs Authorization
Authentication: Verifying who you are (login with username/password)
Authorization: Verifying what you can access (user roles and permissions)
Flow:
- User provides credentials (username/password)
- Application verifies credentials (authentication)
- Application issues JWT token
- Client sends token with each request
- Application verifies token and checks permissions (authorization)
Setting Up Dependencies
Update your pom.xml:
<dependencies>
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- JWT Support -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.3</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.3</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.3</version>
<scope>runtime</scope>
</dependency>
<!-- Existing dependencies -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>For Gradle:
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'io.jsonwebtoken:jjwt-api:0.12.3'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.3'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.3'
}Creating Role and User Entities
Role Entity
src/main/java/com/example/demo/entity/Role.java:
package com.example.demo.entity;
import jakarta.persistence.*;
@Entity
@Table(name = "roles")
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String name;
private String description;
// Constructors
public Role() {}
public Role(String name) {
this.name = name;
}
// Getters and Setters
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
}Update User Entity
Update src/main/java/com/example/demo/entity/User.java to include roles:
package com.example.demo.entity;
import jakarta.persistence.*;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import java.time.LocalDateTime;
import java.util.HashSet;
import java.util.Set;
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank(message = "Username is required")
@Size(min = 3, max = 50)
@Column(unique = true, nullable = false)
private String username;
@NotBlank(message = "Email is required")
@Email(message = "Email should be valid")
@Column(unique = true, nullable = false)
private String email;
@NotBlank(message = "Password is required")
@Size(min = 8)
@Column(nullable = false)
private String password;
@Column(name = "first_name")
private String firstName;
@Column(name = "last_name")
private String lastName;
@Column(nullable = false)
private Boolean active = true;
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(
name = "user_roles",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id")
)
private Set<Role> roles = new HashSet<>();
@CreationTimestamp
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
@UpdateTimestamp
@Column(name = "updated_at")
private LocalDateTime updatedAt;
// Constructors
public User() {}
public User(String username, String email, String password) {
this.username = username;
this.email = email;
this.password = password;
}
// Getters and Setters
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public Boolean getActive() {
return active;
}
public void setActive(Boolean active) {
this.active = active;
}
public Set<Role> getRoles() {
return roles;
}
public void setRoles(Set<Role> roles) {
this.roles = roles;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public LocalDateTime getUpdatedAt() {
return updatedAt;
}
}Implementing UserDetailsService
Spring Security uses UserDetailsService to load user-specific data.
src/main/java/com/example/demo/security/CustomUserDetailsService.java:
package com.example.demo.security;
import com.example.demo.entity.User;
import com.example.demo.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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.transaction.annotation.Transactional;
import java.util.Set;
import java.util.stream.Collectors;
@Service
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
@Autowired
public CustomUserDetailsService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
@Transactional(readOnly = true)
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found: " + username));
if (!user.getActive()) {
throw new UsernameNotFoundException("User account is disabled: " + username);
}
Set<GrantedAuthority> authorities = user.getRoles().stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role.getName()))
.collect(Collectors.toSet());
return org.springframework.security.core.userdetails.User
.withUsername(user.getUsername())
.password(user.getPassword())
.authorities(authorities)
.accountExpired(false)
.accountLocked(false)
.credentialsExpired(false)
.disabled(!user.getActive())
.build();
}
}JWT Token Provider
Create a utility class to generate and validate JWT tokens.
src/main/java/com/example/demo/security/JwtTokenProvider.java:
package com.example.demo.security;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.util.Date;
@Component
public class JwtTokenProvider {
@Value("${app.jwt.secret:mySecretKeyThatIsAtLeast256BitsLongForHS256Algorithm}")
private String jwtSecret;
@Value("${app.jwt.expiration:86400000}") // 24 hours in milliseconds
private long jwtExpirationMs;
private SecretKey getSigningKey() {
return Keys.hmacShaKeyFor(jwtSecret.getBytes());
}
public String generateToken(Authentication authentication) {
UserDetails userPrincipal = (UserDetails) authentication.getPrincipal();
Date now = new Date();
Date expiryDate = new Date(now.getTime() + jwtExpirationMs);
return Jwts.builder()
.subject(userPrincipal.getUsername())
.issuedAt(now)
.expiration(expiryDate)
.signWith(getSigningKey())
.compact();
}
public String getUsernameFromToken(String token) {
Claims claims = Jwts.parser()
.verifyWith(getSigningKey())
.build()
.parseSignedClaims(token)
.getPayload();
return claims.getSubject();
}
public boolean validateToken(String token) {
try {
Jwts.parser()
.verifyWith(getSigningKey())
.build()
.parseSignedClaims(token);
return true;
} catch (JwtException | IllegalArgumentException e) {
// Log the exception
return false;
}
}
}Important: In production, store the JWT secret in environment variables, not in code.
Add to application.properties:
# JWT Configuration
app.jwt.secret=${JWT_SECRET:mySecretKeyThatIsAtLeast256BitsLongForHS256Algorithm}
app.jwt.expiration=86400000JWT Authentication Filter
Create a filter to validate JWT tokens on each request.
src/main/java/com/example/demo/security/JwtAuthenticationFilter.java:
package com.example.demo.security;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
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.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenProvider tokenProvider;
private final CustomUserDetailsService userDetailsService;
@Autowired
public JwtAuthenticationFilter(JwtTokenProvider tokenProvider,
CustomUserDetailsService userDetailsService) {
this.tokenProvider = tokenProvider;
this.userDetailsService = userDetailsService;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
try {
String jwt = getJwtFromRequest(request);
if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) {
String username = tokenProvider.getUsernameFromToken(jwt);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception ex) {
logger.error("Could not set user authentication in security context", ex);
}
filterChain.doFilter(request, response);
}
private String getJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}Security Configuration
Configure Spring Security with JWT authentication.
src/main/java/com/example/demo/config/SecurityConfig.java:
package com.example.demo.config;
import com.example.demo.security.CustomUserDetailsService;
import com.example.demo.security.JwtAuthenticationFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
private final CustomUserDetailsService userDetailsService;
private final JwtAuthenticationFilter jwtAuthenticationFilter;
@Autowired
public SecurityConfig(CustomUserDetailsService userDetailsService,
JwtAuthenticationFilter jwtAuthenticationFilter) {
this.userDetailsService = userDetailsService;
this.jwtAuthenticationFilter = jwtAuthenticationFilter;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService);
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {
return authConfig.getAuthenticationManager();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers(HttpMethod.GET, "/api/users").hasRole("ADMIN")
.requestMatchers(HttpMethod.POST, "/api/users").permitAll()
.requestMatchers(HttpMethod.PUT, "/api/users/**").authenticated()
.requestMatchers(HttpMethod.DELETE, "/api/users/**").hasRole("ADMIN")
.anyRequest().authenticated()
);
http.authenticationProvider(authenticationProvider());
http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}Security Rules:
/api/auth/**: Public (login, register)GET /api/users: ADMIN onlyPOST /api/users: Public (registration)PUT /api/users/**: Authenticated users onlyDELETE /api/users/**: ADMIN only- All other endpoints: Authenticated
Authentication DTOs
Login Request
src/main/java/com/example/demo/dto/LoginRequest.java:
package com.example.demo.dto;
import jakarta.validation.constraints.NotBlank;
public class LoginRequest {
@NotBlank(message = "Username is required")
private String username;
@NotBlank(message = "Password is required")
private String password;
// Constructors
public LoginRequest() {}
public LoginRequest(String username, String password) {
this.username = username;
this.password = password;
}
// Getters and Setters
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}JWT Response
src/main/java/com/example/demo/dto/JwtResponse.java:
package com.example.demo.dto;
public class JwtResponse {
private String token;
private String type = "Bearer";
private String username;
public JwtResponse(String token, String username) {
this.token = token;
this.username = username;
}
// Getters and Setters
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
}Register Request
src/main/java/com/example/demo/dto/RegisterRequest.java:
package com.example.demo.dto;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
public class RegisterRequest {
@NotBlank(message = "Username is required")
@Size(min = 3, max = 50)
private String username;
@NotBlank(message = "Email is required")
@Email(message = "Email should be valid")
private String email;
@NotBlank(message = "Password is required")
@Size(min = 8, message = "Password must be at least 8 characters")
private String password;
private String firstName;
private String lastName;
// Constructors
public RegisterRequest() {}
// Getters and Setters
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
}Authentication Controller
src/main/java/com/example/demo/controller/AuthController.java:
package com.example.demo.controller;
import com.example.demo.dto.JwtResponse;
import com.example.demo.dto.LoginRequest;
import com.example.demo.dto.RegisterRequest;
import com.example.demo.entity.Role;
import com.example.demo.entity.User;
import com.example.demo.repository.RoleRepository;
import com.example.demo.repository.UserRepository;
import com.example.demo.security.JwtTokenProvider;
import jakarta.validation.Valid;
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.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.*;
import java.util.HashSet;
import java.util.Set;
@RestController
@RequestMapping("/api/auth")
public class AuthController {
private final AuthenticationManager authenticationManager;
private final UserRepository userRepository;
private final RoleRepository roleRepository;
private final PasswordEncoder passwordEncoder;
private final JwtTokenProvider tokenProvider;
@Autowired
public AuthController(AuthenticationManager authenticationManager,
UserRepository userRepository,
RoleRepository roleRepository,
PasswordEncoder passwordEncoder,
JwtTokenProvider tokenProvider) {
this.authenticationManager = authenticationManager;
this.userRepository = userRepository;
this.roleRepository = roleRepository;
this.passwordEncoder = passwordEncoder;
this.tokenProvider = tokenProvider;
}
@PostMapping("/login")
public ResponseEntity<JwtResponse> login(@Valid @RequestBody LoginRequest loginRequest) {
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
loginRequest.getUsername(),
loginRequest.getPassword()
)
);
SecurityContextHolder.getContext().setAuthentication(authentication);
String jwt = tokenProvider.generateToken(authentication);
return ResponseEntity.ok(new JwtResponse(jwt, loginRequest.getUsername()));
}
@PostMapping("/register")
public ResponseEntity<String> register(@Valid @RequestBody RegisterRequest registerRequest) {
// Check if username already exists
if (userRepository.existsByUsername(registerRequest.getUsername())) {
return ResponseEntity.badRequest().body("Username is already taken");
}
// Check if email already exists
if (userRepository.existsByEmail(registerRequest.getEmail())) {
return ResponseEntity.badRequest().body("Email is already in use");
}
// Create new user
User user = new User();
user.setUsername(registerRequest.getUsername());
user.setEmail(registerRequest.getEmail());
user.setPassword(passwordEncoder.encode(registerRequest.getPassword()));
user.setFirstName(registerRequest.getFirstName());
user.setLastName(registerRequest.getLastName());
user.setActive(true);
// Assign default USER role
Role userRole = roleRepository.findByName("USER")
.orElseThrow(() -> new RuntimeException("Default USER role not found"));
Set<Role> roles = new HashSet<>();
roles.add(userRole);
user.setRoles(roles);
userRepository.save(user);
return ResponseEntity.status(HttpStatus.CREATED).body("User registered successfully");
}
}Role Repository
src/main/java/com/example/demo/repository/RoleRepository.java:
package com.example.demo.repository;
import com.example.demo.entity.Role;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface RoleRepository extends JpaRepository<Role, Long> {
Optional<Role> findByName(String name);
boolean existsByName(String name);
}Database Migration
Create Flyway migration for roles table.
src/main/resources/db/migration/V3__Add_security_tables.sql:
-- Create roles table (if not exists from previous tutorial)
CREATE TABLE IF NOT EXISTS roles (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(50) UNIQUE NOT NULL,
description VARCHAR(255)
);
-- Create user_roles junction table (if not exists)
CREATE TABLE IF NOT EXISTS user_roles (
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
role_id BIGINT NOT NULL REFERENCES roles(id) ON DELETE CASCADE,
PRIMARY KEY (user_id, role_id)
);
-- Insert default roles
INSERT INTO roles (name, description) VALUES
('USER', 'Regular user with basic permissions'),
('ADMIN', 'Administrator with full permissions')
ON CONFLICT (name) DO NOTHING;
-- Create indexes for performance
CREATE INDEX IF NOT EXISTS idx_user_roles_user_id ON user_roles(user_id);
CREATE INDEX IF NOT EXISTS idx_user_roles_role_id ON user_roles(role_id);Method-Level Security
Use @PreAuthorize for fine-grained authorization.
Update UserController.java:
package com.example.demo.controller;
import com.example.demo.entity.User;
import com.example.demo.service.UserService;
import jakarta.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
@Autowired
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<List<User>> getAllActiveUsers() {
List<User> users = userService.getAllActiveUsers();
return ResponseEntity.ok(users);
}
@GetMapping("/{id}")
@PreAuthorize("hasRole('ADMIN') or @userService.isCurrentUser(#id)")
public ResponseEntity<User> getUser(@PathVariable Long id) {
return userService.getUserById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@GetMapping("/me")
public ResponseEntity<User> getCurrentUser() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String username = authentication.getName();
return userService.getUserByUsername(username)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@PutMapping("/{id}")
@PreAuthorize("hasRole('ADMIN') or @userService.isCurrentUser(#id)")
public ResponseEntity<User> updateUser(@PathVariable Long id,
@Valid @RequestBody User user) {
User updatedUser = userService.updateUser(id, user);
return ResponseEntity.ok(updatedUser);
}
@DeleteMapping("/{id}")
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
userService.deleteUser(id);
return ResponseEntity.noContent().build();
}
}Add helper method to UserService.java:
public boolean isCurrentUser(Long userId) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String currentUsername = authentication.getName();
return userRepository.findById(userId)
.map(user -> user.getUsername().equals(currentUsername))
.orElse(false);
}Testing the API
1. Register a User
curl -X POST http://localhost:8080/api/auth/register \
-H "Content-Type: application/json" \
-d '{
"username": "johndoe",
"email": "john@example.com",
"password": "password123",
"firstName": "John",
"lastName": "Doe"
}'Response:
User registered successfully2. Login
curl -X POST http://localhost:8080/api/auth/login \
-H "Content-Type: application/json" \
-d '{
"username": "johndoe",
"password": "password123"
}'Response:
{
"token": "eyJhbGciOiJIUzI1NiJ9...",
"type": "Bearer",
"username": "johndoe"
}3. Access Protected Endpoint
curl http://localhost:8080/api/users/me \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9..."4. Access Admin-Only Endpoint (will fail for regular users)
curl http://localhost:8080/api/users \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9..."Response: 403 Forbidden (unless user has ADMIN role)
Exception Handling
Create a custom exception handler for authentication errors.
src/main/java/com/example/demo/exception/GlobalExceptionHandler.java:
package com.example.demo.exception;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BadCredentialsException.class)
public ResponseEntity<Map<String, Object>> handleBadCredentials(BadCredentialsException ex) {
Map<String, Object> error = new HashMap<>();
error.put("timestamp", LocalDateTime.now());
error.put("status", HttpStatus.UNAUTHORIZED.value());
error.put("error", "Unauthorized");
error.put("message", "Invalid username or password");
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(error);
}
@ExceptionHandler(UsernameNotFoundException.class)
public ResponseEntity<Map<String, Object>> handleUserNotFound(UsernameNotFoundException ex) {
Map<String, Object> error = new HashMap<>();
error.put("timestamp", LocalDateTime.now());
error.put("status", HttpStatus.NOT_FOUND.value());
error.put("error", "Not Found");
error.put("message", ex.getMessage());
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
}Best Practices
1. Store Secrets Securely
Never hardcode secrets:
# Bad
app.jwt.secret=myHardcodedSecret
# Good - use environment variables
app.jwt.secret=${JWT_SECRET}Set environment variable:
export JWT_SECRET=$(openssl rand -base64 64)2. Use Strong Password Encoding
BCrypt automatically handles salting and is resistant to rainbow table attacks:
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12); // Strength: 12 rounds
}3. Implement Token Refresh
For long-lived sessions, implement refresh tokens:
public String generateRefreshToken(Authentication authentication) {
// Generate token with longer expiration (7 days)
Date expiryDate = new Date(new Date().getTime() + 604800000);
// ...
}4. Add Rate Limiting
Prevent brute force attacks on login endpoint:
<dependency>
<groupId>com.bucket4j</groupId>
<artifactId>bucket4j-core</artifactId>
<version>8.7.0</version>
</dependency>5. Use HTTPS in Production
Always enforce HTTPS for authentication endpoints:
server.ssl.enabled=true
server.ssl.key-store=classpath:keystore.p12
server.ssl.key-store-password=${KEYSTORE_PASSWORD}6. Implement Account Lockout
Lock accounts after multiple failed login attempts:
@Column(name = "failed_attempts")
private Integer failedAttempts = 0;
@Column(name = "account_locked")
private Boolean accountLocked = false;7. Sanitize User Input
Always validate and sanitize input to prevent injection attacks:
@NotBlank
@Pattern(regexp = "^[a-zA-Z0-9_-]{3,50}$")
private String username;8. Log Security Events
Log authentication attempts and failures:
logger.info("Login attempt for user: {}", username);
logger.warn("Failed login attempt for user: {}", username);Testing Security
src/test/java/com/example/demo/security/AuthControllerTest.java:
@SpringBootTest
@AutoConfigureMockMvc
class AuthControllerTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@Test
void testLogin() throws Exception {
LoginRequest loginRequest = new LoginRequest("testuser", "password");
mockMvc.perform(post("/api/auth/login")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(loginRequest)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.token").exists())
.andExpect(jsonPath("$.username").value("testuser"));
}
@Test
void testAccessProtectedEndpointWithoutToken() throws Exception {
mockMvc.perform(get("/api/users/me"))
.andExpect(status().isUnauthorized());
}
}Common Security Issues
Issue 1: CORS Errors
Solution: Configure CORS in SecurityConfig:
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(List.of("http://localhost:3000"));
configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
configuration.setAllowedHeaders(List.of("*"));
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}Issue 2: Token Expiration Not Handled
Solution: Implement token refresh mechanism or handle 401 errors in frontend.
Issue 3: Passwords Stored in Plain Text
Solution: Always use PasswordEncoder before saving:
user.setPassword(passwordEncoder.encode(rawPassword));Conclusion
You've successfully implemented JWT-based authentication and authorization in your Spring Boot application. Key takeaways:
- Spring Security provides robust authentication and authorization
- JWT tokens enable stateless authentication for REST APIs
- BCrypt securely hashes passwords
- Role-based access control (RBAC) manages permissions
- Method-level security provides fine-grained authorization
- Best practices prevent common security vulnerabilities
In the next tutorial, we'll cover deploying your Spring Boot application to production with Docker and CI/CD pipelines.
Resources
- Spring Security Documentation
- JWT.io - JWT token debugger
- OWASP Top 10 - Security best practices
- Spring Security Architecture
📬 Subscribe to Newsletter
Get the latest blog posts delivered to your inbox every week. No spam, unsubscribe anytime.
We respect your privacy. Unsubscribe at any time.
💬 Comments
Sign in to leave a comment
We'll never post without your permission.