Session 9.2: Spring Boot Security - Authentication and Authorization through JPA/DB
1. Clone the following repository
$ git clone https://github.com/M-Gharib/ESI-W9.2.git
If you want to create a new Spring Boot project from scratch, you need to install the following dependencies for both the Product and Inventory services:
- Spring Web
- Spring Security
- Spring Data JPA SQL
- PostgresSQL Driver SQL;
- Lombok
The application has the following structure.
week10 └── config └── MyUserDetails.java └── MyUserDetailsService.java └── SecurityConfig.java └── controller └── userController.java └── model └── User.java └── repository └── userRepository.java └── service └── userService.java
2. Check the code in User.java
, which is used to create a table in the Postgres database to store the users and their credentials. Note how we are using a Universally Unique IDentifier (UUID)
for the Id field, and how we are generating its value automatically.
3. Check the code in userController.java
, userService.java
, and userRepository.java
. There is only a new request handler for adding a new user in userController.java
, and a corresponding function addUser(User user)
in userService.java
. Note how we are encoding the password before adding the user to the database in the addUser(User user)
function.
4. Check the code in SecurityConfig.java
,
passwordEncoder
is the same one we used in the in memory auth example.UserDetailsService
, unlike in memory auth, UserDetailsService returns an instance of ouruserDetailsService
(MyUserDetailsService()
).securityFilterChain(HttpSecurity http)
is almost the same as the one we used in the in-memory example, except we are using.hasAuthority
instead of.hasAnyRole
since we are using Authority in ourUserDetails
(MyUserDetails).authenticationProvider()
we are usingDaoAuthenticationProvider
, which is an AuthenticationProvider implementation that uses a UserDetailsService and PasswordEncoder to authenticate a username and password.
@Configuration @EnableWebSecurity @EnableMethodSecurity public class SecurityConfig { @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean public UserDetailsService userDetailsService() { return new MyUserDetailsService(); } // Note that, unlike in memory authentication, we have changed ".hasAnyRole/.hasRole" to ".hasAuthority" @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { return http .cors(withDefaults()) //.csrf().disable() .csrf(csrf -> csrf.disable()) .authorizeHttpRequests(authorize -> authorize .requestMatchers("/api/public", "/api/new").permitAll() .requestMatchers("/api/admin").hasAuthority("ADMIN") .requestMatchers("/api/user").hasAuthority("USER") .anyRequest().authenticated()) .formLogin(withDefaults()) .httpBasic(withDefaults()) .build(); } @Bean public AuthenticationProvider authenticationProvider(){ DaoAuthenticationProvider authenticationProvider=new DaoAuthenticationProvider(); authenticationProvider.setUserDetailsService(userDetailsService()); authenticationProvider.setPasswordEncoder(passwordEncoder()); return authenticationProvider; } }
5. Check the code in MyUserDetails.java
, the MyUserDetails
class extends the User
class and implements UserDetails
interface as required by Spring Security. Via its getAuthorities() function, it fetches the user roles and assigns them to authority.
public class MyUserDetails extends User implements UserDetails { private User user; public MyUserDetails(final User user) { this.user = user; } @Override public List<? extends GrantedAuthority> getAuthorities() { SimpleGrantedAuthority authority = new SimpleGrantedAuthority(user.getRoles()); return Arrays.asList(authority); } @Override public String getPassword() { return user.getPassword(); } @Override public String getUsername() { return user.getName(); } //hard-coding these attributes @Override public boolean isAccountNonExpired() { return true; } ...
6. Check the code in MyUserDetailsService.java
, the MyUserDetailsService
implements the UserDetailsService
interface as required by Spring Security.
Via its loadUserByUsername() function, it checks whether the user exists in the database, and if it does, it will map its attributes to the MyUserDetails
. If the user does not exist, it will throw an exception.
@Component public class MyUserDetailsService implements UserDetailsService { @Autowired private UserRepository userRepository; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException{ Optional<User> user = userRepository.findByName(username); user .orElseThrow(() -> new UsernameNotFoundException(username + "not found")); return user.map(MyUserDetails::new).get(); } }
Test our application
7. Run your application.
8. create an admin and user users, in the RestClientFile.rest
, there are two HTTP requests, which can be used to create both of these users.
### Add a user with Role/Authority ADMIN POST http://localhost:8090/api/new HTTP/1.1 content-type: application/json { "name": "admin", "password": "admin", "roles": "ADMIN" } ### Add a user with Role/Authority USER POST http://localhost:8090/api/new HTTP/1.1 content-type: application/json { "name": "user", "password": "user", "roles": "USER" }
9. In RestClientFile.rest
, there are three HTTP requests, the first is a public endpoint, i.e., not protected, and the other two requesters are protected and can be accessed by users who have roles of "ADMIN" and "USER" respectively. If you run the following requests in RestClient you will receive a form (an HTML page) as we configure our security (.formLogin(withDefaults())
) to use a form for checking the credentials of a user. Therefore, use the browser when you want to try each of them.
### Public endpoint GET http://localhost:8090/api/public ### Protected endpoint - only admins are allowed - Username: admin Password: admin GET http://localhost:8090/api/admin ### Protected endpoint - only users are allowed - Username: user Password: user GET http://localhost:8090/api/user
Note When you try to log in a cookie will be created and saved in your browser, which may prevent you from trying to log in again using other credentials. In order not to wait until the cookie expires or rerun your application, you can delete this cookie manually, as follows:
right-click (anywhere on the webpage), select Inspect
, and navigate to the application
tab. The cookie will have the name http://localhost:8090
, right-click on it, then, clear. After clearing the cookie, you can try to log in again using any credentials you want.