This is the third article in a series on authentication with Spring Boot.
In the first article we authenticated using social networks, and allowed any user to access our application.
In the second article we used inMemoryAuthentication for users that used the login form. In essence, we hardcoded our users.
This article is about adding users to a database. We are not going to allow users to sign up, we’re just going to add the users manually.
Setup Postgres
For our user entity, we want to save the following fields:
- username
- password (optional)
- role
- name
You can add the clientIds for the social networks that you allow your users to connect with for extra security. But we won’t do that here.
But we do want to be the email to be unique. Every user must have his own email propperty.
CREATE TABLE public."user" ( user_name text NOT NULL, password text, role text NOT NULL, email text NOT NULL, name text, UNIQUE(email) )
To test this our login later, we need to add a user. The password is BCrypt encoded for “password”.
insert into "user" (user_name, password, role, email, name) values ('user','$2a$10$bXetyuwpEai6LomSykjZAuQ5mxU8WqhMBXGuWYnxlveCySRlGxh2i', 'USER', 'test@example.com', 'Test User')
JPA Database access
Once we have the database in place, it would be nice to actually use it in our code. To do this, we need to configure Spring to connect to our database, create a representation of the database in our code, and create a Repository that glues it together.
First, the configuration. Since we created the database schema ourselves, we don’t want Hibernate to do that. You could set spring.jpa.hibernate.ddl-auto to ‘validate’ to make sure the schema matches your model. Also, we want to show the SQL it is executing, so it’s easier to see what’s wrong. You want to turn this off when you’re done, because it generates a lot of logging.
spring: jpa: hibernate: ddl-auto: none show-sql: true datasource: url: jdbc:postgresql://localhost:5432/database username: databaseuser password: password
For the model we’re going to use Lombok, so we don’t have to deal with the boilerplate code of getters and setters. We are using the Java Persistance API to handle the database mapping.
@Getter @Setter @Entity @Table(name="user", schema = "public") public class User { @Id private String email; @Column(name="user_name") private String userName; private String name; private String password; private String role; }
We use a Repository to get the User objects from the database. Spring will do a lot of magic for us, so we only need to specify the JPA query and a method in an interface to retrieve the data.
@Repository public interface UserRepository extends CrudRepository<User, String> { @Query("SELECT u FROM User u WHERE u.userName = :username") User getUserByUsername(String username); @Query("SELECT u FROM User u WHERE u.email = :email") User getUserByEmail(String email); }
UserDetails and UserDetailsService
At this point we need to have UserDetails, and a service to get them from the database. Since we’re using bot FormLogin and OAuth2, I’ve decided to implement both UserDetails and OAuth2User in the same class. This makes things easier later on.
@Repository public class MyUserDetails implements UserDetails, OAuth2User { private final User user; public MyUserDetails(User user){ this.user = user; } @Override public Map<String, Object> getAttributes() { return Collections.emptyMap(); } @Override public Collection<? extends GrantedAuthority> getAuthorities() { SimpleGrantedAuthority authority = new SimpleGrantedAuthority(user.getRole()); return Collections.singletonList(authority); } @Override public String getPassword() { return user.getPassword(); } @Override public String getUsername() { return user.getUserName(); } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } @Override public String getName() { return user.getName(); } }
The service tries to load a user by username, and throws an exception when no user with that username could be found.
@Component public class MyUserDetailsService implements UserDetailsService { private final UserRepository userRepository; @Autowired public MyUserDetailsService(UserRepository userRepository) { this.userRepository = userRepository; } @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userRepository.getUserByUsername(username); if (user == null) { throw new UsernameNotFoundException("Could not find user"); } return new MyUserDetails(user); } }
Update FormLogin configuration
Previously we configured in-memory authentication. Now that we have all pieces in place to retrieve our users from the database, we need to configure it.
We need to configure the UserDetailsService.
@Bean public UserDetailsService userDetailsService(){ return new MyUserDetailsService(userRepository); }
Then we’ll configure a DaoAuthenticationProvider using the UserDetailService. The passwordEncoder was already configured in the last blogpost.
@Bean public DaoAuthenticationProvider authenticationProvider() { DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); authProvider.setUserDetailsService(userDetailsService()); authProvider.setPasswordEncoder(passwordEncoder()); return authProvider; }
And then to tie it toghether, we use this AuthenticationProvider as the source of the users for the LoginForm
@Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.authenticationProvider(authenticationProvider()); }
Update OAuth configuration
Since we now use the database to store our users, we also need to update the OAuth configuration. We need to verify whether the user who’s trying to login using OAuth is actually known to us. The key information that we can use here is the email address. That’s why there is a method to get the user by email address in the repository, which we are going to use here. If there is no user with the email address that was found in the OAuth2 principle, we throw an exception. Otherwise, we return that user.
@Bean public OAuth2UserService<OAuth2UserRequest, OAuth2User> oauth2UserService() { DefaultOAuth2UserService delegate = new DefaultOAuth2UserService(); return request -> { OAuth2User auth2User = delegate.loadUser(request); String email = auth2User.getAttribute("email"); User user = userRepository.getUserByEmail(email); if (user != null){ return new MyUserDetails(user); } throw new InternalAuthenticationServiceException("User not registered"); }; }
Update WebController and frontend
Now that we have all the pieces in place to use the database for user verification, we want to use this information on our site. Since there will be problems with some login attempts (maybe the user misspelled his username), we would like to be able to show an error message on the login page. So we update the /login endpoint like this:
@RequestMapping(value = "/login") public String login(HttpServletRequest request, Model model){ if (request.getSession().getAttribute("error.message")!= null) { String errorMessage = request.getSession().getAttribute("error.message").toString(); log.info("Error message: "+errorMessage); model.addAttribute("errormessage", errorMessage); } return "login"; }
On the login page, we need to add the following to display this error message:
<div class="alert alert-danger" role="alert" th:if="${errormessage}"> <span id="user" th:text="${errormessage}"></span> </div>
In other places we would like to get the user’s name. To do this, we need to get the principal from the authentication token.
private Optional<MyUserDetails> extractMyUserDetails(Principal principal){ if (principal instanceof UsernamePasswordAuthenticationToken) { return Optional.of((MyUserDetails) ((UsernamePasswordAuthenticationToken) principal).getPrincipal()); } else if (principal instanceof OAuth2AuthenticationToken){ return Optional.of((MyUserDetails) ((OAuth2AuthenticationToken) principal).getPrincipal()); } log.severe("Unknown Authentication token type!"); return Optional.empty(); }
And then we get the username from the MyUserDetails class
@RequestMapping(value = "/welcome") public String welcome(Principal principal, Model model) { MyUserDetails userDetails = extractMyUserDetails(principal) .orElseThrow(IllegalStateException::new); model.addAttribute("name", userDetails.getName()); return "welcome"; }
Eén reactie op “Spring Boot – Load users from database”
I think, that you commit an error.