Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Implementing Role-Based Access Control with Spring Security in a Spring Boot Application

Tech May 12 2

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 (not 4) 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 SecurityFilterChain and @EnableMethodSecurity instead of deprecated WebSecurityConfigurerAdapter.

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 @ElementCollection over 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>

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.