If you have a few years of experience in the Java ecosystem, and you're interested in sharing that experience with the community (and getting paid for your work of course), have a look at the "Write for Us" page. Cheers. Eugen

The new Certification Class of Learn Spring Security is out:

>> CHECK OUT THE COURSE


This article is part of a series:
Spring Security Registration Tutorial
The Registration Process With Spring Security
Registration – Activate a New Account by EmailSpring Security Registration – Resend Verification EmailRegistration with Spring Security – Password EncodingThe Registration API becomes RESTfulSpring Security – Reset Your PasswordRegistration – Password Strength and RulesUpdating your Password

1. Overview

In this article, we’ll implement a basic registration process with Spring Security. This is building on top of concepts explored in the previous article, where we looked at login.

The goal here is to add a full registration process that allows a user to sign up, validates and persists user data.

2. The Registration Page

First – let’s implement a simple registration page displaying the following fields:

  • name (first and last name)
  • email
  • password (and password confirmation field)

The following example shows a simple registration.html page:

Example 2.1.

<html>
<body>
<h1 th:text="#{label.form.title}">form</h1>
<form action="/" th:object="${user}" method="POST" enctype="utf8">
    <div>
        <label th:text="#{label.user.firstName}">first</label>
        <input th:field="*{firstName}"/>
        <p th:each="error: ${#fields.errors('firstName')}" 
          th:text="${error}">Validation error</p>
    </div>
    <div>
        <label th:text="#{label.user.lastName}">last</label>
        <input th:field="*{lastName}"/>
        <p th:each="error : ${#fields.errors('lastName')}" 
          th:text="${error}">Validation error</p>
    </div>
    <div>
        <label th:text="#{label.user.email}">email</label>
        <input type="email" th:field="*{email}"/>
        <p th:each="error : ${#fields.errors('email')}" 
          th:text="${error}">Validation error</p>
    </div>
    <div>
        <label th:text="#{label.user.password}">password</label>
        <input type="password" th:field="*{password}"/>
        <p th:each="error : ${#fields.errors('password')}" 
          th:text="${error}">Validation error</p>
    </div>
    <div>
        <label th:text="#{label.user.confirmPass}">confirm</label>
        <input type="password" th:field="*{matchingPassword}"/>
    </div>
    <button type="submit" th:text="#{label.form.submit}">submit</button>
</form>

<a th:href="@{/login.html}" th:text="#{label.form.loginLink}">login</a>
</body>
</html>

3. The User DTO Object

We need a Data Transfer Object to send all of the registration information to our Spring backend. The DTO object should have all the information we’ll require later on when we create and populate our User object:

public class UserDto {
    @NotNull
    @NotEmpty
    private String firstName;
    
    @NotNull
    @NotEmpty
    private String lastName;
    
    @NotNull
    @NotEmpty
    private String password;
    private String matchingPassword;
    
    @NotNull
    @NotEmpty
    private String email;
    
    // standard getters and setters
}

Notice we used standard javax.validation annotations on the fields of the DTO object. Later on, we’re going to also implement our own custom validation annotations to validate the format of the email address as well as for the password confirmation. (see Section 5)

4. The Registration Controller

A Sign-Up link on the login page will take the user to the registration page. This back end for that page lives in the registration controller and is mapped to “/user/registration”:

Example 4.1. – The showRegistration Method

@RequestMapping(value = "/user/registration", method = RequestMethod.GET)
public String showRegistrationForm(WebRequest request, Model model) {
    UserDto userDto = new UserDto();
    model.addAttribute("user", userDto);
    return "registration";
}

When the controller receives the request “/user/registration”, it creates the new UserDto object that will back the registration form, binds it and returns – pretty straightforward.

5. Validating Registration Data

Next – let’s look at the validations that the controller will perform when registering a new account:

  1. All required fields are filled (No empty or null fields)
  2. The email address is valid (well-formed)
  3. The password confirmation field matches the password field
  4. The account doesn’t already exist

5.1. The Built-In Validation

For the simple checks, we’ll use the out of the box bean validation annotations on the DTO object – annotations like @NotNull, @NotEmpty, etc.

To trigger the validation process, we’ll simply annotate the object in the controller layer with the @Valid annotation:

public ModelAndView registerUserAccount(
  @ModelAttribute("user") @Valid UserDto accountDto, 
  BindingResult result, WebRequest request, Errors errors) {
    ...
}

5.2. Custom Validation to Check Email Validity

Next – let’s validate the email address and make sure it’s well-formed. We’re going to be building a custom validator for that, as well as a custom validation annotation – let’s call that @ValidEmail.

A quick sidenote here – we’re rolling our own custom annotation instead of Hibernate’s @Email because Hibernate considers the old intranet addresses format: [email protected] as valid (see Stackoverflow article), which is no good.

Here’s the email validation annotation and the custom validator:

Example 5.2.1. – The Custom Annotation for Email Validation

@Target({TYPE, FIELD, ANNOTATION_TYPE}) 
@Retention(RUNTIME)
@Constraint(validatedBy = EmailValidator.class)
@Documented
public @interface ValidEmail {   
    String message() default "Invalid email";
    Class<?>[] groups() default {}; 
    Class<? extends Payload>[] payload() default {};
}

Note that we have defined the annotation at the FIELD level – since that’s where it applies conceptually.

Example 5.2.2. – The Custom EmailValidator:

public class EmailValidator 
  implements ConstraintValidator<ValidEmail, String> {
    
    private Pattern pattern;
    private Matcher matcher;
    private static final String EMAIL_PATTERN = "^[_A-Za-z0-9-+]+
        (.[_A-Za-z0-9-]+)*@" + "[A-Za-z0-9-]+(.[A-Za-z0-9]+)*
        (.[A-Za-z]{2,})$"; 
    @Override
    public void initialize(ValidEmail constraintAnnotation) {       
    }
    @Override
    public boolean isValid(String email, ConstraintValidatorContext context){   
        return (validateEmail(email));
    } 
    private boolean validateEmail(String email) {
        pattern = Pattern.compile(EMAIL_PATTERN);
        matcher = pattern.matcher(email);
        return matcher.matches();
    }
}

Let’s now use the new annotation on our UserDto implementation:

@ValidEmail
@NotNull
@NotEmpty
private String email;

5.3. Using Custom Validation for Password Confirmation

We also need a custom annotation and validator to make sure that the password and matchingPassword fields match up:

Example 5.3.1. – The Custom Annotation for Validating Password Confirmation

@Target({TYPE,ANNOTATION_TYPE}) 
@Retention(RUNTIME)
@Constraint(validatedBy = PasswordMatchesValidator.class)
@Documented
public @interface PasswordMatches { 
    String message() default "Passwords don't match";
    Class<?>[] groups() default {}; 
    Class<? extends Payload>[] payload() default {};
}

Notice that the @Target annotation indicates that this is a TYPE level annotation. This is because we need the entire UserDto object to perform the validation.

The custom validator that will be called by this annotation is shown below:

Example 5.3.2. The PasswordMatchesValidator Custom Validator

public class PasswordMatchesValidator 
  implements ConstraintValidator<PasswordMatches, Object> { 
    
    @Override
    public void initialize(PasswordMatches constraintAnnotation) {       
    }
    @Override
    public boolean isValid(Object obj, ConstraintValidatorContext context){   
        UserDto user = (UserDto) obj;
        return user.getPassword().equals(user.getMatchingPassword());    
    }     
}

Now, the @PasswordMatches annotation should be applied to our UserDto object:

@PasswordMatches
public class UserDto {
   ...
}

All custom validations are of course evaluated along with all standard annotations when the entire validation process runs.

5.4. Check That The Account Doesn’t Already Exist

The fourth check we’ll implement is verifying that the email account doesn’t already exist in the database.

This is performed after the form has been validated and it’s done with the help of the UserService implementation.

Example 5.4.1. – The Controller’s createUserAccount Method Calls the UserService Object

@RequestMapping(value = "/user/registration", method = RequestMethod.POST)
public ModelAndView registerUserAccount
      (@ModelAttribute("user") @Valid UserDto accountDto, 
      BindingResult result, WebRequest request, Errors errors) {    
    User registered = new User();
    if (!result.hasErrors()) {
        registered = createUserAccount(accountDto, result);
    }
    if (registered == null) {
        result.rejectValue("email", "message.regError");
    }
    // rest of the implementation
}
private User createUserAccount(UserDto accountDto, BindingResult result) {
    User registered = null;
    try {
        registered = service.registerNewUserAccount(accountDto);
    } catch (EmailExistsException e) {
        return null;
    }    
    return registered;
}

Example 5.4.2. – UserService Checks for Duplicate Emails

@Service
public class UserService implements IUserService {
    @Autowired
    private UserRepository repository; 
    
    @Transactional
    @Override
    public User registerNewUserAccount(UserDto accountDto) 
      throws EmailExistsException {
        
        if (emailExist(accountDto.getEmail())) {  
            throw new EmailExistsException(
              "There is an account with that email adress: "
              +  accountDto.getEmail());
        }
        ...
        // the rest of the registration operation
    }
    private boolean emailExist(String email) {
        User user = repository.findByEmail(email);
        if (user != null) {
            return true;
        }
        return false;
    }
}

The UserService relies on the UserRepository class to check if a user with a given email address already exists in the database.

Now – the actual implementation of the UserRepository in the persistence layer isn’t relevant for the current article. One quick way is, of course, to use Spring Data to generate the repository layer.

6. Persisting Data and Finishing-Up Form Processing

Finally – let’s implement the registration logic in our controller layer:

Example 6.1.1. – The RegisterAccount Method in the Controller

@RequestMapping(value = "/user/registration", method = RequestMethod.POST)
public ModelAndView registerUserAccount(
  @ModelAttribute("user") @Valid UserDto accountDto, 
  BindingResult result, 
  WebRequest request, 
  Errors errors) {
    
    User registered = new User();
    if (!result.hasErrors()) {
        registered = createUserAccount(accountDto, result);
    }
    if (registered == null) {
        result.rejectValue("email", "message.regError");
    }
    if (result.hasErrors()) {
        return new ModelAndView("registration", "user", accountDto);
    } 
    else {
        return new ModelAndView("successRegister", "user", accountDto);
    }
}
private User createUserAccount(UserDto accountDto, BindingResult result) {
    User registered = null;
    try {
        registered = service.registerNewUserAccount(accountDto);
    } catch (EmailExistsException e) {
        return null;
    }
    return registered;
}

Things to notice in the code above:

  1. The controller is returning a ModelAndView object which is the convenient class for sending model data (user) tied to the view.
  2. The controller will redirect to the registration form if there are any errors set at validation time.
  3. The createUserAccount method calls the UserService for data persistence. We will discuss the UserService implementation in the following section

7. The UserService – Register Operation

Let’s finish the implementation of the registration operation int the UserService:

Example 7.1. The IUserService Interface

public interface IUserService {
    User registerNewUserAccount(UserDto accountDto)     
      throws EmailExistsException;
}

Example 7.2. – The UserService Class

@Service
public class UserService implements IUserService {
    @Autowired
    private UserRepository repository;
    
    @Transactional
    @Override
    public User registerNewUserAccount(UserDto accountDto) 
      throws EmailExistsException {
        
        if (emailExist(accountDto.getEmail())) {   
            throw new EmailExistsException(
              "There is an account with that email address:  + accountDto.getEmail());
        }
        User user = new User();    
        user.setFirstName(accountDto.getFirstName());
        user.setLastName(accountDto.getLastName());
        user.setPassword(accountDto.getPassword());
        user.setEmail(accountDto.getEmail());
        user.setRoles(Arrays.asList("ROLE_USER"));
        return repository.save(user);       
    }
    private boolean emailExist(String email) {
        User user = repository.findByEmail(email);
        if (user != null) {
            return true;
        }
        return false;
    }
}

8. Loading User Details for Security Login

In our previous article, login was using hard coded credentials. Let’s change that and use the newly registered user information and credentials. We’ll implement a custom UserDetailsService to check the credentials for login from the persistence layer.

8.1. The Custom UserDetailsService

Let’s start with the custom user details service implementation:

@Service
@Transactional
public class MyUserDetailsService implements UserDetailsService {
 
    @Autowired
    private UserRepository userRepository;
    // 
    public UserDetails loadUserByUsername(String email)
      throws UsernameNotFoundException {
 
        User user = userRepository.findByEmail(email);
        if (user == null) {
            throw new UsernameNotFoundException(
              "No user found with username: "+ email);
        }
        boolean enabled = true;
        boolean accountNonExpired = true;
        boolean credentialsNonExpired = true;
        boolean accountNonLocked = true;
        return  new org.springframework.security.core.userdetails.User
          (user.getEmail(), 
          user.getPassword().toLowerCase(), enabled, accountNonExpired, 
          credentialsNonExpired, accountNonLocked, 
          getAuthorities(user.getRoles()));
    }
    
    private static List<GrantedAuthority> getAuthorities (List<String> roles) {
        List<GrantedAuthority> authorities = new ArrayList<>();
        for (String role : roles) {
            authorities.add(new SimpleGrantedAuthority(role));
        }
        return authorities;
    }
}

8.2. Enable the New Authentication Provider

To enable the new user service in the Spring Security configuration – we simply need to add a reference to the UserDetailsService inside the authentication-manager element and add the UserDetailsService bean:

Example 8.2.- The Authentication Manager and the UserDetailsService

<authentication-manager>
    <authentication-provider user-service-ref="userDetailsService" /> 
</authentication-manager>
 
<beans:bean id="userDetailsService" 
  class="org.baeldung.security.MyUserDetailsService"/>

Or, via Java configuration:

@Autowired
private MyUserDetailsService userDetailsService;

@Override
protected void configure(AuthenticationManagerBuilder auth) 
  throws Exception {
    auth.userDetailsService(userDetailsService);
}

9. Conclusion

And we’re done – a complete and almost production ready registration process implemented with Spring Security and Spring MVC. Next, we’re going to discuss the process of activating the newly registered account by verifying the email of the new user.

The implementation of this Spring Security REST Tutorial can be found in the GitHub project – this is an Eclipse based project, so it should be easy to import and run as it is.

Go deeper into Spring Security with the course:

>> LEARN SPRING SECURITY

Sort by:   newest | oldest | most voted
Matt Krevs
Guest

Nice one. I didnt realise you could roll your own annotations for validation rules. I _think_ there is one typo. You specified @ValidUEmail as the annotation for the email field. Shouldnt it be @ValidEmail?

Elena Eidson
Guest

Yes Matt, It’s a typo. @ValidEmail is right.

Ashwani kumar
Guest

Great article…. Can u share the demo project for this article…? like github project…

Eugen Paraschiv
Guest

Hey Kumar, I forgot to add the link – I’ll add it into the article today. Cheers,
Eugen.

Guest
Guest

Nice Article… Can u share the demo project for this article…? like github project…

Simon
Guest

I notice that the password is currently stored plaintext in the User entity, and corresponding database table. I hope there will be a follow-up to this tutorial, showing how to this properly – i.e how to store a hash instead of the actual password, and how to integrate this with Spring-Security?

Eugen Paraschiv
Guest

Hey Simon – yes, the followup is planned but it’s definitely out of scope for this particular article. It is in the pipeline though. Thanks.
Eugen.

Simon
Guest

Good to hear. It’s seriously important, that stuff… for all that tutorials need to start simple and build up the complexity, I wonder if it would be better with this subject to *never* show people how to do the plaintext version. There are almost no cases where it’s desirable behaviour, and it’s such a dangerous thing to teach…

Eugen Paraschiv
Guest

My thinking is that readers understand that this is a tutorial and some aspects that are out of the scope of the article are simplified or not shown. If someone simply picks up this code and puts it into a production system without a second thought – then I would say that system has bigger problems than passwords stored in plaintext 🙂 Hope that makes sense. Cheers,
Eugen.

john muteti
Guest

This ia a great job, i have been searching for user registration for long. Thankyou

wpDiscuz