Write and Publish a Tutorial!
Do you have good notes or papers written by you and seeking for a
platform to publish? We provide the platform to publish your tutorials
in your name. If you wish to publish your tutorial in your name to
help the readers, Please contact us by sending an email to
publish@tools4testing.com or publish@java4coding.com The main way that
others learn about your work is through your published tutorials. If
you don’t publish, it will be as if you never did the work. Your notes
can help the readers only when you share it.
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:
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.
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.
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.
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();
}
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.
getAuthorities method returns the actions that the app allows the user to do as a collection of GrantedAuthority instances.
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.javapackage 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.javapackage 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.javapackage 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.javapackage 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.javapackage 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.javapackage 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.javapackage 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.javapackage 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.propertiesspring.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.sqlcreate table user ( id bigint generated by default as identity, authority varchar(255), password varchar(255), username varchar(255), primary key (id) ); |
data.sqlINSERT 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
Output