Reactive Spring Security For WebFlux REST Web Services - A Complete Blueprint
In this series of posts, we'll dive into how exactly to configure Spring Security for reactive and stateless WebFlux REST APIs. Specifically, we'll look at how to:
- Have Spring Security pick users from our user table, rather than its default in-memory user-store.
- Support form-login to obtain a token — i.e., a user should be able to call
POST /login
with username and password to receive a token. - Configure authorization at request level, as well as method level.
- Make the API stateless, i.e. not storing the Spring Context in the session.
- Return a 401 Unauthorized response instead of redirecting to a login page when an unauthenticated user tries to access a restricted page, or in case of an authentication failure.
- Configure custom token authentication (for stateless authentication, using Authorization Bearer JWT/JWE tokens).
- Configure stuffs like CSRF, CORS, logout etc.
For code examples, we’ll refer to Spring Lemon. If you haven’t heard of Spring Lemon, you should give it a look. It’s a library encapsulating the sophisticated non-functional code and configuration that’s needed when developing real-world RESTful web services using the Spring framework and Spring Boot.
So, let the adventure begin!
The Basics
Spring Security has documented here a minimal version of configuration for WebFlux applications, which looks as below:
@EnableWebFluxSecurity public class HelloWebfluxSecurityConfig { @Bean public MapReactiveUserDetailsService userDetailsService() { UserDetails user = User.withDefaultPasswordEncoder() .username("user") .password("user") .roles("USER") .build(); return new MapReactiveUserDetailsService(user); } @Bean public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { http .authorizeExchange() .anyExchange().authenticated() // 1 .and() .httpBasic().and() // 2 .formLogin(); // 3 return http.build(); //4 } }
In summary, we'll need to provide a couple of beans:
- A ReactiveUserDetailsService connecting to our user DB
- A SecurityWebFilterChain for configuring access rules etc.
Spring Boot auto configuration
Default beans similar to above get auto configured when using Spring Boot — as documented here. But, to meet our requirements, we of course need to replace those with ours.
Also note that the @EnableWebFluxSecurity
annotation isn't required in Spring Boot applications.
A Better ReactiveUserDetailsService
As you see above, we need to configure a ReactiveUserDetailsService
, so that Spring Security finds our users. A map based user details service is configured above, but in real world we'll need to code it to access our user store. For example, here is a minimal sample derived from Spring Lemon's user details service:
@Service public class MyReactiveUserDetailsService implements ReactiveUserDetailsService { @Autowired private final UserRepository userRepository; @Override public Mono<UserDetails> findByUsername(String username) { return userRepository.findByUsername(username).switchIfEmpty(Mono.defer(() -> { return Mono.error(new UsernameNotFoundException("User Not Found")); })).map(User::toUserDetails); } }
The above code assumes that we have a User
domain class, which has a toUserDetails
method that returns a UserDetails
object. So, we'll need to define the User class and an implementation of UserDetails.
The User class could look something like this::
public class User { private String username; private String password; private Collection<String> roles; // Getters and setters public UserDetails toUserDetails() { // returns a UserDetails object } }
The toUserDetails
method above should return an object that should have implemented UserDetails, looking something like this:
public class MyUserDetails implements UserDetails { private String username; private String password; private Collection<String> roles; @Override public Collection<? extends GrantedAuthority> getAuthorities() { // return authorities derived from roles; } @Override public String getPassword() { return password; } @Override public String getUsername() { return username; } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } }
This being same as traditional Spring Security, here we'll not discuss the details.
So, next step will be configuring our SecurityWebFilterChain
bean, which we'll take up in the next post.
1 comments