Okay, this chapter is titled Securing Your App with Spring Boot, yet we have spent a fair amount of time... NOT securing our app! That is about to change. Thanks to this little bit of restructuring, we can move forward with locking things down as desired.
Let's take a crack at writing some security policies, starting with the chat microservice:
@EnableWebFluxSecurity public class SecurityConfiguration { @Bean SecurityWebFilterChain springWebFilterChain(HttpSecurity http) { return http .authorizeExchange() .pathMatchers("/**").authenticated() .and() .build(); } }
The preceding security policy can be defined as follows:
- @EnableWebFluxSecurity activates the Spring WebFlux security filters needed to secure our application
- @Bean marks the one method as a bean definition
- HttpSecurity.http() lets us define a simple set of authentication and authorization rules
- In this case, every Spring WebFlux exchange (denoted by /**) must be authenticated
This is a nice beginning to define a security policy, but we need some way to track user data to authenticate against. To do so, we need a User domain object and a way to store such data. To minimize our effort at storing user information in a database, let's leverage Spring Data again.
First, we'll create a User domain object like this:
@Data @AllArgsConstructor @NoArgsConstructor public class User { @Id private String id; private String username; private String password; private String[] roles; }
This preceding User class can easily be described as follows:
- @Data uses the Lombok annotation to mark this for getters, setters, equals, toString, and hashCode functions
- @AllArgsConstructor creates a constructor call for all of the attributes
- @NoArgsConstructor creates an empty constructor call
- @Id marks this id field as the key in MongoDB
- username, password, and roles are critical fields required to properly integrate with Spring Security, as shown further in the chapter
To interact with MongoDB, we need to create a Spring Data repository as follows:
public interface UserRepository extends Repository<User, String> { Mono<User> findByUsername(String username); }
This is similar to the other repositories we have built so far in the following ways:
- It extends Spring Data Commons' Repository, indicating that the domain type is User and the ID type is String
- It has one finder needed for security lookups, findByUsername, which is returned as a reactive Mono<User>, signaling Spring Data MongoDB to use reactive MongoDB operations
With this handy repository defined, let's preload some user data into our system by creating an InitUsers class, as shown here:
@Configuration public class InitUsers { @Bean CommandLineRunner initializeUsers(MongoOperations operations) { return args -> { operations.dropCollection(User.class); operations.insert( new User( null, "greg", "turnquist", new String[]{"ROLE_USER", "ROLE_ADMIN"})); operations.insert( new User( null, "phil", "webb", new String[]{"ROLE_USER"})); operations.findAll(User.class).forEach(user -> { System.out.println("Loaded " + user); }); }; } }
This preceding user-loading class can be described as follows:
- @Configuration indicates this class contains bean definitions
- @Bean marks the initializeUsers method as a Spring bean
- initializeUsers requires a copy of the blocking MongoOperations bean defined by Spring Boot's MongoDB autoconfiguration code
- The return type is CommandLineRunner, which we'll supply with a lambda function
- Inside our lambda function, we drop the User based collection, insert two new users, and then print out the collection
Now, let's see how to put that to good use! To hook into Reactive Spring Security, we must implement its UserDetailsRepository interface. This interface is designed to look up a user record through any means necessary and bridge it to Spring Security as a Mono<UserDetails> return type. The solution can be found here:
@Component public class SpringDataUserDetailsRepository implements
UserDetailsRepository { private final UserRepository repository; public SpringDataUserDetailsRepository(UserRepository
repository) { this.repository = repository; } @Override public Mono<UserDetails> findByUsername(String username) { return repository.findByUsername(username) .map(user -> new User( user.getUsername(), user.getPassword(), AuthorityUtils.createAuthorityList(user.getRoles()) )); } }
The previous code can be described as follows:
- It injects a UserRepository we just defined through constructor injection
- It implements the interface's one method, findByUsername, by invoking our repository's findByUsername method and then mapping it onto a Spring Security User object (which implements the UserDetails interface)
- AuthorityUtils.createAuthorityList is a convenient utility to translate a String[] of roles into a List<GrantedAuthority>
- If no such user exists in MongoDB, it will return a Mono.empty(), which is the Reactor equivalent of null
By hooking MongoDB-stored users into Spring Security, we can now attempt to access the system.
When we try to access localhost:8080, we can expect a login prompt, as shown in this screenshot:
This popup (run from an incognito window to ensure there are no cookies or lingering session data) lets us nicely log in to the gateway.