Yes. The next extension should add user registration + login/logout + Spring Security + per-user Todo lists.
The architecture becomes:
Browser
│
▼
Spring Security
│
├── Login
├── Logout
└── Authentication
│
▼
Todo Controller
│
▼
Todo Service
│
├──────────────┐
▼ ▼
UserRepository TodoRepository
│ │
└──────┬───────┘
▼
MySQL
I recommend using Spring Security 6 / Spring Boot 3.x, with passwords stored using BCrypt, never as plain text.
I can give you the complete extension code, replacing the previous project with:
UserentityTodo→Userrelationship- Registration page
- Login page
- Logout
- BCrypt password hashing
- Spring Security configuration
- User-specific todos
- Protected
/todos/**pages - REST API authentication
- MySQL database changes
- Bootstrap UI
- Complete Eclipse project structure
The important database design will become:
users
────────────────────
id
username
password
role
todos
────────────────────
id
title
completed
user_id → users.id
So, for example:
User: devesh
│
├── Learn Java
├── Learn Spring
└── Learn MySQL
User: rahul
│
├── Learn Python
└── Learn Django
Each logged-in user will see only their own todos.
If you want, I can provide the full copy-paste code for every Java, HTML, pom.xml, and application.properties file as the next step.
Yes. Below is the complete upgraded Spring Todo Web Application with Login/Security.
It extends the previous project with:
- Spring Security
- Registration
- Login/logout
- BCrypt password hashing
- User roles
- MySQL
- User-specific Todo lists
- Add/edit/delete/toggle Todo
- Thymeleaf + Bootstrap
- REST API
- Validation
1. Final project structure
todo/
├── pom.xml
│
└── src/main/
├── java/com/example/todo/
│
│ ├── TodoApplication.java
│ │
│ ├── config/
│ │ └── SecurityConfig.java
│ │
│ ├── controller/
│ │ ├── AuthController.java
│ │ ├── TodoController.java
│ │ └── TodoRestController.java
│ │
│ ├── model/
│ │ ├── User.java
│ │ └── Todo.java
│ │
│ ├── repository/
│ │ ├── UserRepository.java
│ │ └── TodoRepository.java
│ │
│ └── service/
│ ├── UserService.java
│ └── TodoService.java
│
└── resources/
├── templates/
│ ├── login.html
│ ├── register.html
│ ├── index.html
│ └── edit.html
│
└── application.properties
2. MySQL database
Start MySQL:
sudo systemctl start mysql
Open MySQL:
sudo mysql
Create database and user:
CREATE DATABASE todo_db;
CREATE USER 'todo_user'@'localhost'
IDENTIFIED BY 'todo_password';
GRANT ALL PRIVILEGES ON todo_db.*
TO 'todo_user'@'localhost';
FLUSH PRIVILEGES;
EXIT;
You don’t need to manually create the users or todo tables.
JPA/Hibernate will create them.
3. pom.xml
Replace your existing pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.6</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>todo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>todo</name>
<description>
Spring Boot Todo Application with Security
</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<!-- Spring Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Thymeleaf -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- Thymeleaf Spring Security -->
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity6</artifactId>
</dependency>
<!-- JPA -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- Validation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- MySQL -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- DevTools -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
<!-- Testing -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Security Testing -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
4. Main application
TodoApplication.java
package com.example.todo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class TodoApplication {
public static void main(String[] args) {
SpringApplication.run(
TodoApplication.class,
args
);
}
}
5. User entity
Create:
model/User.java
package com.example.todo.model;
import jakarta.persistence.*;
import java.util.ArrayList;
import java.util.List;
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String username;
@Column(nullable = false)
private String password;
@Column(nullable = false)
private String role = "USER";
@OneToMany(
mappedBy = "user",
cascade = CascadeType.ALL,
orphanRemoval = true
)
private List<Todo> todos = new ArrayList<>();
public User() {
}
public User(
String username,
String password,
String role) {
this.username = username;
this.password = password;
this.role = role;
}
public Long getId() {
return id;
}
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;
}
public String getRole() {
return role;
}
public void setRole(String role) {
this.role = role;
}
public List<Todo> getTodos() {
return todos;
}
public void setTodos(List<Todo> todos) {
this.todos = todos;
}
}
6. Todo entity
Replace the previous Todo.java with:
package com.example.todo.model;
import jakarta.persistence.*;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
@Entity
@Table(name = "todos")
public class Todo {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank(message = "Title is required")
@Size(
min = 3,
max = 100,
message = "Title must be between 3 and 100 characters"
)
private String title;
private boolean completed;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User user;
public Todo() {
}
public Todo(String title) {
this.title = title;
this.completed = false;
}
public Long getId() {
return id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public boolean isCompleted() {
return completed;
}
public void setCompleted(boolean completed) {
this.completed = completed;
}
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
}
7. User repository
repository/UserRepository.java
package com.example.todo.repository;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import com.example.todo.model.User;
public interface UserRepository
extends JpaRepository<User, Long> {
Optional<User> findByUsername(String username);
boolean existsByUsername(String username);
}
8. Todo repository
repository/TodoRepository.java
package com.example.todo.repository;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import com.example.todo.model.Todo;
import com.example.todo.model.User;
public interface TodoRepository
extends JpaRepository<Todo, Long> {
List<Todo> findByUser(User user);
java.util.Optional<Todo>
findByIdAndUser(Long id, User user);
}
This is important.
Instead of:
findAll()
we use:
findByUser(user)
Therefore one user cannot see another user’s todos.
9. User service
Create:
service/UserService.java
package com.example.todo.service;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import com.example.todo.model.User;
import com.example.todo.repository.UserRepository;
@Service
public class UserService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
public UserService(
UserRepository userRepository,
PasswordEncoder passwordEncoder) {
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
}
public User registerUser(
String username,
String password) {
if (userRepository.existsByUsername(username)) {
throw new RuntimeException(
"Username already exists"
);
}
String encodedPassword =
passwordEncoder.encode(password);
User user = new User(
username,
encodedPassword,
"USER"
);
return userRepository.save(user);
}
}
10. Todo service
Replace the previous service:
service/TodoService.java
with:
package com.example.todo.service;
import java.util.List;
import org.springframework.stereotype.Service;
import com.example.todo.model.Todo;
import com.example.todo.model.User;
import com.example.todo.repository.TodoRepository;
@Service
public class TodoService {
private final TodoRepository todoRepository;
public TodoService(
TodoRepository todoRepository) {
this.todoRepository = todoRepository;
}
public List<Todo> getTodosForUser(User user) {
return todoRepository.findByUser(user);
}
public Todo getTodo(
Long id,
User user) {
return todoRepository
.findByIdAndUser(id, user)
.orElseThrow(() ->
new RuntimeException(
"Todo not found"
)
);
}
public Todo saveTodo(
Todo todo,
User user) {
todo.setUser(user);
return todoRepository.save(todo);
}
public void updateTodo(
Long id,
Todo details,
User user) {
Todo todo = getTodo(id, user);
todo.setTitle(details.getTitle());
todo.setCompleted(
details.isCompleted()
);
todoRepository.save(todo);
}
public void deleteTodo(
Long id,
User user) {
Todo todo = getTodo(id, user);
todoRepository.delete(todo);
}
public void toggleTodo(
Long id,
User user) {
Todo todo = getTodo(id, user);
todo.setCompleted(
!todo.isCompleted()
);
todoRepository.save(todo);
}
}
11. Spring Security configuration
This is the most important new class.
Create:
config/SecurityConfig.java
package com.example.todo.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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain securityFilterChain(
HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers(
"/login",
"/register",
"/css/**",
"/js/**"
).permitAll()
.requestMatchers("/api/**")
.authenticated()
.anyRequest()
.authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.defaultSuccessUrl(
"/",
true
)
.permitAll()
)
.logout(logout -> logout
.logoutSuccessUrl(
"/login?logout"
)
.permitAll()
);
return http.build();
}
}
12. Custom UserDetailsService
Spring Security needs to know how to load users from MySQL.
Create:
service/CustomUserDetailsService.java
package com.example.todo.service;
import org.springframework.security.core.userdetails.*;
import org.springframework.stereotype.Service;
import com.example.todo.model.User;
import com.example.todo.repository.UserRepository;
@Service
public class CustomUserDetailsService
implements UserDetailsService {
private final UserRepository userRepository;
public CustomUserDetailsService(
UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public UserDetails loadUserByUsername(
String username)
throws UsernameNotFoundException {
User user =
userRepository
.findByUsername(username)
.orElseThrow(() ->
new UsernameNotFoundException(
"User not found"
)
);
return User.withUsername(
user.getUsername()
)
.password(user.getPassword())
.roles(user.getRole())
.build();
}
}
13. Authentication controller
Create:
controller/AuthController.java
package com.example.todo.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import com.example.todo.service.UserService;
@Controller
public class AuthController {
private final UserService userService;
public AuthController(
UserService userService) {
this.userService = userService;
}
@GetMapping("/login")
public String login(
@RequestParam(
value = "error",
required = false
)
String error,
@RequestParam(
value = "logout",
required = false
)
String logout,
Model model) {
if (error != null) {
model.addAttribute(
"error",
"Invalid username or password"
);
}
if (logout != null) {
model.addAttribute(
"message",
"You have been logged out"
);
}
return "login";
}
@GetMapping("/register")
public String registerPage(
Model model) {
model.addAttribute(
"username",
""
);
return "register";
}
@PostMapping("/register")
public String register(
@RequestParam String username,
@RequestParam String password,
Model model) {
if (username == null ||
username.trim().isEmpty()) {
model.addAttribute(
"error",
"Username is required"
);
return "register";
}
if (password == null ||
password.length() < 6) {
model.addAttribute(
"error",
"Password must contain at least 6 characters"
);
return "register";
}
try {
userService.registerUser(
username.trim(),
password
);
return "redirect:/login?registered";
} catch (RuntimeException e) {
model.addAttribute(
"error",
e.getMessage()
);
return "register";
}
}
}
14. Todo web controller
Replace your previous TodoController.java with:
package com.example.todo.controller;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;
import com.example.todo.model.Todo;
import com.example.todo.model.User;
import com.example.todo.repository.UserRepository;
import com.example.todo.service.TodoService;
import jakarta.validation.Valid;
@Controller
public class TodoController {
private final TodoService todoService;
private final UserRepository userRepository;
public TodoController(
TodoService todoService,
UserRepository userRepository) {
this.todoService = todoService;
this.userRepository = userRepository;
}
private User getCurrentUser(
Authentication authentication) {
return userRepository
.findByUsername(
authentication.getName()
)
.orElseThrow();
}
@GetMapping("/")
public String home(
Model model,
Authentication authentication) {
User user =
getCurrentUser(authentication);
model.addAttribute(
"todos",
todoService.getTodosForUser(user)
);
model.addAttribute(
"todo",
new Todo()
);
model.addAttribute(
"username",
user.getUsername()
);
return "index";
}
@PostMapping("/todos")
public String addTodo(
@Valid @ModelAttribute("todo")
Todo todo,
BindingResult result,
Authentication authentication,
Model model) {
User user =
getCurrentUser(authentication);
if (result.hasErrors()) {
model.addAttribute(
"todos",
todoService.getTodosForUser(user)
);
model.addAttribute(
"username",
user.getUsername()
);
return "index";
}
todoService.saveTodo(
todo,
user
);
return "redirect:/";
}
@GetMapping("/todos/edit/{id}")
public String editTodo(
@PathVariable Long id,
Authentication authentication,
Model model) {
User user =
getCurrentUser(authentication);
Todo todo =
todoService.getTodo(id, user);
model.addAttribute(
"todo",
todo
);
return "edit";
}
@PostMapping("/todos/update/{id}")
public String updateTodo(
@PathVariable Long id,
@Valid @ModelAttribute("todo")
Todo todo,
BindingResult result,
Authentication authentication) {
if (result.hasErrors()) {
return "edit";
}
User user =
getCurrentUser(authentication);
todoService.updateTodo(
id,
todo,
user
);
return "redirect:/";
}
@GetMapping("/todos/delete/{id}")
public String deleteTodo(
@PathVariable Long id,
Authentication authentication) {
User user =
getCurrentUser(authentication);
todoService.deleteTodo(
id,
user
);
return "redirect:/";
}
@GetMapping("/todos/toggle/{id}")
public String toggleTodo(
@PathVariable Long id,
Authentication authentication) {
User user =
getCurrentUser(authentication);
todoService.toggleTodo(
id,
user
);
return "redirect:/";
}
}
15. REST controller
Create:
controller/TodoRestController.java
package com.example.todo.controller;
import java.util.List;
import org.springframework.http.*;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;
import com.example.todo.model.Todo;
import com.example.todo.model.User;
import com.example.todo.repository.UserRepository;
import com.example.todo.service.TodoService;
@RestController
@RequestMapping("/api/todos")
public class TodoRestController {
private final TodoService todoService;
private final UserRepository userRepository;
public TodoRestController(
TodoService todoService,
UserRepository userRepository) {
this.todoService = todoService;
this.userRepository = userRepository;
}
private User currentUser(
Authentication authentication) {
return userRepository
.findByUsername(
authentication.getName()
)
.orElseThrow();
}
@GetMapping
public List<Todo> getTodos(
Authentication authentication) {
User user =
currentUser(authentication);
return todoService.getTodosForUser(user);
}
@GetMapping("/{id}")
public ResponseEntity<Todo> getTodo(
@PathVariable Long id,
Authentication authentication) {
try {
User user =
currentUser(authentication);
return ResponseEntity.ok(
todoService.getTodo(id, user)
);
} catch (RuntimeException e) {
return ResponseEntity
.notFound()
.build();
}
}
@PostMapping
public ResponseEntity<Todo> createTodo(
@RequestBody Todo todo,
Authentication authentication) {
User user =
currentUser(authentication);
Todo saved =
todoService.saveTodo(
todo,
user
);
return ResponseEntity
.status(HttpStatus.CREATED)
.body(saved);
}
@PutMapping("/{id}")
public ResponseEntity<Todo> updateTodo(
@PathVariable Long id,
@RequestBody Todo todo,
Authentication authentication) {
try {
User user =
currentUser(authentication);
todoService.updateTodo(
id,
todo,
user
);
return ResponseEntity.ok(
todoService.getTodo(id, user)
);
} catch (RuntimeException e) {
return ResponseEntity
.notFound()
.build();
}
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteTodo(
@PathVariable Long id,
Authentication authentication) {
try {
User user =
currentUser(authentication);
todoService.deleteTodo(
id,
user
);
return ResponseEntity
.noContent()
.build();
} catch (RuntimeException e) {
return ResponseEntity
.notFound()
.build();
}
}
@PatchMapping("/{id}/toggle")
public ResponseEntity<Todo> toggleTodo(
@PathVariable Long id,
Authentication authentication) {
try {
User user =
currentUser(authentication);
todoService.toggleTodo(
id,
user
);
return ResponseEntity.ok(
todoService.getTodo(id, user)
);
} catch (RuntimeException e) {
return ResponseEntity
.notFound()
.build();
}
}
}
16. Login page
Create:
src/main/resources/templates/login.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, initial-scale=1">
<title>Login - Todo</title>
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
rel="stylesheet">
</head>
<body class="bg-light">
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-5">
<div class="card shadow">
<div class="card-header
bg-primary
text-white">
<h3 class="text-center">
Todo Login
</h3>
</div>
<div class="card-body">
<div
th:if="${error}"
class="alert alert-danger"
th:text="${error}">
</div>
<div
th:if="${message}"
class="alert alert-success"
th:text="${message}">
</div>
<div
th:if="${param.registered}"
class="alert alert-success">
Registration successful.
Please login.
</div>
<form
action="/login"
method="post">
<div class="mb-3">
<label class="form-label">
Username
</label>
<input
type="text"
name="username"
class="form-control"
required>
</div>
<div class="mb-3">
<label class="form-label">
Password
</label>
<input
type="password"
name="password"
class="form-control"
required>
</div>
<button
type="submit"
class="btn btn-primary w-100">
Login
</button>
</form>
<hr>
<div class="text-center">
<span>
Don't have an account?
</span>
<a href="/register">
Register
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>
17. Registration page
Create:
src/main/resources/templates/register.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, initial-scale=1">
<title>Register - Todo</title>
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
rel="stylesheet">
</head>
<body class="bg-light">
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-5">
<div class="card shadow">
<div class="card-header
bg-success
text-white">
<h3 class="text-center">
Create Account
</h3>
</div>
<div class="card-body">
<div
th:if="${error}"
class="alert alert-danger"
th:text="${error}">
</div>
<form
action="/register"
method="post">
<div class="mb-3">
<label class="form-label">
Username
</label>
<input
type="text"
name="username"
class="form-control"
minlength="3"
required>
</div>
<div class="mb-3">
<label class="form-label">
Password
</label>
<input
type="password"
name="password"
class="form-control"
minlength="6"
required>
<small class="text-muted">
Minimum 6 characters
</small>
</div>
<button
type="submit"
class="btn btn-success w-100">
Register
</button>
</form>
<hr>
<div class="text-center">
Already have an account?
<a href="/login">
Login
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>
18. Todo home page
Create/replace:
src/main/resources/templates/index.html
<!DOCTYPE html>
<html lang="en"
xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, initial-scale=1">
<title>My Todos</title>
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
rel="stylesheet">
</head>
<body class="bg-light">
<nav class="navbar navbar-dark bg-primary">
<div class="container">
<span class="navbar-brand">
Todo Application
</span>
<div class="d-flex align-items-center">
<span class="text-white me-3">
Welcome,
<strong th:text="${username}">
user
</strong>
</span>
<form
action="/logout"
method="post">
<button
type="submit"
class="btn btn-light btn-sm">
Logout
</button>
</form>
</div>
</div>
</nav>
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-10">
<div class="card shadow">
<div class="card-header">
<h3 class="mb-0">
My Todo List
</h3>
</div>
<div class="card-body">
<!-- ADD TODO -->
<form
th:action="@{/todos}"
th:object="${todo}"
method="post"
class="mb-4">
<div class="input-group">
<input
type="text"
th:field="*{title}"
class="form-control"
placeholder="Enter a new todo">
<button
type="submit"
class="btn btn-primary">
Add Todo
</button>
</div>
<div
th:if="${#fields.hasErrors('title')}"
th:errors="*{title}"
class="text-danger mt-2">
</div>
</form>
<!-- TODO TABLE -->
<table class="table table-hover">
<thead class="table-dark">
<tr>
<th>ID</th>
<th>Todo</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr th:each="todo : ${todos}">
<td th:text="${todo.id}">
1
</td>
<td>
<span
th:text="${todo.title}"
th:classappend="${todo.completed}
? 'text-decoration-line-through text-muted'
: ''">
Todo
</span>
</td>
<td>
<span
th:if="${todo.completed}"
class="badge bg-success">
Completed
</span>
<span
th:unless="${todo.completed}"
class="badge bg-warning text-dark">
Pending
</span>
</td>
<td>
<a
th:href="@{/todos/toggle/{id}(id=${todo.id})}"
class="btn btn-sm btn-success">
Toggle
</a>
<a
th:href="@{/todos/edit/{id}(id=${todo.id})}"
class="btn btn-sm btn-primary">
Edit
</a>
<a
th:href="@{/todos/delete/{id}(id=${todo.id})}"
class="btn btn-sm btn-danger"
onclick="return confirm('Delete this todo?');">
Delete
</a>
</td>
</tr>
<tr
th:if="${#lists.isEmpty(todos)}">
<td
colspan="4"
class="text-center text-muted">
You don't have any todos yet.
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</body>
</html>
19. Edit page
Create:
src/main/resources/templates/edit.html
<!DOCTYPE html>
<html lang="en"
xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, initial-scale=1">
<title>Edit Todo</title>
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
rel="stylesheet">
</head>
<body class="bg-light">
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card shadow">
<div class="card-header bg-primary text-white">
<h3 class="mb-0">
Edit Todo
</h3>
</div>
<div class="card-body">
<form
th:action="@{/todos/update/{id}(id=${todo.id})}"
th:object="${todo}"
method="post">
<div class="mb-3">
<label class="form-label">
Todo
</label>
<input
type="text"
th:field="*{title}"
class="form-control">
<div
th:if="${#fields.hasErrors('title')}"
th:errors="*{title}"
class="text-danger">
</div>
</div>
<div class="form-check mb-3">
<input
type="checkbox"
th:field="*{completed}"
class="form-check-input">
<label class="form-check-label">
Completed
</label>
</div>
<button
type="submit"
class="btn btn-primary">
Update
</button>
<a
href="/"
class="btn btn-secondary">
Cancel
</a>
</form>
</div>
</div>
</div>
</div>
</div>
</body>
</html>
20. application.properties
Use:
spring.application.name=todo
# ==========================================
# MySQL
# ==========================================
spring.datasource.url=jdbc:mysql://localhost:3306/todo_db
spring.datasource.username=todo_user
spring.datasource.password=todo_password
# ==========================================
# JPA / Hibernate
# ==========================================
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
# ==========================================
# Thymeleaf
# ==========================================
spring.thymeleaf.cache=false
# ==========================================
# Server
# ==========================================
server.port=8080
21. Update Maven project in Eclipse
After changing pom.xml:
Right click project
↓
Maven
↓
Update Project
↓
Check your project
↓
OK
Or from terminal:
./mvnw clean install
Then:
./mvnw spring-boot:run
22. Start the application
Make sure MySQL is running:
sudo systemctl status mysql
Then:
./mvnw spring-boot:run
Open:
http://localhost:8080
Because the application is secured, you’ll be redirected to:
/login
23. Register a user
Open:
http://localhost:8080/register
Create:
Username: devesh
Password: password123
Click Register.
You’ll be redirected to login.
24. Login
Enter:
Username: devesh
Password: password123
After successful authentication:
/login
↓
Spring Security
↓
/
↓
Todo Application
You should see:
+------------------------------------------------+
| Todo Application Welcome, devesh |
| [ Logout ] |
+------------------------------------------------+
| My Todo List |
| |
| [ Learn Spring Boot ] [Add Todo] |
| |
| ID | Todo | Status | Actions |
|----|-------------------|--------|--------------|
| 1 | Learn Java | Pending| Toggle Edit |
| 2 | Learn Spring Boot | Done | Toggle Edit |
+------------------------------------------------+
25. Test multi-user security
This is an important part of the project.
Create:
User 1:
username = devesh
password = password123
Add:
Learn Java
Learn Spring Boot
Learn MySQL
Logout.
Register:
User 2:
username = rahul
password = password123
Login as rahul.
You should see:
My Todo List
No todos yet.
Rahul cannot see Devesh’s todos.
The reason is this repository method:
findByIdAndUser(id, user)
and:
findByUser(user)
This is much safer than simply doing:
findAll()
26. What happens to the password?
Suppose the user registers:
password123
We do:
passwordEncoder.encode(password);
The database does not store:
password123
Instead, it stores a BCrypt hash similar to:
$2a$10$...
You can inspect it:
USE todo_db;
SELECT id, username, password, role
FROM users;
You’ll see something like:
+----+----------+----------------------+------+
| id | username | password | role |
+----+----------+----------------------+------+
| 1 | devesh | $2a$10$.............. | USER |
+----+----------+----------------------+------+
Never store real passwords directly in your database.
27. Database relationship
Hibernate creates approximately:
users
--------------------------------
id BIGINT PRIMARY KEY
username VARCHAR
password VARCHAR
role VARCHAR
and:
todos
--------------------------------
id BIGINT PRIMARY KEY
title VARCHAR
completed BOOLEAN
user_id BIGINT
Relationship:
users
|
| 1
|
| *
v
todos
In Java:
@OneToMany
private List<Todo> todos;
and:
@ManyToOne
private User user;
28. REST API
The REST API is also protected.
GET /api/todos
GET /api/todos/1
POST /api/todos
PUT /api/todos/1
PATCH /api/todos/1/toggle
DELETE /api/todos/1
You need to be authenticated to access these endpoints.
For example:
curl http://localhost:8080/api/todos
Without authentication, Spring Security will reject the request.
29. Test the API after login
For a browser-based form login, testing the REST API with curl is slightly different because the authentication is session-based.
For development, you can use Postman:
POST /login
with:
username=devesh
password=password123
Then retain the session cookie and call:
GET /api/todos
For a production REST application, I would normally change this architecture to JWT authentication rather than using the browser session.
30. Important Spring Security flow
The login flow is:
Browser
|
| POST /login
v
Spring Security
|
v
CustomUserDetailsService
|
v
UserRepository
|
v
MySQL
|
v
PasswordEncoder
|
v
BCrypt check
|
┌────────┴────────┐
| |
SUCCESS FAILURE
| |
v v
"/" /login?error
The important code is:
@Service
public class CustomUserDetailsService
implements UserDetailsService
Spring Security calls:
loadUserByUsername()
which loads the user from MySQL.
Then BCrypt verifies the password.
31. Important security rule
Do not do this:
user.setPassword(password);
Instead:
user.setPassword(
passwordEncoder.encode(password)
);
And don’t create a database column containing plain-text passwords.
32. Run and verify
Use these commands:
sudo systemctl start mysql
cd ~/todo
./mvnw clean
./mvnw spring-boot:run
Then:
http://localhost:8080/register
Test this sequence:
Register
↓
Login
↓
Add Todo
↓
Edit Todo
↓
Toggle Todo
↓
Delete Todo
↓
Logout
Then create a second account and verify that the first user’s todos aren’t visible.
Final architecture
Your application now looks like:
┌───────────────┐
│ Browser │
└───────┬───────┘
│
▼
┌────────────────────┐
│ Spring Security │
└─────────┬──────────┘
│
┌──────────┴──────────┐
│ │
▼ ▼
Login/Auth TodoController
│ │
▼ ▼
UserService TodoService
│ │
▼ ▼
UserRepository TodoRepository
│ │
└──────────┬──────────┘
│
▼
MySQL
This is now a solid Spring Boot MVC + Security + JPA + MySQL CRUD application. The natural next production-level extension is role-based authorization (USER/ADMIN), admin user management, BCrypt password validation, CSRF-safe delete forms, and JWT-based REST authentication.