Build a Secure Ecommerce Login System
Build a Spring Boot login system with BCrypt, Redis sessions, and Spring Security.
Introduction
30 Second Summary
Every time you log into an online store, there's an entire architecture working behind the scenes to keep your password safe, your session alive, and your account locked to you alone. That invisible machinery is what separates a toy demo from a production-grade app.
In this project, you will build a complete ecommerce login system using Spring Boot, Spring Security, MySQL, and Redis. You will experience firsthand why senior developers make each architectural decision by hitting the real problems those decisions solve.
What You'll Build
By the end, you will have a working storefront where you can register a new account, log in, visit a protected account page, restart the entire server, and still be logged in when you come back.
By the end of this project, you'll have:
- A product catalog page anyone can browse without logging in, rendered with Thymeleaf and backed by sample data seeded on startup.
- A registration and login flow where passwords are hashed with BCrypt and stored in MySQL. You can query the database and see the hash yourself.
- A protected account page that rejects unauthenticated visitors, with sessions persisted in Redis so logins survive server restarts.
- Secret Mission: Add role-based access control with an admin-only dashboard that returns 403 Forbidden to regular customers.
Are there any prerequisites?
This project assumes basic Java knowledge and that you already have JDK 21, MySQL, and Docker Desktop installed on your machine. Step 1 will walk you through verifying everything is ready.
Before We Start
Before we dive in, let's lock in what you're about to build and why it matters.
This project isn't just about getting login to work. You're going to build authentication the way senior developers do it in production, understanding the real problems each piece solves along the way.
Set Up Your Spring Boot Project
A secure login system requires a stack of components working together: a web framework to handle requests, a security framework to protect pages, a database to store users, and a session store to remember who is logged in.
Getting these dependencies wired together correctly from the start prevents cascading errors later. In this step, you will generate a Spring Boot project with all the right pieces, start a Redis container for session storage, and confirm the app boots cleanly.
In this step, get ready to:
- Generate a Spring Boot project with the required dependencies at start.spring.io.
- Start Redis via Docker and configure your database connections.
- Run the application and confirm Spring Security's default login page appears.
Generate your Spring Boot project
Spring Initializr is a web tool that generates pre-configured Spring Boot projects. You will use it to create a project with all the dependencies your auth system needs.
- Open your browser and navigate to https://start.spring.io.
- Set **Project** to Maven.
- Set Language to Java.
- Set Spring Boot to 3.5.16.
- Under Project Metadata, set Group to com.ecommerce.
- Set Artifact to shop.
- Set Description to Ecommerce shop with secure login.
- Set Java version to 21.
Now add the dependencies your authentication system requires.
- Click the ADD DEPENDENCIES button (or press Ctrl+B).
- Search for and add each of these dependencies one by one: Spring Web, Spring Security, Spring Data JPA, MySQL Driver, Spring Session Data Redis, Spring Data Redis, and Thymeleaf.
- Click GENERATE to download the project as a zip file.
What do these dependencies do?
Spring Web lets your app handle HTTP requests. Spring Security protects pages and handles login. Spring Data JPA talks to your MySQL database.
Spring Session Data Redis and Spring Data Redis store user sessions in Redis so they survive server restarts. Thymeleaf renders HTML templates with dynamic data. MySQL Driver connects Java to your MySQL database.
- Extract the downloaded shop.zip file to your Desktop.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type VS Code and press Enter to open it.
- In VS Code, click File in the top menu bar.
- Click Open Folder.
- Navigate to and select the shop folder you extracted on your Desktop, then click Open.
You should see the project structure in the VS Code explorer panel, including pom.xml, the src folder, and the Maven wrapper files (mvnw and mvnw.cmd).
Don't see the project files?
Make sure you opened the inner shop folder (the one containing pom.xml), not the outer zip or an empty parent folder.
help me find and open my Spring Boot project in VS Code
Start Redis and configure the database connections
Your app needs two backing services: MySQL to store user accounts and Redis to store login sessions. MySQL is already installed on your machine. Redis will run inside a Docker container.
- Open a terminal in VS Code by pressing Ctrl+` (backtick key, top-left of your keyboard).
- Start a Redis container by running this command:
docker run -d --name redis -p 6379:6379 redis
What does this command do?
- docker run creates and starts a new container.
- -d runs it in the background so your terminal stays free.
- --name redis gives the container a friendly name.
- -p 6379:6379 maps the container's Redis port to your machine's port 6379.
- Verify Redis is running by pinging it:
docker exec -it redis redis-cli ping
You should see PONG printed in your terminal. This confirms Redis is ready to accept connections.
Seeing an error about the container name?
If you see a message about the name "redis" already being in use, a container with that name already exists. Remove it first with docker rm -f redis and then re-run the docker run command above.
help me fix my Redis Docker container startup issue
Now create the MySQL database your app will connect to.
- Log into MySQL by running:
mysql -u root -p
- Enter your MySQL root password when prompted.
- Create the database by running:
CREATE DATABASE ecommerce_shop;
- Exit MySQL by typing:
exit;
You should see Query OK, 1 row affected after the CREATE DATABASE command. This confirms your database exists.
Now configure your Spring Boot application to connect to both MySQL and Redis.
- In VS Code's explorer, expand src > main > resources.
- Open application.properties.
- Replace the empty file contents with the following configuration:
# MySQL connection
spring.datasource.url=jdbc:mysql://localhost:3306/ecommerce_shop
spring.datasource.username=root
spring.datasource.password=your-mysql-password-here
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# JPA settings
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect
# Redis connection
spring.data.redis.host=localhost
spring.data.redis.port=6379
# Session management
spring.session.store-type=redis
spring.session.redis.namespace=spring:session
server.servlet.session.timeout=30m
What does this configuration do?
- The MySQL section tells Spring how to connect to your local database. Replace your-mysql-password-here with your actual MySQL root password.
- ddl-auto=update tells JPA to auto-create database tables based on your Java classes.
- The Redis section connects to the container you just started on port 6379.
- spring.session.store-type=redis tells Spring to store login sessions in Redis instead of server memory.
- Replace your-mysql-password-here with your actual MySQL root password.
- Save the file.
Not sure what your MySQL password is?
This is the password you set when you first installed MySQL. If you never set one, try leaving the value as an empty string. If you use a different MySQL username, update the spring.datasource.username line as well.
help me find or reset my MySQL root password
✔️ Awesome, I've got everything!
Great. Make sure you have saved application.properties with your real MySQL password filled in.
ⓧ I'd like to double check the full code
Your application.properties should look exactly like this (with your own password substituted):
spring.datasource.url=jdbc:mysql://localhost:3306/ecommerce_shop
spring.datasource.username=root
spring.datasource.password=your-mysql-password-here
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect
spring.data.redis.host=localhost
spring.data.redis.port=6379
spring.session.store-type=redis
spring.session.redis.namespace=spring:session
server.servlet.session.timeout=30m
Run the application
Everything is wired up. Time to boot the app and see what Spring Security does out of the box.
Before you run it, take a guess: what do you think you will see when you open localhost:8080 in your browser? A blank page? An error? Something else?
macOS
- In your VS Code terminal, make the Maven wrapper executable by running:
chmod +x mvnw
- Start the application by running:
./mvnw spring-boot:run
Windows
- In your VS Code terminal, start the application by running:
.\mvnw spring-boot:run
The first run downloads dependencies, so it may take a minute or two. When you see a line containing Started ShopApplication in the terminal output, your app is running.
- Open your browser and navigate to http://localhost:8080.
You see a login page with a username and password field. You did not build this page. Spring Security generated it automatically because it locks down every URL by default.
Why does a login page appear?
Spring Security follows a "secure by default" philosophy. The moment you add the Spring Security dependency, every endpoint requires authentication.
The framework auto-generates a login page so something is there to greet unauthenticated users. In the next step, you will replace this with a public product catalog page and your own custom login form.
App won't start?
- If you see a connection refused error mentioning MySQL, double-check that your MySQL service is running and that the password in application.properties matches your actual root password.
- If you see a connection refused error mentioning Redis, confirm your Docker container is running with docker ps. You should see the redis container listed.
- On macOS, if you see "Permission denied" when running ./mvnw, make sure you ran chmod +x mvnw first.
help me debug my Spring Boot application startup error
Your project is fully wired up with all the dependencies, a running Redis session store, and a connected MySQL database. Next up, you will build a product listing page that anyone can browse without logging in.
Build the Product Listing Page
In the previous step, you confirmed your Spring Boot app starts and Spring Security's default login page appears. Now it's time to build something visible.
Before adding any authentication logic, you need a page worth protecting. In this step, you'll create a product catalog that anyone can browse without logging in. This gives you a working storefront immediately and sets up the contrast when Spring Security locks down other pages later.
In this step, get ready to:
- Create a Product entity and data layer that seeds sample products into MySQL.
- Build a controller and Thymeleaf template to display products in the browser.
- Configure Spring Security to allow public access to the products page.
Create the Product entity and repository
Every ecommerce site needs products. You'll define a Product class that maps to a MySQL table, then create a repository interface so Spring Data can query it.
- In VS Code, create a new folder path: src/main/java/com/ecommerce/shop/model.
- Inside that folder, create a new file called Product.java and paste the following entity structure:
package com.ecommerce.shop.model;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
@Entity
@Table(name = "products")
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String description;
private double price;
// No-arg constructor required by JPA
public Product() {}
// Constructor for creating new products
public Product(String name, String description, double price) {
this.name = name;
this.description = description;
this.price = price;
}
What does this code do?
The @Entity annotation tells Hibernate this class maps to a database table. @Table(name = "products") sets the table name explicitly.
The @Id and @GeneratedValue annotations mark the primary key and tell MySQL to auto-increment it. JPA requires a no-arg constructor to create instances when reading from the database.
- Still in Product.java, add the getter and setter methods below the constructor to complete the class:
// Getters and setters for JPA and Thymeleaf to access fields
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; }
public double getPrice() { return price; }
public void setPrice(double price) { this.price = price; }
}
Why are getters and setters needed?
JPA uses these methods to read and write field values when loading from or saving to the database. Thymeleaf also uses them to display product data in your HTML templates.
- Save Product.java.
✔️ Awesome, I've got everything!
Great. Make sure you've saved Product.java with both code blocks combined into one file.
ⓧ I'd like to double check the full code
Your Product.java should have two sections: the entity structure (package, imports, class with fields and constructors) followed by the getters and setters. Scroll up and confirm both code blocks are present in order, with the closing } brace at the very end.
Next, you need a repository interface so Spring Data knows how to query your products table.
- Create a new folder path: src/main/java/com/ecommerce/shop/repository.
- Inside that folder, create ProductRepository.java with the following code:
package com.ecommerce.shop.repository;
import com.ecommerce.shop.model.Product;
import org.springframework.data.jpa.repository.JpaRepository;
// Spring Data auto-generates CRUD methods (findAll, save, count, etc.)
public interface ProductRepository extends JpaRepository<Product, Long> {
}
What does this code do?
By extending JpaRepository, Spring Data automatically provides methods like findAll(), save(), and count() without you writing any implementation code. You just declare the interface and Spring handles the rest.
- Save ProductRepository.java.
Now verify that Hibernate picks up your entity and creates the products table.
- Switch back to the terminal where the app is running.
- Press Ctrl+C to stop the running application.
- Restart the app by running:
.\mvnw spring-boot:run
In the console output, look for a line containing Hibernate: create table products. This confirms JPA scanned your entity and created the table in MySQL.
Don't see the create table output?
- If the table already exists from a previous run, Hibernate won't recreate it (since ddl-auto=update only adds new structures). Check that the app starts without errors.
- If you see a connection error, confirm MySQL is running and your password in application.properties is correct.
Still stuck? Help me debug my Spring Boot startup error with Hibernate and MySQL
Seed sample products on startup
An empty product table is not useful for testing. You'll create a data initializer that seeds sample products the first time the app starts.
- Stop the running app with Ctrl+C.
- Create a new folder path: src/main/java/com/ecommerce/shop/config.
- Inside that folder, create DataInitializer.java with the following code:
package com.ecommerce.shop.config;
import com.ecommerce.shop.model.Product;
import com.ecommerce.shop.repository.ProductRepository;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
@Component
public class DataInitializer implements CommandLineRunner {
private final ProductRepository productRepository;
public DataInitializer(ProductRepository productRepository) {
this.productRepository = productRepository;
}
@Override
public void run(String... args) {
// Only seed if the table is empty
if (productRepository.count() == 0) {
productRepository.save(new Product("Wireless Headphones", "Noise-cancelling over-ear headphones", 79.99));
productRepository.save(new Product("Mechanical Keyboard", "RGB backlit with Cherry MX switches", 129.99));
productRepository.save(new Product("USB-C Hub", "7-in-1 adapter for laptops", 49.99));
productRepository.save(new Product("Webcam HD", "1080p with built-in microphone", 59.99));
}
}
}
What does this code do?
The @Component annotation registers this class as a Spring bean. Implementing CommandLineRunner means the run() method executes automatically every time the app starts.
The if (productRepository.count() == 0) check prevents duplicate data on repeated restarts. It only seeds products when the table is empty.
- Save DataInitializer.java and restart the app by running:
.\mvnw spring-boot:run
In the console output, you should now see four Hibernate: insert into products statements. This proves your seed data is being written to MySQL.
Not seeing insert statements?
- If you previously ran the app and products already exist in the table, the count() == 0 check skips the seeding. That's expected behavior.
- If you see a compilation error, make sure your Product import matches the package com.ecommerce.shop.model.Product.
Need more help? Help me debug why my DataInitializer isn't inserting products
Build the product controller and template
Your products exist in MySQL but there's no way to see them in the browser yet. You need a controller to fetch products and a Thymeleaf template to render them as HTML.
- Stop the running app with Ctrl+C.
- Create a new folder path: src/main/java/com/ecommerce/shop/controller.
- Inside that folder, create ProductController.java with the following code:
package com.ecommerce.shop.controller;
import com.ecommerce.shop.repository.ProductRepository;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class ProductController {
private final ProductRepository productRepository;
// Spring injects the repository automatically
public ProductController(ProductRepository productRepository) {
this.productRepository = productRepository;
}
// Maps GET /products to the products.html template
@GetMapping("/products")
public String products(Model model) {
model.addAttribute("products", productRepository.findAll());
return "products";
}
}
What does this code do?
The @Controller annotation tells Spring this class handles web requests. @GetMapping("/products") maps the URL path /products to this method.
The method fetches all products from the database and adds them to the Model object. Returning the string "products" tells Thymeleaf to render the products.html template.
- Save ProductController.java.
Now create the HTML template that renders the product data.
- In VS Code, confirm the folder src/main/resources/templates already exists (Spring Boot created it during project generation).
- Create a new file called products.html inside that folder and paste the page structure:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Our Products</title>
<style>
body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }
table { width: 100%; border-collapse: collapse; margin-top: 20px; }
th, td { border: 1px solid #ddd; padding: 12px; text-align: left; }
th { background-color: #333; color: white; }
tr:nth-child(even) { background-color: #f2f2f2; }
.nav { margin-bottom: 20px; }
.nav a { margin-right: 15px; text-decoration: none; color: #333; }
</style>
</head>
<body>
<div class="nav">
<a href="/products">Products</a>
<a href="/account">My Account</a>
<a href="/login">Login</a>
<a href="/register">Register</a>
</div>
<h1>Product Catalog</h1>
What does this code do?
The xmlns:th attribute enables Thymeleaf processing in this HTML file. The inline <style> block adds basic table formatting.
The navigation links point to pages you'll build in later steps (/account, /login, /register). They won't work yet, but having them here saves you from editing this file again later.
- Still in products.html, add the product table below the heading to complete the file:
<table>
<thead>
<tr>
<th>Name</th>
<th>Description</th>
<th>Price</th>
</tr>
</thead>
<tbody>
<tr th:each="product : ${products}">
<td th:text="${product.name}">Product Name</td>
<td th:text="${product.description}">Description</td>
<td th:text="'$' + ${product.price}">$0.00</td>
</tr>
</tbody>
</table>
</body>
</html>
What does this code do?
The th:each attribute loops over the list of products passed from the controller. For each product, Thymeleaf renders a table row with the name, description, and price.
The static text like "Product Name" is placeholder content that only shows if Thymeleaf processing is disabled. When the app runs, th:text replaces it with real data from MySQL.
- Save products.html.
✔️ Awesome, I've got everything!
Great. Make sure products.html contains both the page structure and the table combined into one file.
ⓧ I'd like to double check the full code
Your products.html should have the <head> section with styles, then the <body> with a nav div, an <h1> heading, and the <table> element using th:each to loop over products. Scroll up and confirm both code blocks are present in order.
Your controller and template are ready, but if you try to browse /products right now, Spring Security will redirect you to a login page. That's because Spring Security blocks all URLs by default.
Configure Spring Security to allow public access
Spring Security's default behavior is to lock down every URL. You need a SecurityFilterChain bean that explicitly permits public pages and configures how login works.
- Inside the src/main/java/com/ecommerce/shop/config folder (same folder as DataInitializer.java), create SecurityConfig.java with the following class structure:
package com.ecommerce.shop.config;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
// BCrypt encoder for hashing passwords (used in later steps)
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
What does this code do?
The @Configuration and @EnableWebSecurity annotations tell Spring this class contains security configuration. The PasswordEncoder bean uses BCrypt to hash passwords. You'll use it when building registration in a later step.
- Now add the SecurityFilterChain bean above the passwordEncoder method. This is the core security rule that controls which pages require login:
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
// Public pages - no login required
.requestMatchers("/products", "/register", "/css/**").permitAll()
// Everything else requires authentication
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.defaultSuccessUrl("/account", true)
.permitAll()
)
.logout(logout -> logout
.logoutSuccessUrl("/products")
.invalidateHttpSession(true)
.deleteCookies("SESSION")
);
return http.build();
}
What does this code do?
The requestMatchers("/products", "/register", "/css/**").permitAll() line makes those URLs accessible without logging in. The anyRequest().authenticated() line locks everything else behind a login.
The formLogin block points to a custom login page at /login (you'll build this in Step 4). The logout block configures where users go after logging out and ensures the session cookie is cleaned up.
Why requestMatchers ordering matters
Spring Security evaluates rules top to bottom and uses the first match. The .permitAll() rules must come before .anyRequest().authenticated(). If you put the catch-all first, it would block everything regardless of the rules below it.
- Save SecurityConfig.java.
✔️ Awesome, I've got everything!
Great. Make sure the securityFilterChain method appears above the passwordEncoder method inside the class.
ⓧ I'd like to double check the full code
Your SecurityConfig.java should have the class structure (with @Configuration and @EnableWebSecurity) containing two beans: securityFilterChain first, then passwordEncoder. Scroll up and confirm both code blocks are present in order.
Time to see it all come together.
- Restart the app by running:
.\mvnw spring-boot:run
Before you open the browser, think about what you expect to see at localhost:8080/products. Will you get the login page again, or something different?
- Open your browser and navigate to localhost:8080/products.
You should see a product catalog table with four items: Wireless Headphones ($79.99), Mechanical Keyboard ($129.99), USB-C Hub ($49.99), and Webcam HD ($59.99). No login was required because your SecurityConfig permits /products for everyone.
Still seeing a login page?
- Make sure SecurityConfig.java is saved and the requestMatchers("/products", "/register", "/css/**").permitAll() line is present.
- Confirm you fully stopped the previous app process before restarting. Spring Boot does not hot-reload without devtools.
- Check the terminal for compilation errors. A missing import or typo in SecurityConfig prevents the app from starting.
Need more help? Help me debug why my products page still requires login
Your storefront is live and anyone can browse your products. Next up, you'll build a registration system so users can create accounts with securely hashed passwords.
Add User Registration with BCrypt
Your storefront is live, but right now anyone can browse products without ever identifying themselves. Before you can add login, you need actual users stored in your database.
This step teaches you how senior developers handle password storage. Storing plaintext passwords is a career-ending mistake in production. You will use BCrypt adaptive hashing to protect your users' credentials.
In this step, get ready to:
- Create a User entity and repository so JPA builds a users table in MySQL.
- Build an AuthController that handles registration and login page routing.
- Create registration and login templates with CSRF-protected forms.
Create the User entity and repository
Your app needs a User class that maps to a MySQL table. Each user has an email, a hashed password, and a role.
- In VS Code, create a new file at src/main/java/com/ecommerce/shop/model/User.java.
- Add the entity definition with its fields by pasting this code:
package com.ecommerce.shop.model;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
// Maps this class to the "users" table in MySQL
@Entity
@Table(name = "users")
public class User {
// Auto-generated primary key
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// Email must be unique across all users
@Column(unique = true, nullable = false)
private String email;
// Stores the BCrypt-hashed password
@Column(nullable = false)
private String password;
// Role determines access level (CUSTOMER, ADMIN)
@Column(nullable = false)
private String role;
What does this code do?
The @Entity annotation tells JPA this class represents a database table. The @Table(name = "users") annotation names the actual MySQL table.
The @Column(unique = true) on email ensures no two users can register with the same address. The password field stores the hashed version, never the raw password the user types.
- Below the fields (still inside the class), add the constructors and accessor methods:
// No-arg constructor required by JPA
public User() {}
public User(String email, String password, String role) {
this.email = email;
this.password = password;
this.role = role;
}
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
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 getRole() { return role; }
public void setRole(String role) { this.role = role; }
}
What does this code do?
The no-argument constructor is required by JPA so Hibernate can instantiate the entity when loading from the database. The parameterized constructor lets your code create new users with a single line.
Getters and setters give JPA and your controllers access to the fields.
- Save User.java.
✔️ Awesome, I've got everything!
Great. Make sure you have saved the file before moving on.
ⓧ I'd like to double check the full code
Your User.java file should contain both chunks above in order: the package and imports, entity declaration with fields, then the constructors and getters/setters, all inside the User class. Scroll up and confirm both code blocks are present with the closing } brace at the end.
Now create the repository interface that lets Spring query the users table.
- Create a new file at src/main/java/com/ecommerce/shop/repository/UserRepository.java and paste this code:
package com.ecommerce.shop.repository;
import com.ecommerce.shop.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
// Spring Data JPA generates the implementation at runtime
public interface UserRepository extends JpaRepository<User, Long> {
// Custom query method to find a user by their email address
Optional<User> findByEmail(String email);
}
What does this code do?
Extending JpaRepository gives you save(), findAll(), findById(), and more without writing any SQL.
The findByEmail method is a custom query. Spring automatically generates the SQL from the method name. It returns Optional<User> so you can safely check if a user exists before acting on it.
- Save UserRepository.java.
What about the PasswordEncoder?
Your SecurityConfig.java already has a passwordEncoder() bean that returns a BCryptPasswordEncoder. You defined this in the previous step. This bean will be injected into the controller you build next.
Time to verify that JPA creates the users table automatically.
- Stop the running app with Ctrl+C in your terminal.
- Restart the app by running:
.\mvnw spring-boot:run
- Once the app starts, open a new terminal and verify the table was created by running:
mysql -u root -p -e "SHOW TABLES;" ecommerce_shop
- You should see both products and users listed in the output.
Don't see the users table?
- Check that spring.jpa.hibernate.ddl-auto=update is set in your application.properties. This tells Hibernate to create missing tables on startup.
- Look at the Spring Boot startup output for errors. A typo in the @Table or @Column annotation can prevent the table from being created.
- Make sure MySQL is running and the ecommerce_shop database exists.
Why isn't my users table being created?
Create the AuthController and registration template
You need a controller to handle the registration form display and a template for the form itself. Together, these let users visit /register and see a working sign-up page.
- Create a new file at src/main/java/com/ecommerce/shop/controller/AuthController.java and paste this code:
package com.ecommerce.shop.controller;
import com.ecommerce.shop.repository.UserRepository;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class AuthController {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
// Spring injects both beans via constructor injection
public AuthController(UserRepository userRepository, PasswordEncoder passwordEncoder) {
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
}
// Displays the registration form
@GetMapping("/register")
public String showRegistrationForm() {
return "register";
}
// Displays the login page
@GetMapping("/login")
public String showLoginForm() {
return "login";
}
}
What does this code do?
This controller has two GET endpoints. The @GetMapping("/register") returns the name of a Thymeleaf template (register.html). The @GetMapping("/login") does the same for the login page.
Spring injects the UserRepository and PasswordEncoder through the constructor. You will use them when you add the POST registration logic shortly.
- Save AuthController.java.
- Now create the registration template at src/main/resources/templates/register.html and paste this code:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Register</title>
<style>
body { font-family: Arial, sans-serif; max-width: 400px; margin: 0 auto; padding: 20px; }
form { display: flex; flex-direction: column; gap: 15px; }
input { padding: 10px; border: 1px solid #ddd; border-radius: 4px; }
button { padding: 12px; background-color: #333; color: white; border: none; border-radius: 4px; cursor: pointer; }
.error { color: red; margin-bottom: 10px; }
a { color: #333; }
</style>
</head>
<body>
<h1>Create Account</h1>
<p class="error" th:if="${error}" th:text="${error}"></p>
<!-- th:action generates the CSRF token automatically -->
<form th:action="@{/register}" method="post">
<input type="email" name="email" placeholder="Email" required>
<input type="password" name="password" placeholder="Password" required>
<input type="password" name="confirmPassword" placeholder="Confirm Password" required>
<button type="submit">Register</button>
</form>
<p>Already have an account? <a href="/login">Login here</a></p>
</body>
</html>
What does th:action do for security?
When you use th:action="@{/register}", Thymeleaf automatically includes a hidden CSRF token in the form. This protects against cross-site request forgery attacks where a malicious site tricks the browser into submitting forms on behalf of the user.
Spring Security enables CSRF protection by default. Without the token, form submissions would be rejected with a 403 error.
- Save register.html.
- Stop the app with Ctrl+C and restart it:
.\mvnw spring-boot:run
- Navigate to localhost:8080/register in your browser.
- You should see a clean registration form with Email, Password, and Confirm Password fields.
Seeing a Whitelabel Error page?
- Make sure register.html is saved in the src/main/resources/templates/ folder, not in a subfolder or at the project root.
- Check that the method in AuthController returns the string "register" (the template name without .html).
Why am I getting a Whitelabel Error when visiting /register?
Add the registration logic and verify password hashing
The form displays, but submitting it does nothing yet. You need the POST handler that validates input, hashes the password, and saves the user.
- Create the login template first (the registration redirects to it after success). Create a new file at src/main/resources/templates/login.html and paste:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Login</title>
<style>
body { font-family: Arial, sans-serif; max-width: 400px; margin: 0 auto; padding: 20px; }
form { display: flex; flex-direction: column; gap: 15px; }
input { padding: 10px; border: 1px solid #ddd; border-radius: 4px; }
button { padding: 12px; background-color: #333; color: white; border: none; border-radius: 4px; cursor: pointer; }
.error { color: red; margin-bottom: 10px; }
.success { color: green; margin-bottom: 10px; }
a { color: #333; }
</style>
</head>
<body>
<h1>Login</h1>
<p class="error" th:if="${param.error}">Invalid email or password</p>
<p class="success" th:if="${param.registered}">Registration successful! Please log in.</p>
<!-- name="username" is required by Spring Security's filter -->
<form th:action="@{/login}" method="post">
<input type="email" name="username" placeholder="Email" required>
<input type="password" name="password" placeholder="Password" required>
<button type="submit">Login</button>
</form>
<p>No account? <a href="/register">Register here</a></p>
</body>
</html>
Why is the email field named username?
Spring Security's authentication filter expects form fields named username and password by default. Even though your users log in with email, the input must be named username for the framework to pick it up.
- Save login.html.
- Now open AuthController.java. Find the import lines at the top of the file and add these additional imports below them:
import com.ecommerce.shop.model.User;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
- Now add the POST method inside the class. Find the showLoginForm() method and add this new method directly above it:
// Handles the registration form submission
@PostMapping("/register")
public String registerUser(@RequestParam String email,
@RequestParam String password,
@RequestParam String confirmPassword,
Model model) {
// Check that both password fields match
if (!password.equals(confirmPassword)) {
model.addAttribute("error", "Passwords do not match");
return "register";
}
// Prevent duplicate registrations
if (userRepository.findByEmail(email).isPresent()) {
model.addAttribute("error", "Email already registered");
return "register";
}
// Hash the password with BCrypt before saving
User user = new User(email, passwordEncoder.encode(password), "CUSTOMER");
userRepository.save(user);
return "redirect:/login?registered";
}
What does this code do?
This method handles the form submission from register.html. It validates that passwords match and that the email is not already taken.
The critical line is passwordEncoder.encode(password). This hashes the raw password with BCrypt before storing it. Even if someone steals your database, they cannot reverse the hash back to the original password.
After saving, it redirects to /login?registered so the login page can display a success message.
- Save AuthController.java.
✔️ Awesome, I've got everything!
Great. Make sure you have saved all files before testing.
ⓧ I'd like to double check the full code
Your AuthController.java should have these methods in order inside the class: the constructor, showRegistrationForm(), registerUser(), and showLoginForm(). Scroll up and confirm the initial class shell and the POST method are both present.
Now for the moment of truth. Before you run this, what do you think will be stored in the password column when you register a new user?
- Stop the app with Ctrl+C and restart it:
.\mvnw spring-boot:run
- Navigate to localhost:8080/register in your browser.
- Enter an email address (e.g. test@example.com).
- Enter a password in both password fields.
- Click Register.
- You should be redirected to the login page with a green "Registration successful" message.
- Now verify the password is hashed by running this in a separate terminal:
mysql -u root -p -e "SELECT email, password FROM users;" ecommerce_shop
You should see your email in plaintext, but the password column shows a long BCrypt hash starting with $2a$. That prefix identifies it as a BCrypt hash with the default strength of 10 rounds.
Why BCrypt instead of plain hashing?
Simple hashes like SHA-256 are fast to compute, which makes them fast to crack. BCrypt is intentionally slow. It applies multiple rounds of hashing (the default is 10 log rounds, meaning 2^10 = 1024 iterations).
This adaptive cost means you can increase the rounds as hardware gets faster. Even if an attacker gets your database, cracking a single BCrypt hash takes orders of magnitude longer than cracking a plain SHA-256 hash.
Registration redirects but no user appears in the database?
- Check the Spring Boot console output for SQL errors. A missing users table means the entity annotations have a typo.
- Make sure the @PostMapping("/register") method is inside the AuthController class, not accidentally placed outside the closing brace.
- Confirm your form uses th:action="@{/register}" and method="post". Without th:action, the CSRF token is missing and Spring Security rejects the request.
Why is my registration not saving users to MySQL?
Your users can now register with securely hashed passwords. Next up, you will wire Spring Security's login flow so users can actually authenticate against these stored credentials.
Implement Login and Protected Pages
Your registration system stores users with hashed passwords in MySQL. But registration alone is useless without a way for those users to actually log in.
In this step, you will wire Spring Security's authentication flow to validate credentials against your MySQL database. You will also build a protected account page that only logged-in users can access.
In this step, get ready to:
- Wire Spring Security to authenticate users against your MySQL database.
- Build a protected account page only logged-in users can access.
- Experience the session problem that the next step solves.
Create the UserDetailsService
Spring Security handles login for you, but it needs to know where your users live. You tell it by implementing a UserDetailsService that loads user data from your MySQL database.
- In VS Code, create a new folder service inside src/main/java/com/ecommerce/shop/.
- Create a new file called CustomUserDetailsService.java inside that service folder, and paste the following code:
package com.ecommerce.shop.service;
import com.ecommerce.shop.model.User;
import com.ecommerce.shop.repository.UserRepository;
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;
@Service
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
public CustomUserDetailsService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
User user = userRepository.findByEmail(email)
.orElseThrow(() -> new UsernameNotFoundException("User not found: " + email));
return org.springframework.security.core.userdetails.User.builder()
.username(user.getEmail())
.password(user.getPassword())
.roles(user.getRole())
.build();
}
}
What does this code do?
When someone submits the login form, Spring Security calls loadUserByUsername() with whatever was typed in the username field. Our login form sends the email address in that field.
The method looks up the user in MySQL using findByEmail(). If found, it returns a Spring Security UserDetails object containing the email, hashed password, and role. Spring Security then compares the submitted password (after BCrypt hashing) against the stored hash automatically.
Why is the parameter called email but the method says Username?
Spring Security's form login filter always sends the form field named username to loadUserByUsername(). Our login form names the email field username (check login.html) so that Spring Security picks it up correctly.
- Save CustomUserDetailsService.java.
- Stop the running app with Ctrl+C in your terminal, then restart it by running:
.\mvnw spring-boot:run
- Navigate to localhost:8080/login in your browser.
- Enter the email and password you registered in the previous step, then click Login.
You will see a Whitelabel Error Page. That is expected. The login succeeded (Spring Security validated your credentials against MySQL), but the redirect destination /account does not have a controller or template yet.
Seeing 'Bad credentials' instead?
- Make sure you registered a user in the previous step. If you dropped and recreated the database, you need to register again.
- Confirm the email in your login form matches exactly what you registered with (case-sensitive).
- Check that CustomUserDetailsService.java is inside the com.ecommerce.shop.service package. Spring's component scan needs it under the base package to find the @Service annotation.
Still stuck? Why is Spring Security rejecting my login credentials?
✔️ Awesome, I've got everything!
Great. Make sure you saved CustomUserDetailsService.java before moving on.
ⓧ I'd like to double check the full code
package com.ecommerce.shop.service;
import com.ecommerce.shop.model.User;
import com.ecommerce.shop.repository.UserRepository;
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;
@Service
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
public CustomUserDetailsService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
User user = userRepository.findByEmail(email)
.orElseThrow(() -> new UsernameNotFoundException("User not found: " + email));
return org.springframework.security.core.userdetails.User.builder()
.username(user.getEmail())
.password(user.getPassword())
.roles(user.getRole())
.build();
}
}
Build the protected account page
Login works, but it has nowhere meaningful to land. You need a controller that reads the authenticated user's identity and a template to display it.
- Create a new file called AccountController.java inside src/main/java/com/ecommerce/shop/controller/ and paste the following code:
package com.ecommerce.shop.controller;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class AccountController {
@GetMapping("/account")
public String account(@AuthenticationPrincipal UserDetails userDetails, Model model) {
// Read the logged-in user's email from the security context
model.addAttribute("email", userDetails.getUsername());
return "account";
}
}
What does @AuthenticationPrincipal do?
Spring Security stores the authenticated user in a security context after login. The @AuthenticationPrincipal annotation injects that user directly into your controller method. You do not need to manually query the database again.
The getUsername() call returns the email because that is what you set as username in your CustomUserDetailsService.
✔️ Awesome, I've got everything!
Great. Make sure you saved AccountController.java.
ⓧ I'd like to double check the full code
package com.ecommerce.shop.controller;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class AccountController {
@GetMapping("/account")
public String account(@AuthenticationPrincipal UserDetails userDetails, Model model) {
model.addAttribute("email", userDetails.getUsername());
return "account";
}
}
- Now create the template. Create a new file called account.html inside src/main/resources/templates/ and paste the following code:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>My Account</title>
<style>
body { font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; }
.card { border: 1px solid #ddd; border-radius: 8px; padding: 20px; margin-top: 20px; }
.nav { margin-bottom: 20px; }
.nav a { margin-right: 15px; text-decoration: none; color: #333; }
.logout-form { margin-top: 20px; }
button { padding: 10px 20px; background-color: #dc3545; color: white; border: none; border-radius: 4px; cursor: pointer; }
</style>
</head>
<body>
<div class="nav">
<a href="/products">Products</a>
<a href="/account">My Account</a>
</div>
<h1>My Account</h1>
<div class="card">
<p><strong>Email:</strong> <span th:text="${email}">user@example.com</span></p>
<p><strong>Status:</strong> Authenticated</p>
<form th:action="@{/logout}" method="post" class="logout-form">
<button type="submit">Logout</button>
</form>
</div>
</body>
</html>
What does this template do?
The template displays the authenticated user's email using th:text="${email}", which reads the value your controller added to the model.
The logout button submits a POST request to /logout using th:action="@{/logout}". Thymeleaf automatically includes a CSRF token as a hidden field, which Spring Security requires for POST requests.
- Save account.html.
- Stop the app with Ctrl+C and restart it by running:
.\mvnw spring-boot:run
- Navigate to localhost:8080/login in your browser.
- Enter your registered email and password, then click Login.
You are redirected to /account and see your email displayed under "My Account" with a status of "Authenticated" and a red Logout button.
Still seeing the Whitelabel Error Page?
- Make sure account.html is saved inside src/main/resources/templates/ (not inside a subfolder).
- Confirm AccountController.java returns the string "account" (matching the filename without the .html extension).
- Make sure you fully restarted the app after saving both files.
Still stuck? Help me debug why my account page is not loading after login.
✔️ Awesome, I've got everything!
Great. Make sure you saved account.html before moving on.
ⓧ I'd like to double check the full code
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>My Account</title>
<style>
body { font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; }
.card { border: 1px solid #ddd; border-radius: 8px; padding: 20px; margin-top: 20px; }
.nav { margin-bottom: 20px; }
.nav a { margin-right: 15px; text-decoration: none; color: #333; }
.logout-form { margin-top: 20px; }
button { padding: 10px 20px; background-color: #dc3545; color: white; border: none; border-radius: 4px; cursor: pointer; }
</style>
</head>
<body>
<div class="nav">
<a href="/products">Products</a>
<a href="/account">My Account</a>
</div>
<h1>My Account</h1>
<div class="card">
<p><strong>Email:</strong> <span th:text="${email}">user@example.com</span></p>
<p><strong>Status:</strong> Authenticated</p>
<form th:action="@{/logout}" method="post" class="logout-form">
<button type="submit">Logout</button>
</form>
</div>
</body>
</html>
Experience the session problem
Your login flow works end-to-end. But there is a hidden problem that every production ecommerce site must solve.
Before you run the next test, make a prediction: if you stop the server and restart it, then refresh /account, do you think you will still be logged in?
- Make sure you are still on the localhost:8080/account page showing your email.
- In your terminal, press Ctrl+C to stop the server.
- Restart the server by running:
.\mvnw spring-boot:run
- Go back to your browser and refresh the localhost:8080/account page.
You are kicked back to the login page. Your session was lost because it lived in the server's memory. When the server stopped, the session disappeared with it.
Why does this matter for a real ecommerce site?
Every time you deploy new code, the server restarts. If sessions live in memory, every restart logs out all your users. In production, this means thousands of abandoned shopping carts and frustrated customers.
Senior developers solve this by storing sessions outside the server. That is exactly what you will do in the next step with Redis.
You have proven that login works, but in-memory sessions are fragile. Next up, you will fix this with a single configuration change that moves sessions into Redis.
Survive Restarts with Redis Sessions
In the previous step, you built a working login system. But you also discovered a critical flaw: stopping the server wiped your session and kicked you back to the login page.
In production, every deployment would log out every single user. Senior developers solve this by externalizing sessions to Redis, a fast in-memory data store. Spring Session makes this fix nearly invisible. Your project already has the dependency and configuration in place from the initial setup. In this step, you will prove it works.
In this step, get ready to:
- Confirm that Redis session configuration is already in place.
- Log in, restart the server, and verify your session survives.
- Inspect Redis directly to see where your session data lives.
Confirm your Redis session configuration
Spring Boot auto-configures Redis-backed sessions when two things are true: the spring-session-data-redis dependency is on the classpath, and the spring.session.store-type property tells Spring Boot to use Redis.
- In VS Code, open application.properties and find these three lines:
spring.session.store-type=redis
spring.session.redis.namespace=spring:session
server.servlet.session.timeout=30m
What do these properties do?
- spring.session.store-type=redis tells Spring Boot to store sessions in Redis instead of server memory.
- spring.session.redis.namespace=spring:session sets the key prefix that Spring Session uses when writing to Redis.
- server.servlet.session.timeout=30m keeps sessions alive for 30 minutes of inactivity.
Why externalize sessions to Redis?
When sessions live in server memory, they die whenever the server restarts. Every deployment, every crash, every scaling event logs out all your users.
Redis stores sessions externally so they survive restarts. It also lets multiple server instances share the same session data. This is how production ecommerce sites keep thousands of users logged in across server clusters.
No code changes are needed. The dependency was added in pom.xml during project generation, and the properties were configured in Step 1. Everything is already wired up.
Test session persistence across a restart
Now for the moment of truth. You will log in, kill the server, restart it, and see if your session survives.
- Start your application by running this command in your terminal:
.\mvnw spring-boot:run
- Open your browser and navigate to localhost:8080/login.
- Log in with the email and password you registered in the previous step.
- Confirm you are redirected to /account and see your email displayed.
Good. You are logged in. Now stop the server.
- In your terminal, press Ctrl+C to stop the Spring Boot application.
Before you restart, think about what you expect to happen. When you refresh /account after restarting the server, do you think you will still be logged in, or will you be kicked back to the login page?
- Start the application again by running:
.\mvnw spring-boot:run
- Once the app has started, go back to your browser and refresh localhost:8080/account.
You are still logged in. Your email is still displayed, and you were not redirected to the login page. The session survived the restart because it lives in Redis, not in server memory.
Why did this work when it failed in Step 4?
In Step 4, you experienced the session loss to understand the problem. Now Spring Session is actively routing your session to Redis. When the server restarts, it reconnects to Redis and finds your session data still there. From the browser's perspective, nothing changed.
Still getting redirected to the login page?
- Make sure your Redis container is still running. Run `docker ps` and check for a container named `redis`.
- Verify spring.session.store-type=redis is in your application.properties file. Without this property, Spring Boot defaults to in-memory sessions.
- Check your terminal for Redis connection errors on startup. If you see a connection refused message, restart the Redis container with docker start redis.
Why is my session not surviving restarts even though Redis is configured?
Inspect session data in Redis
You proved the session survives, but where exactly does it live? You can peek inside Redis to see the raw session keys.
- Open a new terminal and query Redis for session keys by running this command:
docker exec -it redis redis-cli KEYS "spring:session:*"
You will see keys like spring:session:sessions:<session-id> in the output. Each key represents an active user session.
What do these Redis keys mean?
The spring:session prefix comes from the namespace you configured in application.properties. Each session ID maps to a hash in Redis that holds the user's authentication state, creation time, and expiry.
This is exactly how production ecommerce sites handle sessions across multiple server instances. Every server reads from the same Redis, so it does not matter which server handles the next request.
Seeing an empty list?
- Make sure you are still logged in at `/account` before running the Redis query. If you logged out, no session exists to find.
- Confirm the namespace matches. Your application.properties must have spring.session.redis.namespace=spring:session for the KEYS query to find anything.
Why is my Redis KEYS query returning empty even though I'm logged in?
Secret mission
Add Role-Based Access Control
Real ecommerce sites have different user types. Customers browse products, admins manage inventory. Can you extend your SecurityFilterChain with role-based rules to gate an admin-only dashboard by role?
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally, so there are no ongoing cloud costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- Docker container redis running on port 6379.
- MySQL database ecommerce_shop (local).
- Spring Boot project folder shop with all source code.
Keep everything running
No action needed. Choose this if you are still actively building on this project or plan to extend it further.
- Your Redis container continues running in the background via Docker.
- Your MySQL database retains all registered users and product data.
- To resume development later, run .\mvnw spring-boot:run from inside the shop folder.
Pause - I'll come back to this later
Stop the Redis container to free up memory. Your data stays intact so you can pick up where you left off.
- Stop Redis by running:
docker stop redis
- When you are ready to come back, restart Redis by running:
docker start redis
- Your MySQL database does not need to be paused. It only runs when the MySQL service is active.
- Your project files remain in the shop folder, ready for next time.
Delete - I don't want to use this again
Remove all project resources and start fresh. This permanently deletes your data.
- Remove the Redis container by running:
docker rm -f redis
- Drop the MySQL database by opening the MySQL CLI and running:
DROP DATABASE ecommerce_shop;
- Delete the project folder. On macOS or Linux, run:
rm -rf shop
- On Windows, run:
rmdir /s /q shop
- After these steps, all traces of the project are removed from your machine.
Nice Work!
Nice Work!
Nice work! You just built a fully working ecommerce authentication system using the same architecture senior developers deploy in production.
You've learned how to:
- Build a Spring Boot authentication system using the modern SecurityFilterChain pattern with BCrypt adaptive password hashing stored in MySQL.
- Experience firsthand why in-memory sessions break on restart, then solve it with Redis-backed sessions via Spring Session.
- Implement form-based login and logout with Thymeleaf server-side rendering and automatic CSRF protection.
- Secret Mission: Added role-based access control separating customers from admins using URL-level security rules in SecurityFilterChain.
Ready to quiz yourself?