Spring Boot Mastery: Spring Security & @EnableWebSecurity Configuration
This chapter covers the fundamentals of securing Spring Boot applications using Spring Security. We will transition from an insecure application—where sensitive data like usernames are passed insecurely via path parameters—to a production-grade, secure architecture. You will learn how to implement custom database-backed authentication, secure endpoints selectively, hash passwords using BCrypt, and manage stateless sessions for REST APIs.
1. The Problem: Insecure Endpoints & Naive Parameter Passing
In a naive or early-stage application, endpoints are often completely unprotected. Anyone with the URL can access, modify, or delete resources.
The Naive Approach
Previously, we passed the username directly as a path parameter to identify which user's journal entries to retrieve or modify:
GET http://localhost:8080/journal/ram
Why This Is Highly Insecure
- No Verification: There is no password verification. Anyone can change the path parameter to
/journal/shyamand access another user's private data. - Exposed Credentials: Usernames and passwords should never be sent via path parameters or query parameters. Path parameters are logged by web servers, reverse proxies, and browser histories, exposing sensitive identifiers.
- Lack of Access Control: There is no mechanism to verify if the requester is actually who they claim to be.
To solve this, we must decouple identity verification from the API's path parameters and delegate it to a dedicated security framework: Spring Security.
2. Core Concepts: Authentication vs. Authorization
Spring Security is a powerful, highly customizable authentication and access-control framework. It is the de facto standard for securing Spring-based applications, focusing on two primary pillars: Authentication and Authorization.
Real-Life Analogy: Database Server Access
Imagine you want to access a MongoDB Atlas database cluster:
- Authentication: You enter your database user credentials. If the credentials match, the MongoDB server allows you to connect. You have successfully authenticated.
- Authorization: Once connected, what can you actually do? Can you only read collections (
Read-Only), or can you also write data, delete collections, and modify cluster settings (Read-Write/DbAdmin)? These permissions are determined by your assigned roles (Authorization).
3. Getting Started: Adding Spring Security & Default Behavior
To secure your Spring Boot application, you must first add the Spring Security starter dependency to your pom.xml file.
Step 1: Add the Dependency
Add the following dependency to your Maven configuration:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
Step 2: Automatic Auto-Configuration
Once this dependency is added, Spring Boot's auto-configuration feature immediately kicks in upon application startup:
- All Endpoints Secured: Every single endpoint in your application is secured by default.
- Default User Created: Spring Security registers a default user with the username
user. - Random Password Generation: A unique, random security password is generated and printed to the console logs during startup:
Using generated security password: 5f3e7b8c-1234-4abc-9def-0123456789ab
If you attempt to access any endpoint without credentials, the server will return an HTTP 401 Unauthorized or HTTP 403 Forbidden response.
4. HTTP Basic Authentication & Base64 Encoding
By default, Spring Security configures HTTP Basic Authentication.
How HTTP Basic Authentication Works
Under Basic Authentication, the client must send credentials with every request inside a standard HTTP header named Authorization.
The Encoding Mechanism
The credentials are formatted as a single string: username:password. This string is then encoded using Base64 encoding.
For example, if the username is user and the generated password is my-secret-password:
- Combine them:
user:my-secret-password - Encode to Base64:
dXNlcjpteS1zZWNyZXQtcGFzc3dvcmQ= - Construct the header:
HTTP
Authorization: Basic dXNlcjpteS1zZWNyZXQtcGFzc3dvcmQ=
Note: Base64 is an encoding scheme, not encryption. It can be easily decoded by anyone who intercepts the packet. Therefore, HTTP Basic Authentication must always be used in conjunction with HTTPS (SSL/TLS) in production environments to prevent credential sniffing.
5. Customizing Security: The Configuration Class & @EnableWebSecurity
While default security is useful for testing, production applications require custom rules (e.g., keeping registration endpoints public while securing dashboard endpoints). To achieve this, we create a custom security configuration class.
The @EnableWebSecurity Annotation
This annotation tells Spring that this class contains custom web security configurations. It is used in conjunction with @Configuration.
package com.journal.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@Configuration
@EnableWebSecurity
public class SpringSecurity extends WebSecurityConfigurerAdapter {
// Custom configuration methods go here
}
Note on Spring Boot Versioning: In older versions of Spring Security (Spring Boot 2.x), customizing security required extending
WebSecurityConfigurerAdapter. In modern Spring Boot 3.x (Spring Security 6.x),WebSecurityConfigurerAdapteris deprecated and removed. Instead, security is configured by declaring a@Beanof typeSecurityFilterChain.The code examples below follow the class-inheritance structure explained in the lecture, but are structured cleanly for production understanding.
Overriding the configure(HttpSecurity http) Method
This method defines how specific HTTP requests are matched and what security constraints are applied to them.
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/journal/**").authenticated() // Secure all journal endpoints
.anyRequest().permitAll() // Allow public access to everything else
.and()
.httpBasic(); // Use HTTP Basic Authentication
}
Understanding Method Chaining
Spring Security uses a fluent builder API (method chaining) to configure security policies:
authorizeRequests(): Starts the process of defining access restrictions.antMatchers("/path"): Matches specific URL patterns. You can use wildcards like**(matches zero or more directories).authenticated(): Specifies that the matched URLs require authentication.permitAll(): Specifies that the matched URLs require no authentication.and(): A utility method to chain different configuration blocks together, returning the builder to the parent context (e.g., moving back to thehttplevel to configurehttpBasic()).
6. Stateful vs. Stateless: Session Management & CSRF Protection
When building REST APIs, understanding the difference between stateful and stateless architectures is critical.
Stateful vs. Stateless Authentication
Cross-Site Request Forgery (CSRF)
CSRF is an attack where a malicious website tricks a user's browser into performing an unwanted action on a trusted site where the user is currently authenticated.
Real-Life Analogy: The Malicious Website
- You log into your online banking website. The bank creates a session and stores a session cookie in your browser.
- In another tab, you visit a malicious site.
- The malicious site contains a hidden form that submits a POST request to
bank.com/transfer?to=attacker&amount=10000.- Your browser automatically attaches your active banking session cookie to this request.
- The bank server sees the valid cookie and processes the transfer, thinking you initiated it.
Why We Disable CSRF for Stateless REST APIs
By default, Spring Security enables CSRF protection. It expects a unique, synchronized CSRF token in every mutating request (POST, PUT, DELETE).
If we are building a stateless REST API (where clients are mobile apps, React frontends, or other servers that do not rely on browser session cookies), CSRF attacks are not possible because there are no session cookies to exploit. Therefore, we disable CSRF and configure the session creation policy to STATELESS:
http.csrf().disable()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
7. Database-Backed Authentication: The 4-Step Architecture
Instead of using a single hardcoded user, we want to authenticate users against credentials stored in our MongoDB database. This requires a 4-step architectural implementation.
Step 1: Update the User Entity
We must ensure our User entity has fields for username, password, and a list of roles (for authorization).
package com.journal.entity;
import org.bson.types.ObjectId;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.index.Indexed;
import org.springframework.data.mongodb.core.mapping.Document;
import lombok.Data;
import lombok.NonNull;
import java.util.List;
@Document(collection = "users")
@Data
public class User {
@Id
private ObjectId id;
@Indexed(unique = true)
@NonNull
private String username;
@NonNull
private String password;
private List<String> roles; // e.g., ["USER", "ADMIN"]
}
Step 2: Create the User Repository
We need a query method to find a user by their unique username.
package com.journal.repository;
import com.journal.entity.User;
import org.bson.types.ObjectId;
import org.springframework.data.mongodb.repository.MongoRepository;
import java.util.Optional;
public interface UserRepository extends MongoRepository<User, ObjectId> {
Optional<User> findByUsername(String username);
}
Step 3: Implement UserDetailsService
UserDetailsService is a core Spring Security interface. We implement it to load user-specific data during authentication.
package com.journal.service;
import com.journal.entity.User;
import com.journal.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.Component;
import java.util.Optional;
@Component
public class CustomUserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Optional<User> userVar = userRepository.findByUsername(username);
if (userVar.isPresent()) {
User user = userVar.get();
// Map our custom User entity to Spring Security's UserDetails object
return org.springframework.security.core.userdetails.User.builder()
.username(user.getUsername())
.password(user.getPassword()) // Must be hashed in DB
.roles(user.getRoles().toArray(new String[0]))
.build();
}
throw new UsernameNotFoundException("User not found with username: " + username);
}
}
Step 4: Integrate with Security Configuration
We wire our custom UserDetailsService and a PasswordEncoder bean into our security configuration class.
package com.journal.config;
import com.journal.service.CustomUserDetailsServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
@EnableWebSecurity
public class SpringSecurity extends WebSecurityConfigurerAdapter {
@Autowired
private CustomUserDetailsServiceImpl userDetailsService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/journal/**", "/user/**").authenticated()
.anyRequest().permitAll()
.and()
.httpBasic();
// Configure stateless session management and disable CSRF
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.csrf().disable();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// Bind our custom UserDetailsService and PasswordEncoder
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
8. Password Hashing with BCrypt
Storing passwords in plain text inside a database is a critical security vulnerability. If an attacker gains unauthorized access to the database, all user accounts are compromised.
Why BCrypt?
BCryptPasswordEncoder uses a strong, one-way hashing algorithm (BCrypt) that incorporates a random salt.
- Salt: A random string added to the password before hashing. This ensures that even if two users have the exact same password, their database hashes will be completely different.
- One-Way: It is computationally infeasible to reverse-engineer the original password from the hash.
Saving Encoded Passwords
When registering or creating a new user, we must encode the plain text password before saving it to MongoDB.
package com.journal.service;
import com.journal.entity.User;
import com.journal.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.Arrays;
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private PasswordEncoder passwordEncoder;
public void saveNewUser(User user) {
// 1. Encode the plain text password
String encodedPassword = passwordEncoder.encode(user.getPassword());
user.setPassword(encodedPassword);
// 2. Assign default roles
user.setRoles(Arrays.asList("USER"));
// 3. Save to database
userRepository.save(user);
}
}
9. Refactoring Controllers: Retrieving Authenticated User from Security Context
Now that our endpoints are secured, we no longer need to pass the username as a path parameter. Spring Security automatically populates the Security Context with the authenticated user's details once they pass basic authentication.
The Security Context Holder
The SecurityContextHolder is where Spring Security stores the details of who is authenticated. We can access the currently logged-in user's username from anywhere in our application code using:
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String username = authentication.getName();
Refactoring the User Controller
We will refactor the UserController to remove path parameters for updates and deletions. Users can now only update or delete their own account based on their authenticated session.
package com.journal.controller;
import com.journal.entity.User;
import com.journal.repository.UserRepository;
import com.journal.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@Autowired
private UserRepository userRepository;
@PutMapping
public ResponseEntity<?> updateUser(@RequestBody User user) {
// Extract authenticated username from Security Context
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String username = authentication.getName();
User userInDb = userRepository.findByUsername(username).orElse(null);
if (userInDb != null) {
userInDb.setUsername(user.getUsername());
userInDb.setPassword(user.getPassword()); // Will be encoded in service layer
userService.saveNewUser(userInDb);
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
@DeleteMapping
public ResponseEntity<?> deleteUser() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String username = authentication.getName();
userRepository.deleteByUsername(username);
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
}
Creating a Public Controller
For endpoints that must remain accessible to unauthenticated users (such as user registration or health checks), we create a separate PublicController mapped to /public.
package com.journal.controller;
import com.journal.entity.User;
import com.journal.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/public")
public class PublicController {
@Autowired
private UserService userService;
@GetMapping("/health-check")
public String healthCheck() {
return "OK";
}
@PostMapping("/create-user")
public void createUser(@RequestBody User user) {
userService.saveNewUser(user);
}
}
10. Complete Request Flow Walkthrough
Here is the complete step-by-step execution flow when a client hits a secured endpoint:
11. Summary and Key Takeaways
- Spring Security provides robust, enterprise-grade Authentication (verifying identity) and Authorization (verifying permissions) out of the box.
- HTTP Basic Authentication transmits credentials in the
Authorizationheader as a Base64-encoded string (username:password). It must always be paired with HTTPS in production. @EnableWebSecurityenables web security support and allows developers to write custom configurations by overriding methods in a configuration class.- CSRF Protection should be disabled for stateless REST APIs that do not use browser session cookies to prevent unnecessary
403 Forbiddenerrors on mutating requests (POST, PUT, DELETE). - Stateless Session Policy (
SessionCreationPolicy.STATELESS) ensures that the server does not store session state or createJSESSIONIDcookies, making the API highly scalable. UserDetailsServiceis implemented to fetch user credentials from custom databases (like MongoDB) and map them to Spring Security's internalUserDetailsmodel.- BCrypt Hashing is a secure, salted, one-way hashing algorithm used to store passwords safely. Plain text passwords should never be saved to a database.
SecurityContextHolderallows developers to securely retrieve the currently authenticated user's details directly in the controller layer, eliminating the need for insecure path parameters.