Spring Security is responsible for authenticate and authorize Spring applications, it provide multiple way of doing authentication but Basic is probably the simplest.
With basic authentication, for every request made to the api, we send the user credentials in the headers, generally encoded with Base64 format.
Creating the Spring Boot application
We keep it simple with only spring web and spring security.
We will add a simple GET request:
package com.example.spring_basic;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class DemoController {
@GetMapping("/hello")
public String hello(){
return "hello world !";
}
}
If we run the application and we inspect the logs, we can see that Spring is already handling security with a generated password
We can already test the request, for instance with IntelliJ Http request, and see that it works:
### GET request to example server
GET http://localhost:8080/hello
Authorization: Basic user <Generated password>
However this is not what we want, so let's add a new Spring Security Configuration:
package com.example.spring_basic;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import static org.springframework.security.config.Customizer.withDefaults;
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity.httpBasic(withDefaults());
httpSecurity.authorizeHttpRequests(http ->{
http.requestMatchers("/users").permitAll();
http.anyRequest().authenticated();
});
return httpSecurity.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public InMemoryUserDetailsManager inMemoryUserDetailsManager() {
UserDetails user= User.builder().username("user")
.password(passwordEncoder().encode("password"))
.roles("USER").build();
UserDetails admin=User.builder().username("admin")
.password(passwordEncoder().encode("password"))
.roles("USER","ADMIN").build();
return new InMemoryUserDetailsManager(user, admin);
}
}
First we recreate a SecurityFilterChain bean, for now we only add basic auth and require all requests to be authenticated.
Then we declare a PasswordEncoder bean that will be used to encode the password during the security process.
Then we declare a UserDetails implementation. For the moment we will not create custom UserDetails and User. You can see that we use the passwordEncoder to encode the "password" value. So when we will compare the provided password with the user password we will compare the encoded version of the password.
We can test the request by providing credentials:
### GET request to example server
GET http://localhost:8080/hello
Authorization: Basic user password
Use a database to store user
If we want to use a real database we must first add dependencies, in this case we will use spring data jpa and h2 file database
in pom.xml:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.3.232</version>
</dependency>
With this in application.properties
spring.datasource.url=jdbc:h2:file:./data/demo
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=password
We will add a new User. We will implement UserDetails interface and to keep it simple, we will not add roles in our user case.
@Entity
@Table(name = "users")
public class User implements UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String password;
public Long getId() {
return id;
}
public User setId(Long id) {
this.id = id;
return this;
}
public String getUsername() {
return username;
}
public User setUsername(String username) {
this.username = username;
return this;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of(new SimpleGrantedAuthority("ROLE_USER"));
}
public String getPassword() {
return password;
}
public User setPassword(String password) {
this.password = password;
return this;
}
}
We will create a repository for this user:
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUsername(String username);
}
And a Controller to create new users. Pay attention to inject a passwordEncoder to encrypt the password before saving it to database:
@RestController
@RequestMapping("/users")
public class UserController {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
public UserController(UserRepository userRepository, PasswordEncoder passwordEncoder) {
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
}
@PostMapping
public User createUser(@RequestBody User user) {
user.setPassword(passwordEncoder.encode(user.getPassword()));
return userRepository.save(user);
}
}
We will remove the InMemoryUserDetailsManager bean inside the SecurityConfig file, and create a new UserDetailService class
@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 {
return userRepository.findByUsername(username).orElse(null);
}
}
With this http request
POST http://localhost:8080/users
Content-Type: application/json
{
"username": "user",
"password": "password"
}
It should works, but how ?
Behind the scene
When we make the request, it flows through a filter chain with differents filters, when a filter detects the headers he wants, he can call the AuthenticationManager. In our case, when the BasicAuthenticationFilter receive a request with username and password, it call the AuthenticationManager with a non valided UsernamePasswordToken.
The AuthenticationManager will then find a provider that handle this token, in our case the DaoAuthenticationProvider. It will use the UserDetails to find User with the same username as the credentials, then encode the password and compare it with the credentials password. If it matchs, it will update the token, add it to the SecurityContext, and return the token to the manager, then to the filter.
You can find the source code here
Top comments (0)