Add concurrency control with JPA locking and secure the application with Spring Security.
Secure Task Management API with:
Add to pom.xml:
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- Security Test -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
Learn why locking is necessary in multi-user applications.
Scenario: Two users edit the same task simultaneously.
Time User A User B Database
---- ---------------------- ---------------------- -------------
T1 Read task (status=TODO) status=TODO
T2 Read task (status=TODO) status=TODO
T3 Update status=DOING
T4 Update status=DONE
T5 Save (status=DOING) status=DOING
T6 Save (status=DONE) status=DONE ❌
Result: User A's update is lost! User B overwrote it.
Scenario: One transaction reads uncommitted changes from another.
Time Transaction A Transaction B
---- ---------------------- ----------------------
T1 Update task price=100
T2 Read task price=100
T3 ROLLBACK
T4 Use price=100 ❌ (dirty)
Scenario: Reading the same data twice in a transaction gives different results.
Time Transaction A Transaction B
---- ---------------------- ----------------------
T1 Read task (status=TODO)
T2 Update status=DONE
T3 COMMIT
T4 Read task (status=DONE) ❌ Different value!
Two approaches to prevent these problems:
Add version field to detect concurrent modifications.
package com.example.taskmanager.model;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import java.time.LocalDateTime;
import java.util.HashSet;
import java.util.Set;
@Entity
@Table(name = "tasks")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Task {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 100)
private String title;
@Column(length = 500)
private String description;
@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 20)
private TaskStatus status;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User assignedTo;
@ManyToMany(fetch = FetchType.LAZY)
@JoinTable(
name = "task_categories",
joinColumns = @JoinColumn(name = "task_id"),
inverseJoinColumns = @JoinColumn(name = "category_id")
)
private Set<Category> categories = new HashSet<>();
// ⭐ Optimistic locking version field
@Version
private Long version;
@CreationTimestamp
@Column(nullable = false, updatable = false)
private LocalDateTime createdAt;
@UpdateTimestamp
@Column(nullable = false)
private LocalDateTime updatedAt;
public Task(String title, String description) {
this.title = title;
this.description = description;
this.status = TaskStatus.TODO;
}
}
Initial State: Task(id=1, title="Fix bug", version=0)
User A reads: Task(id=1, version=0)
User B reads: Task(id=1, version=0)
User A updates:
UPDATE tasks
SET title='Fixed bug', version=1
WHERE id=1 AND version=0 ✅ Success (1 row updated)
User B updates:
UPDATE tasks
SET title='Bug fixed', version=1
WHERE id=1 AND version=0 ❌ Fails (0 rows updated)
Throws: OptimisticLockException
Version increments automatically on each update.
Create custom exception:
package com.example.taskmanager.exception;
public class TaskVersionConflictException extends RuntimeException {
private final Long taskId;
private final Long attemptedVersion;
public TaskVersionConflictException(Long taskId, Long attemptedVersion) {
super(String.format(
"Task %d was modified by another user. Expected version: %d. " +
"Please refresh and try again.", taskId, attemptedVersion
));
this.taskId = taskId;
this.attemptedVersion = attemptedVersion;
}
public Long getTaskId() {
return taskId;
}
public Long getAttemptedVersion() {
return attemptedVersion;
}
}
Update GlobalExceptionHandler:
import jakarta.persistence.OptimisticLockException;
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
// ... existing handlers ...
@ExceptionHandler(OptimisticLockException.class)
public ResponseEntity<ErrorResponse> handleOptimisticLock(
OptimisticLockException ex,
HttpServletRequest request) {
log.warn("Optimistic lock conflict: {}", ex.getMessage());
ErrorResponse error = new ErrorResponse(
LocalDateTime.now(),
HttpStatus.CONFLICT.value(),
"Conflict",
"The resource was modified by another user. Please refresh and retry.",
request.getRequestURI(),
null
);
return ResponseEntity.status(HttpStatus.CONFLICT).body(error);
}
@ExceptionHandler(TaskVersionConflictException.class)
public ResponseEntity<ErrorResponse> handleVersionConflict(
TaskVersionConflictException ex,
HttpServletRequest request) {
log.warn("Task version conflict: taskId={}, version={}",
ex.getTaskId(), ex.getAttemptedVersion());
ErrorResponse error = new ErrorResponse(
LocalDateTime.now(),
HttpStatus.CONFLICT.value(),
"Version Conflict",
ex.getMessage(),
request.getRequestURI(),
null
);
return ResponseEntity.status(HttpStatus.CONFLICT).body(error);
}
}
Add version checking in update method:
@Transactional
public Task updateTask(Long id, Task taskDetails) {
log.debug("Updating task with ID: {}", id);
Task task = taskRepository.findById(id)
.orElseThrow(() -> new TaskNotFoundException(id));
// Check if client provided version for optimistic locking
if (taskDetails.getVersion() != null &&
!task.getVersion().equals(taskDetails.getVersion())) {
throw new TaskVersionConflictException(id, taskDetails.getVersion());
}
task.setTitle(taskDetails.getTitle());
task.setDescription(taskDetails.getDescription());
task.setStatus(taskDetails.getStatus());
return taskRepository.save(task);
}
# Create task
curl -X POST http://localhost:8080/api/tasks \
-H "Content-Type: application/json" \
-d '{"title": "Test Locking", "description": "Version test"}'
# Response: {"id": 1, "title": "Test Locking", "version": 0, ...}
# Update with correct version
curl -X PUT http://localhost:8080/api/tasks/1 \
-H "Content-Type: application/json" \
-d '{"title": "Updated", "version": 0}'
# Response: {"id": 1, "title": "Updated", "version": 1, ...}
# Update with old version (simulates conflict)
curl -X PUT http://localhost:8080/api/tasks/1 \
-H "Content-Type: application/json" \
-d '{"title": "Conflict", "version": 0}'
# Response: 409 Conflict
# {"error": "Version Conflict", "message": "Task was modified..."}
Lock records during read to prevent concurrent modifications.
// Shared lock (read lock) - allows other reads, blocks writes
LockModeType.PESSIMISTIC_READ
// Exclusive lock (write lock) - blocks all other access
LockModeType.PESSIMISTIC_WRITE
// Increment version without lock
LockModeType.PESSIMISTIC_FORCE_INCREMENT
package com.example.taskmanager.repository;
import com.example.taskmanager.model.Task;
import com.example.taskmanager.model.TaskStatus;
import jakarta.persistence.LockModeType;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Lock;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface TaskRepository extends JpaRepository<Task, Long> {
// ... existing methods ...
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT t FROM Task t WHERE t.id = :id")
Optional<Task> findByIdWithLock(@Param("id") Long id);
@Lock(LockModeType.PESSIMISTIC_READ)
@Query("SELECT t FROM Task t WHERE t.status = :status")
List<Task> findByStatusWithLock(@Param("status") TaskStatus status);
}
@Transactional
public Task updateTaskWithLock(Long id, Task taskDetails) {
log.debug("Updating task {} with pessimistic lock", id);
// Acquire write lock (blocks other transactions)
Task task = taskRepository.findByIdWithLock(id)
.orElseThrow(() -> new TaskNotFoundException(id));
// Hold lock until transaction commits
task.setTitle(taskDetails.getTitle());
task.setDescription(taskDetails.getDescription());
task.setStatus(taskDetails.getStatus());
return taskRepository.save(task);
// Lock released when transaction commits
}
-- PostgreSQL
SELECT * FROM tasks WHERE id = 1 FOR UPDATE;
-- MySQL
SELECT * FROM tasks WHERE id = 1 FOR UPDATE;
-- H2
SELECT * FROM tasks WHERE id = 1 FOR UPDATE;
Aspect | Optimistic | Pessimistic |
When to use | Low contention | High contention |
Performance | Better (no locks) | Slower (locks) |
Failures | At save time | At read time |
Scalability | Better | Limited |
Deadlocks | No | Possible |
User Experience | May need retry | Blocks/waits |
Use case | Web apps, REST APIs | Banking, inventory |
Configure Spring Security for authentication and authorization.
package com.example.taskmanager.model;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.CreationTimestamp;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@Entity
@Table(name = "users")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true, length = 50)
private String username;
@Column(nullable = false, unique = true, length = 100)
private String email;
@Column(nullable = false, length = 100)
private String fullName;
// ⭐ Security fields
@Column(nullable = false)
private String password;
@ElementCollection(fetch = FetchType.EAGER)
@CollectionTable(name = "user_roles", joinColumns = @JoinColumn(name = "user_id"))
@Column(name = "role")
private Set<String> roles = new HashSet<>();
@Column(nullable = false)
private boolean enabled = true;
@OneToMany(mappedBy = "assignedTo", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Task> tasks = new ArrayList<>();
@CreationTimestamp
@Column(nullable = false, updatable = false)
private LocalDateTime createdAt;
public User(String username, String email, String fullName, String password) {
this.username = username;
this.email = email;
this.fullName = fullName;
this.password = password;
this.roles.add("ROLE_USER");
}
}
package com.example.taskmanager.security;
import com.example.taskmanager.model.User;
import com.example.taskmanager.repository.UserRepository;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Collection;
import java.util.stream.Collectors;
@Service
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
public CustomUserDetailsService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
@Transactional(readOnly = true)
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException(
"User not found with username: " + username));
return new org.springframework.security.core.userdetails.User(
user.getUsername(),
user.getPassword(),
user.isEnabled(),
true, // accountNonExpired
true, // credentialsNonExpired
true, // accountNonLocked
getAuthorities(user)
);
}
private Collection<? extends GrantedAuthority> getAuthorities(User user) {
return user.getRoles().stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
}
}
package com.example.taskmanager.config;
import com.example.taskmanager.security.CustomUserDetailsService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig {
private final CustomUserDetailsService userDetailsService;
public SecurityConfig(CustomUserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable()) // Disable for REST API
.authorizeHttpRequests(auth -> auth
// Public endpoints
.requestMatchers("/h2-console/**").permitAll()
.requestMatchers("/swagger-ui/**", "/api-docs/**").permitAll()
.requestMatchers(HttpMethod.POST, "/api/users/register").permitAll()
// Admin only endpoints
.requestMatchers("/api/users/**").hasRole("ADMIN")
.requestMatchers(HttpMethod.DELETE, "/api/tasks/**").hasRole("ADMIN")
// Authenticated endpoints
.requestMatchers("/api/tasks/**").authenticated()
.requestMatchers("/api/categories/**").authenticated()
// All other requests require authentication
.anyRequest().authenticated()
)
.httpBasic(basic -> {}) // Enable HTTP Basic authentication
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.headers(headers -> headers.frameOptions(frame -> frame.disable())); // For H2 console
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService);
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration authConfig) throws Exception {
return authConfig.getAuthenticationManager();
}
}
// Public access (no authentication required)
.requestMatchers("/swagger-ui/**").permitAll()
// Role-based access (requires specific role)
.requestMatchers("/api/users/**").hasRole("ADMIN")
// Authenticated (any logged-in user)
.requestMatchers("/api/tasks/**").authenticated()
// HTTP Basic Authentication
.httpBasic()
// Stateless sessions (for REST APIs)
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
Role Naming Convention:
ROLE_ADMIN, ROLE_USERhasRole("ADMIN") - Spring adds "ROLE_" prefix automaticallySecure service methods with annotations.
package com.example.taskmanager.service;
import com.example.taskmanager.model.User;
import com.example.taskmanager.repository.UserRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
@Slf4j
@Transactional(readOnly = true)
public class UserService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder) {
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
}
@Transactional
public User registerUser(User user) {
log.debug("Registering user: {}", user.getUsername());
if (userRepository.existsByUsername(user.getUsername())) {
throw new RuntimeException("Username already exists: " + user.getUsername());
}
if (userRepository.existsByEmail(user.getEmail())) {
throw new RuntimeException("Email already exists: " + user.getEmail());
}
// Encode password
user.setPassword(passwordEncoder.encode(user.getPassword()));
// Set default role
if (user.getRoles().isEmpty()) {
user.getRoles().add("ROLE_USER");
}
return userRepository.save(user);
}
@Transactional
@PreAuthorize("hasRole('ADMIN')")
public User createUserAsAdmin(User user, String role) {
log.debug("Admin creating user: {} with role: {}", user.getUsername(), role);
user.getRoles().add(role);
return registerUser(user);
}
@PreAuthorize("hasRole('ADMIN') or #username == authentication.principal.username")
public User getUserByUsername(String username) {
return userRepository.findByUsername(username)
.orElseThrow(() -> new RuntimeException("User not found: " + username));
}
@PreAuthorize("hasRole('ADMIN')")
public List<User> getAllUsers() {
return userRepository.findAll();
}
@Transactional
@PreAuthorize("hasRole('ADMIN') or #id == principal.id")
public void deleteUser(Long id) {
if (!userRepository.existsById(id)) {
throw new RuntimeException("User not found with id: " + id);
}
userRepository.deleteById(id);
}
public User getCurrentUser() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String username = authentication.getName();
return userRepository.findByUsername(username)
.orElseThrow(() -> new RuntimeException("Current user not found"));
}
}
@Service
@Slf4j
@Transactional(readOnly = true)
public class TaskService {
// ... existing fields ...
@Transactional
@PreAuthorize("hasRole('USER')")
public Task createTask(Task task) {
log.debug("Creating new task: {}", task.getTitle());
// Auto-assign to current user
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
String username = auth.getName();
User currentUser = userRepository.findByUsername(username)
.orElseThrow(() -> new RuntimeException("User not found"));
task.setAssignedTo(currentUser);
if (task.getStatus() == null) {
task.setStatus(TaskStatus.TODO);
}
Task savedTask = taskRepository.save(task);
log.info("Task created with ID: {}", savedTask.getId());
return savedTask;
}
@PreAuthorize("hasRole('USER')")
public Task getTaskById(Long id) {
Task task = taskRepository.findById(id)
.orElseThrow(() -> new TaskNotFoundException(id));
// Check ownership
checkTaskOwnership(task);
return task;
}
@Transactional
@PreAuthorize("hasRole('USER')")
public Task updateTask(Long id, Task taskDetails) {
Task task = taskRepository.findById(id)
.orElseThrow(() -> new TaskNotFoundException(id));
// Check ownership
checkTaskOwnership(task);
task.setTitle(taskDetails.getTitle());
task.setDescription(taskDetails.getDescription());
task.setStatus(taskDetails.getStatus());
return taskRepository.save(task);
}
@Transactional
@PreAuthorize("hasRole('ADMIN')")
public void deleteTask(Long id) {
if (!taskRepository.existsById(id)) {
throw new TaskNotFoundException(id);
}
taskRepository.deleteById(id);
}
@PreAuthorize("hasRole('USER')")
public List<Task> getMyTasks() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
String username = auth.getName();
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new RuntimeException("User not found"));
return taskRepository.findByAssignedToId(user.getId());
}
private void checkTaskOwnership(Task task) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
String username = auth.getName();
if (task.getAssignedTo() != null &&
!task.getAssignedTo().getUsername().equals(username)) {
// Check if user has ADMIN role
boolean isAdmin = auth.getAuthorities().stream()
.anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN"));
if (!isAdmin) {
throw new RuntimeException("Access denied: Task belongs to another user");
}
}
}
}
// @PreAuthorize - Check before method execution
@PreAuthorize("hasRole('ADMIN')")
public void adminMethod() { }
@PreAuthorize("hasRole('USER') or hasRole('ADMIN')")
public void userOrAdminMethod() { }
@PreAuthorize("#username == authentication.principal.username")
public void ownResourceMethod(String username) { }
// @PostAuthorize - Check after method execution
@PostAuthorize("returnObject.owner == authentication.principal.username")
public Task getTask(Long id) { }
// @Secured - Simple role checking
@Secured("ROLE_ADMIN")
public void simpleAdminMethod() { }
@Secured({"ROLE_USER", "ROLE_ADMIN"})
public void multiRoleMethod() { }
// Authentication principal
authentication.principal.username
principal.username // shorthand
// Authorities/Roles
hasRole('ADMIN')
hasAnyRole('USER', 'ADMIN')
hasAuthority('READ_PRIVILEGE')
// Method parameters
#username == principal.username
#id == principal.id
// Return value
returnObject.owner == principal.username
// Combine conditions
hasRole('ADMIN') or #user.id == principal.id
Add registration endpoint and update controllers for security.
package com.example.taskmanager.controller;
import com.example.taskmanager.model.User;
import com.example.taskmanager.service.UserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.net.URI;
import java.util.List;
@RestController
@RequestMapping("/api/users")
@Tag(name = "User Management")
@Slf4j
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@Operation(summary = "Register a new user (public)")
@PostMapping("/register")
public ResponseEntity<User> registerUser(@RequestBody User user) {
log.info("POST /api/users/register - Registering: {}", user.getUsername());
User registered = userService.registerUser(user);
// Don't expose password in response
registered.setPassword(null);
URI location = URI.create("/api/users/" + registered.getId());
return ResponseEntity.created(location).body(registered);
}
@Operation(summary = "Get current user", security = @SecurityRequirement(name = "basicAuth"))
@GetMapping("/me")
public ResponseEntity<User> getCurrentUser() {
log.info("GET /api/users/me");
User user = userService.getCurrentUser();
user.setPassword(null);
return ResponseEntity.ok(user);
}
@Operation(summary = "Get all users (Admin only)", security = @SecurityRequirement(name = "basicAuth"))
@GetMapping
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<List<User>> getAllUsers() {
log.info("GET /api/users");
List<User> users = userService.getAllUsers();
users.forEach(u -> u.setPassword(null));
return ResponseEntity.ok(users);
}
@Operation(summary = "Get user by username", security = @SecurityRequirement(name = "basicAuth"))
@GetMapping("/username/{username}")
public ResponseEntity<User> getUserByUsername(@PathVariable String username) {
log.info("GET /api/users/username/{}", username);
User user = userService.getUserByUsername(username);
user.setPassword(null);
return ResponseEntity.ok(user);
}
@Operation(summary = "Delete user (Admin only)", security = @SecurityRequirement(name = "basicAuth"))
@DeleteMapping("/{id}")
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
log.info("DELETE /api/users/{}", id);
userService.deleteUser(id);
return ResponseEntity.noContent().build();
}
}
@RestController
@RequestMapping("/api/tasks")
@Tag(name = "Task Management")
@SecurityRequirement(name = "basicAuth")
@Slf4j
public class TaskController {
// ... existing code ...
@Operation(summary = "Get my tasks")
@GetMapping("/my-tasks")
public ResponseEntity<List<TaskResponse>> getMyTasks() {
log.info("GET /api/tasks/my-tasks");
List<TaskResponse> tasks = taskService.getMyTasks().stream()
.map(TaskResponse::new)
.toList();
return ResponseEntity.ok(tasks);
}
}
package com.example.taskmanager.config;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.License;
import io.swagger.v3.oas.models.security.SecurityScheme;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class OpenAPIConfig {
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.components(new Components()
.addSecuritySchemes("basicAuth",
new SecurityScheme()
.type(SecurityScheme.Type.HTTP)
.scheme("basic")
.description("HTTP Basic Authentication")
)
)
.info(new Info()
.title("Task Management API")
.version("1.3.0")
.description("Secure Task Management API with JPA locking and Spring Security")
.contact(new Contact()
.name("Your Name")
.email("your.email@example.com")
.url("https://yourwebsite.com")
)
.license(new License()
.name("Apache 2.0")
.url("https://www.apache.org/licenses/LICENSE-2.0.html")
)
);
}
}
Test authentication and authorization.
Add data initialization:
package com.example.taskmanager;
import com.example.taskmanager.model.User;
import com.example.taskmanager.repository.UserRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
@Slf4j
public class DataInitializer {
@Bean
public CommandLineRunner initData(UserRepository userRepository,
PasswordEncoder passwordEncoder) {
return args -> {
if (userRepository.count() == 0) {
// Create admin user
User admin = new User();
admin.setUsername("admin");
admin.setEmail("admin@example.com");
admin.setFullName("Admin User");
admin.setPassword(passwordEncoder.encode("admin123"));
admin.getRoles().add("ROLE_ADMIN");
admin.getRoles().add("ROLE_USER");
userRepository.save(admin);
// Create regular user
User user = new User();
user.setUsername("john");
user.setEmail("john@example.com");
user.setFullName("John Doe");
user.setPassword(passwordEncoder.encode("user123"));
user.getRoles().add("ROLE_USER");
userRepository.save(user);
log.info("Test users created: admin/admin123, john/user123");
}
};
}
}
1. Register new user:
curl -X POST http://localhost:8080/api/users/register \
-H "Content-Type: application/json" \
-d '{
"username": "alice",
"email": "alice@example.com",
"fullName": "Alice Smith",
"password": "password123"
}'
2. Get current user (authenticated):
curl -u john:user123 http://localhost:8080/api/users/me
3. Create task (auto-assigned):
curl -u john:user123 -X POST http://localhost:8080/api/tasks \
-H "Content-Type: application/json" \
-d '{
"title": "My Secure Task",
"description": "Testing Spring Security"
}'
4. Get my tasks:
curl -u john:user123 http://localhost:8080/api/tasks/my-tasks
5. Try admin endpoint (should fail):
curl -u john:user123 http://localhost:8080/api/users
# Response: 403 Forbidden
6. Admin endpoint (should succeed):
curl -u admin:admin123 http://localhost:8080/api/users
7. Delete task (admin only):
curl -u admin:admin123 -X DELETE http://localhost:8080/api/tasks/1
john / user123Congratulations! 🎉 You've secured your application with Spring Security and JPA locking!
Security features:
git add .
git commit -m "Codelab 3.4: JPA Locking & Spring Security complete"
git tag codelab-3.4