×
☰ See All Chapters

Managing Users in Spring Security

In Spring Security, user-related functionalities are managed through contracts such as UserDetails, GrantedAuthority, UserDetailsManager, and UserDetailsService. These contracts define the structure and behavior that user management components should adhere to within a Spring application.

Let's break down these contracts and their default implementations:

  1. UserDetails: This interface represents core user information. It typically includes attributes such as username, password, and a collection of authorities (roles). Implementing UserDetails allows you to define your user model and provide necessary information for authentication and authorization. 

  2. GrantedAuthority: Spring Security represents the actions that a user can do with the GrantedAuthority interface. We often call these authorities, and a user has one or more authorities. This interface represents a granted authority, which typically is used to represent roles or permissions. Implementations of this interface hold information about what a principal (user) can do within an application. 

  3. UserDetailsManager: This interface provides basic user management operations such as creating, updating, deleting, and retrieving user details. Default implementations like InMemoryUserDetailsManager, JdbcUserDetailsManager, and LdapUserDetailsManager are provided by Spring Security for managing users using different data sources.  

  4. UserDetailsService: This interface is used to retrieve user details given a username. It's commonly used by the authentication process to load user details when attempting to authenticate a user. Custom implementations of UserDetailsService allow you to fetch user details from various sources like databases, LDAP, or other external systems. 

Now, let's discuss how to implement these contracts effectively:

Define a User Class

Implement the UserDetails interface to define your user class. This class should contain fields for username, password, and authorities. Below is the structure of UserDetails interface.

public interface UserDetails extends Serializable {
   
String getUsername();
   
String getPassword();
   Collection<?
extends GrantedAuthority> getAuthorities();
   
boolean isAccountNonExpired();
   
boolean isAccountNonLocked();
   
boolean isCredentialsNonExpired();
   
boolean isEnabled();
}

  1. getUsername and getPassword methods return the user credentials. The app uses these values in the process of authentication, and these are the only details related to authentication from this contract. The other five methods all relate to authorizing the user for accessing the application’s resources.  

  2. getAuthorities method returns the actions that the app allows the user to do as a collection of GrantedAuthority instances. 

  3. isAccountNonExpired, isAccountNonLocked, isCredentialsNonExpired and isEnabled methods enable or disable the account for different reasons. These methods should be implemented such that those needing to be enabled return true. Not all applications have accounts that expire or get locked with certain conditions. If you do not need to implement these functionalities in your application, you can simply make these four methods return true. 

Customize GrantedAuthorities

Implement the GrantedAuthority interface to define roles or permissions for users in your application. This can involve creating custom authority objects based on your application's authorization requirements.

Below is the structure of GrantedAuthority interface.

public interface GrantedAuthority extends Serializable {
   
String getAuthority();
}

The GrantedAuthority interface has only one abstract method so we use a lambda expression for its implementation. Another possibility is to use the SimpleGrantedAuthority class to create authority instances. The SimpleGrantedAuthority class offers a way to create immutable instances of the type GrantedAuthority.

GrantedAuthority g1 = () -> "READ";
GrantedAuthority g2 = new SimpleGrantedAuthority("READ");

Create a Custom UserDetailsService

Implement the UserDetailsService interface to load user details from your data source. This typically involves fetching user details from a database or any other storage mechanism.  The UserDetailsService interface contains only one method, as follows

public interface UserDetailsService {
   UserDetails
loadUserByUsername(String username)
           
throws UsernameNotFoundException;
}

The only thing Spring Security needs from you is an implementation to retrieve the user by username. If the username doesn’t exist, the method throws a UsernameNotFoundException.

Implement UserDetailsManager

 If you need custom user management operations, implement the UserDetailsManager interface. This can involve creating, updating, deleting, and retrieving user details according to your application's requirements. If the app only needs to authenticate the users, then implementing the UserDetailsService contract is enough to cover the desired functionality.  When implementing the UserDetailsManager interface you have to implement five methods of its own and one which it inherits from the UserDetailsService which it extends. These methods are: createUser, updateUser, deleteUser, changePassword, userExists and the inherited method loadUserByUsername.

public interface UserDetailsManager extends UserDetailsService {
   
void createUser(UserDetails user);
   
void updateUser(UserDetails user);
   
void deleteUser(String username);
   
void changePassword(String oldPassword, String newPassword);
   
boolean userExists(String username);
}

In the Spring Security framework v2.0.4 there are two concrete implementations of UserDetailsManager: JdbcUserDetailsManager and LdapUserDetailsManager. Generally, we don’t implement this interface or even we don’t use JdbcUserDetailsManager and LdapUserDetailsManager. After authentication/login use project specific flows to create, update, delete user. This interface is included purely as a convenience interface which the user of the framework may or may not decide to use. 

Configure Spring Security

Finally, configure Spring Security to use your custom implementations. This involves specifying your custom UserDetailsService, UserDetailsManager, and any other custom components in your security configuration.

Example

In this example, we write a UserDetailsService that retrieves user details from in memory h2 database.

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="https://maven.apache.org/POM/4.0.0" xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
     
xsi:schemaLocation="https://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>2.6.0</version>
     <
relativePath/> <!-- lookup parent from repository, not local -->
 
</parent>
  <
groupId>com.java4coding</groupId>
  <
artifactId>ManagingUsersInSpringSecurity</artifactId>
  <
version>0.0.1-SNAPSHOT</version>
  <
name>ManagingUsersInSpringSecurity</name>
  <
description>Managing Users in Spring Security</description>
  <
properties>
     <
java.version>11</java.version>
     <
project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
     <
project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
     <
spring-boot.version>2.6.0</spring-boot.version>
     <
lombok.version>1.18.28</lombok.version>
  </
properties>
  <
dependencies>
     <
dependency>
        <
groupId>org.springframework.boot</groupId>
        <
artifactId>spring-boot-starter</artifactId>
     </
dependency>
     <
dependency>
        <
groupId>org.springframework.boot</groupId>
        <
artifactId>spring-boot-starter-web</artifactId>
     </
dependency>
     <
dependency>
        <
groupId>org.springframework.boot</groupId>
        <
artifactId>spring-boot-starter-security</artifactId>
     </
dependency>
     <
dependency>
        <
groupId>org.projectlombok</groupId>
        <
artifactId>lombok</artifactId>
        <
version>${lombok.version}</version>
        <
scope>provided</scope>
     </
dependency>
     <
dependency>
        <
groupId>com.h2database</groupId>
        <
artifactId>h2</artifactId>
        <
scope>runtime</scope>
     </
dependency>
     <
dependency>
        <
groupId>org.springframework.boot</groupId>
        <
artifactId>spring-boot-starter-data-jpa</artifactId>
     </
dependency>
  </
dependencies>
  <
dependencyManagement>
     <
dependencies>
        <
dependency>
           <
groupId>org.springframework.boot</groupId>
           <
artifactId>spring-boot-dependencies</artifactId>
           <
version>${spring-boot.version}</version>
           <
type>pom</type>
           <
scope>import</scope>
        </
dependency>
     </
dependencies>
  </
dependencyManagement>

  <
build>
     <
plugins>
        <
plugin>
           <
groupId>org.springframework.boot</groupId>
           <
artifactId>spring-boot-maven-plugin</artifactId>
           <
version>${spring-boot.version}</version>
           <
executions>
              <
execution>
                 <
id>build-info</id>
                 <
goals>
                    <
goal>build-info</goal>
                    <
goal>repackage</goal>
                 </
goals>
              </
execution>
           </
executions>
        </
plugin>
     </
plugins>
  </
build>

</
project>

 

 

SpringBootDemo.java

package com.java4coding;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;

@SpringBootApplication
@EntityScan
@EnableJpaRepositories
public class SpringBootDemo {
   
public static void main(String[] args) {
       
SpringApplication.run(SpringBootDemo.class, args);
   }
}

 

 

ApplicationConfig.java

package com.java4coding.config;

import com.java4coding.repository.UserRepository;
import com.java4coding.security.UserDetailsServiceImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class ApplicationConfig {

   
@Bean
   
public UserDetailsService userDetailsService(UserRepository userRepository) {
       
return new UserDetailsServiceImpl(userRepository);
   }

   
@Bean
   
public PasswordEncoder passwordEncoder() {
       
return NoOpPasswordEncoder.getInstance();
   }
}

 

 

ApplicationWebSecurityConfigurerAdapter.java

package com.java4coding.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
public class ApplicationWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {

   
/*
   Since H2 has its own authentication provider, you can skip the Spring Security for the
   path of h2 console entirely in the same way that you do for your static content.
   In order to do that, in your Spring security config, you have to override the configuration
    method which takes an instance of org.springframework.security.config.annotation.web.builders.WebSecurity
    as a parameter instead of the one which takes an instance of org.springframework.security.config.annotation.web.builders.HttpSecurity
    */
   
@Override
   
public void configure(WebSecurity web) throws Exception {
       web.ignoring().antMatchers(
"/h2-console/**");
   }

}

 

 

DemoController.java

package com.java4coding.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class DemoController {

   
@GetMapping(value = "/demo")
   
public String sayHello() {
       
return "Hurray! You are Authorized.";
   }
}

 

 

User.java

package com.java4coding.entity;

import lombok.Getter;
import lombok.Setter;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
@Setter
@Getter
public class User {
   
@Id
   @GeneratedValue
(strategy = GenerationType.IDENTITY)
   
private Long id;
   
private String username;
   
private String password;
   
private String authority;
}

 

 

UserRepository.java

package com.java4coding.repository;

import com.java4coding.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

public interface UserRepository extends JpaRepository<User, Long> {
   
List<User> findByUsername(String userName);
}

 

 

SecurityUser.java

package com.java4coding.security;

import com.java4coding.entity.User;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.List;

public class SecurityUser implements UserDetails {
   
private final User user;

   
public SecurityUser(User user) {
       
this.user = user;
   }

   
@Override
   
public String getUsername() {
       
return user.getUsername();
   }

   
@Override
   
public String getPassword() {
       
return user.getPassword();
   }

   
@Override
   
public Collection<? extends GrantedAuthority> getAuthorities() {
       
return List.of(() -> user.getAuthority());
   }

   
@Override
   
public boolean isAccountNonExpired() {
       
return true;
   }

   
@Override
   
public boolean isAccountNonLocked() {
       
return true;
   }

   
@Override
   
public boolean isCredentialsNonExpired() {
       
return true;
   }

   
@Override
   
public boolean isEnabled() {
       
return true;
   }
}

 

 

UserDetailsServiceImpl.java

package com.java4coding.security;

import com.java4coding.entity.User;
import com.java4coding.repository.UserRepository;
import lombok.AllArgsConstructor;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;

import java.util.List;

@AllArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService {
   
private UserRepository userRepository;

   
@Override
   
public SecurityUser loadUserByUsername(String username) throws UsernameNotFoundException {
       
List<User> usersByUserName = userRepository.findByUsername(username);
       
return new SecurityUser(usersByUserName.get(0));
   }
}

 

 

application.properties

spring.h2.console.enabled=true
spring.h2.console.path=/h2-console/

spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=pass
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
#spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.hibernate.ddl-auto=none

spring.jpa.properties.hibernate.show_sql=true
spring.jpa.properties.hibernate.use_sql_comments=true
spring.jpa.properties.hibernate.format_sql=true

 

 

schema.sql

create table user (
  id bigint generated
by default as identity,
   authority
varchar(255),
   password
varchar(255),
   username
varchar(255),
   
primary key (id)
);

 

 

data.sql

INSERT INTO User (id, username, password, authority) VALUES (1, 'manu', 'pass', 'read');
INSERT INTO User (id, username, password, authority) VALUES (2, 'advith', 'xyz123', 'read');
INSERT INTO User (id, username, password, authority) VALUES (3, 'aashvith', 'xyz123', 'read');

 

Project Structure

managing-users-in-spring-security-0
 

Output

managing-users-in-spring-security-1
 

All Chapters
Author