Implementing Role-Based Access Control with Spring Security in a Spring Boot Application
1. Required Dependencies
Add the following to your pom.xml:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>
Note: Use
springsecurity5(not4) for Spring Boot 2.3+.
2. Custom UserDetailsService Implementation
Create a service that loads user details and authorities from persistence:
@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: " + username));
Collection<GrantedAuthority> authorities = user.getRoles().stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role.toUpperCase()))
.collect(Collectors.toList());
return new org.springframework.security.core.userdetails.User(
user.getUsername(),
user.getEncodedPassword(),
authorities
);
}
}
Assume User has methods like getUsername(), getEncodedPassword(), and getRoles() returning a List<String>.
3. Security Configuration Class
Configure HTTP security, pasword encoding, and authentication flow:
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(authz -> authz
.requestMatchers("/", "/js/**", "/css/**", "/images/**", "/fonts/**").permitAll()
.requestMatchers("/user/login", "/user/registration").anonymous()
.requestMatchers("/user/settings").authenticated()
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/user/login")
.defaultSuccessUrl("/dashboard", true)
.permitAll()
)
.logout(logout -> logout
.logoutSuccessUrl("/?logged-out")
.invalidateHttpSession(true)
.clearAuthentication(true)
.permitAll()
);
return http.build();
}
@Bean
public AuthenticationManager authenticationManager(
UserDetailsService userDetailsService,
PasswordEncoder passwordEncoder) {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(userDetailsService);
provider.setPasswordEncoder(passwordEncoder);
return new ProviderManager(provider);
}
}
Uses modern
SecurityFilterChainand@EnableMethodSecurityinstead of deprecatedWebSecurityConfigurerAdapter.
4. Registration and Profile Management Controller
Handle registration, login, and authenticated profile updates:
@Controller
public class AccountController {
private final UserService userService;
private final PasswordEncoder passwordEncoder;
public AccountController(UserService userService, PasswordEncoder passwordEncoder) {
this.userService = userService;
this.passwordEncoder = passwordEncoder;
}
@GetMapping("/user/registration")
public String showRegistrationForm(Model model) {
model.addAttribute("registrationForm", new RegistrationForm());
return "account/register";
}
@PostMapping("/user/registration")
public String processRegistration(
@Valid @ModelAttribute("registrationForm") RegistrationForm form,
BindingResult result,
Model model) {
if (result.hasErrors()) {
return "account/register";
}
if (userService.existsByUsername(form.getUsername()) ||
userService.existsByEmail(form.getEmail())) {
model.addAttribute("error", "Username or email already taken.");
return "account/register";
}
User newUser = new User();
newUser.setUsername(form.getUsername());
newUser.setEmail(form.getEmail());
newUser.setEncodedPassword(passwordEncoder.encode(form.getPassword()));
newUser.setRoles(List.of("USER"));
newUser.setActivated(true);
userService.save(newUser);
return "redirect:/user/login?registered";
}
@GetMapping("/user/settings")
@PreAuthorize("isAuthenticated()")
public String showSettings(Model model) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
User currentUser = userService.findByUsername(auth.getName());
model.addAttribute("settingsForm", new SettingsForm(currentUser));
return "account/settings";
}
@PostMapping("/user/settings")
@PreAuthorize("isAuthenticated()")
public String updateSettings(
@Valid @ModelAttribute("settingsForm") SettingsForm form,
BindingResult result,
Model model) {
if (result.hasErrors()) {
return "account/settings";
}
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
User currentUser = userService.findByUsername(auth.getName());
currentUser.setEmail(form.getEmail());
currentUser.setBio(form.getBio());
userService.update(currentUser);
model.addAttribute("success", "Profile updated successfully.");
return "account/settings";
}
}
5. Service Layer Abstraction
Define interface and implementation for user operasions:
public interface UserService {
User save(User user);
User findByUsername(String username);
boolean existsByUsername(String username);
boolean existsByEmail(String email);
void update(User user);
}
@Service
public class DefaultUserService implements UserService {
private final UserRepository repository;
public DefaultUserService(UserRepository repository) {
this.repository = repository;
}
@Override
public User save(User user) {
return repository.save(user);
}
@Override
public User findByUsername(String username) {
return repository.findByUsername(username)
.orElse(null);
}
@Override
public boolean existsByUsername(String username) {
return repository.existsByUsername(username);
}
@Override
public boolean existsByEmail(String email) {
return repository.existsByEmail(email);
}
@Override
public void update(User user) {
repository.update(user);
}
}
6. Reopsitory Interface and Mapping
Use Spring Data JPA or MyBatis — here’s a MyBatis example:
@Mapper
public interface UserRepository {
@Insert("INSERT INTO T_USER (username, password, email, activated, date_created, bio, roles) " +
"VALUES (#{username}, #{encodedPassword}, #{email}, #{activated}, NOW(), #{bio}, #{roles})")
@Options(useGeneratedKeys = true, keyProperty = "id")
int insert(User user);
@Select("SELECT * FROM T_USER WHERE username = #{username}")
Optional<User> findByUsername(String username);
@Select("SELECT COUNT(*) > 0 FROM T_USER WHERE username = #{username}")
boolean existsByUsername(String username);
@Select("SELECT COUNT(*) > 0 FROM T_USER WHERE email = #{email}")
boolean existsByEmail(String email);
@Update("UPDATE T_USER SET email = #{email}, bio = #{bio} WHERE id = #{id}")
void update(User user);
}
7. User Entity with Role Handling
Simplify role storage and expose authorities safely:
@Entity
@Table(name = "T_USER")
public class User {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String username;
@Column(nullable = false)
private String encodedPassword;
@Column(unique = true, nullable = false)
private String email;
private boolean activated = true;
private LocalDateTime dateCreated = LocalDateTime.now();
private String bio;
@ElementCollection(fetch = FetchType.EAGER)
@CollectionTable(name = "user_roles", joinColumns = @JoinColumn(name = "user_id"))
@Column(name = "role")
private List<String> roles = new ArrayList<>(Collections.singletonList("USER"));
// Getters and setters omitted for brevity
public Collection<? extends GrantedAuthority> getAuthorities() {
return roles.stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role.toUpperCase()))
.collect(Collectors.toList());
}
}
Prefer
@ElementCollectionover comma-separated strings for type safety and queryability.
8. Thymeleaf Integration for Secured Views
In templates, use sec:authorize to conditionally render content:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<body>
<div sec:authorize="isAuthenticated()">
Welcome, <span sec:authentication="name"></span>!
<a href="/user/settings">Edit Profile</a>
<a href="/logout">Logout</a>
</div>
<div sec:authorize="hasRole('ADMIN')">
<a href="/admin/users">Manage Users</a>
</div>
</body>
</html>