Lab M02P02
In production environment application does not maintain Users in memory. In this lab, you utilize the Spring Security provided JdbcUserDetailsManager which supports persistence of User details in SQL database. Spring implements its own model how to store usernames, password and authorities. You are free to customize the default model to your needs. You will try both concepts, so let's begin.
-
First review the project setup. There is PostreSQL DataSource configured in the application.properties. You need to start PostgreSQL server in Docker environment using docker compose file. The resource contains sql scripts for db schema and data creation. The application itself is simple server with the REST API you need to secure.
-
Now, configure Spring Security to use the JdbcUserDetailsManager, which is provided by the framework, so you do not need to implement one. JDBC UserDetail implementation requires db schema which defines two tables USERS and AUTHORITIES. The default model is included in Spring Security package \org\springframework\security\core\userdetails\jdbc\users.ddl. Add Bean definition of type UserDetailsManager into SecurityConfig class. Use JdbcUserDetailsManager implementation and initialize it with some Users. Use defined PasswordEncoder bean to hash passwords. You should never store plain password into database, or anywhere else.
Hint:
@Bean public UserDetailsManager users(DataSource dataSource) throws Exception { UserDetails admin = User.builder() .username("admin") .password(passwordEncoder().encode("admin")) .roles("ADMIN","USER") .build(); UserDetails user = User.builder() .username("user") .password(passwordEncoder().encode("user")) .roles("USER") .build(); JdbcUserDetailsManager users = new JdbcUserDetailsManager(dataSource); users.createUser(user); users.createUser(admin); return users; } // This makes sure that the passwords are hashed // (by default using the `bcrypt' hashing algorithm). @Bean public PasswordEncoder passwordEncoder() { return PasswordEncoderFactories.createDelegatingPasswordEncoder(); } -
You can initialize database by schema defined in schema.sql, which is automatically picked up by framework during application (in development) start. Open the schema.sql and add tables creation statements:
Review the UserDetailsManager bean defined in step 2. Above tables are populated during application start by the users.createUser(user) code.DROP TABLE IF EXISTS authorities; DROP TABLE IF EXISTS users; CREATE TABLE users ( username VARCHAR(50) NOT NULL, password VARCHAR(100) NOT NULL, enabled BOOLEAN NOT NULL, PRIMARY KEY (username) ); CREATE TABLE authorities ( username VARCHAR(50) NOT NULL, authority VARCHAR(50) NOT NULL, FOREIGN KEY (username) REFERENCES users (username) ); -
Now start the application and try to call API. There is postman collection included. Review the library database and check if password in USERS table are encoded. (jdbc:postgresql://localhost:35432/library; library/password)
-
What if default Spring provided DB model to maintain Users does not suit your needs? Spring flexibility enables you to define any model + you implement UserDetailService which provides Users and Authorities. Let's try some customization. User data typically includes more profile properties as first and last name, e-mail address, and more.
-
First remove, or comment out UserDetailsManager bean definition from SecurityConfig class. It is not required any more as you are going to implement custom UserDetailsService bean.
-
There is empty CustomerService class. Implement the UserDetailsService, which is the core building block of Spring Security. The Customer entity and CustomerRepository are already implemented in domain.model package. Customer (name was chosen to distinguish from default User entity ) represents user of the application.
@Service public class CustomerService implements UserDetailsService { private final CustomerRepository customerRepository; public CustomerService(CustomerRepository customerRepository) { this.customerRepository = customerRepository; } @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { String password = null; List<GrantedAuthority> authorities = null; Customer customer = customerRepository.findById(username).orElse(null); if(customer == null) { throw new UsernameNotFoundException("User details not found for the user : " + username); } else { password = customer.getPwd(); authorities = new ArrayList<>(); for(String role: customer.getRoles()) { authorities.add(new SimpleGrantedAuthority("ROLE_" + role)); } } return new User(username, password, authorities); } } -
Add required database tables definition into schema.sql. Open the schema.sql and add tables creation statements:
DROP TABLE IF EXISTS roles; DROP TABLE IF EXISTS customers; create table customers ( enabled boolean, email varchar(255) not null, name varchar(255), pwd varchar(255), primary key (email) ); create table roles ( role varchar(255) not null, username varchar(255) not null, FOREIGN KEY (username) REFERENCES customers (email) );Define test Customers in data.sql script. Open the schema.sql and add:
insert into CUSTOMERS (NAME, EMAIL, PWD, ENABLED) values ('user','user@email.com','{bcrypt}$2a$10$9lPjTDy6jYJmbqaKsV28Geu7jRCwqGuaTu87whuY8fuc9d/eALngq',True); insert into CUSTOMERS (NAME, EMAIL, PWD, ENABLED) values ('admin','admin@email.com','{bcrypt}$2a$10$IiRuhM/2DTdq4f5HvqsXEufyjJOrC7quBUD5MWAzr6BnIVaf5pqpq',True); insert into ROLES (USERNAME, ROLE) values ('user@email.com','USER'); insert into ROLES (USERNAME, ROLE) values ('admin@email.com','USER'); insert into ROLES (USERNAME, ROLE) values ('admin@email.com','ADMIN');Password values are encrypted by bcrypt, which is the default password-hashing function defined in Spring Security.
-
Finally, start the application and test API.
Method Authorization (Access Control)
- @Secured - accepts list of ROLES which has permission to access given method
- @RolesAllowed - standard Java (JSR250) variant of Spring @Secured annotation
- @PreAuthorize - accepts SpEL expression with roles as arguments (hasAnyRole(...), hasRole(...), #username == authentication.principal.username)
- @PostAuthorize - accepts SpEL expression with roles as arguments, and it checks access after method is executed
- @PreFilter - is applied on target method argument of type Java Collection
- @PostFilter - ia applied on return method argument of type Java Collection