1. Overview
In this article we’ll implement a basic registration process with Spring Security.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)
- password (and password confirmation field)
- message tag: to display customized and i18n (internationalized) messages with values extracted form the application´s property file.
- form tag: to enable the binding of input fields with the model (as explained in the next section).
Example 2.1.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
| <!DOCTYPE html><%@ page contentType="text/html;charset=UTF-8" language="java"%><%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%><%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%><%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%><%@ page session="false"%><html><head><meta http-equiv="Content-Type" content="text/html; charset=US-ASCII"><title><spring:message code="label.form.title"></spring:message></title></head><body> <H1> <spring:message code="label.form.title"></spring:message> </H1> <form:form modelAttribute="user" method="POST" enctype="utf8"> <br> <tr> <td><label><spring:message code="label.user.firstName"></spring:message> </label> </td> <td><form:input path="firstName" value="" /></td> <form:errors path="firstName" element="div"/> </tr> <tr> <td><label><spring:message code="label.user.lastName"></spring:message> </label> </td> <td><form:input path="lastName" value="" /></td> <form:errors path="lastName" element="div" /> </tr> <tr> <td><label><spring:message code="label.user.email"></spring:message> </label> </td> <td><form:input path="email" value="" /></td> <form:errors path="email" element="div" /> </tr> <tr> <td><label><spring:message code="label.user.password"></spring:message> </label> </td> <td><form:input path="password" value="" type="password" /></td> <form:errors path="password" element="div" /> </tr> <tr> <td><label><spring:message code="label.user.confirmPass"></spring:message> </label> </td> <td><form:input path="matchingPassword" value="" type="password" /></td> <form:errors element="div" /> </tr> <button type="submit"><spring:message code="label.form.submit"></spring:message> </button> </form:form> <br> <a href="<c:url value="login.html" />"> <spring:message code="label.form.loginLink"></spring:message> </a></body></html> |
2.1. Binding a User to the Registration Form
To bind the registration form to the model object (the User DTO) we’re going to make use of Spring’s form tag<form:form modelAttribute="user" method="POST" >
Next – we’re going to bind each input field in the registration form to a field in the model object (with the corresponding getter and setter) :
1
2
3
4
5
6
7
| <form:form modelAttribute="user" method="POST" enctype="utf8"> <tr> <td><label>First Name:</label></td> <td><form:input path="firstName" /></td> </tr> ...</form:form> |
2.2. Displaying Errors in the Registration Page
We’re also displaying errors using the <form:errors > elements – these will come from the various validations we’re going to implement on the server side.Notice the <form: errors path=”nameOfFiled”…> element. These are field level errors. Filed level errors are related to format, empty or null value errors in the filed itself. They’re generated if field input data does not comply with a validation rule.
Also notice the <form:errors … > element without a path. These are global errors – errors at the level of the entire DTO object.
A global error in our example would be generated if our UserDto object does not comply with the validation rule that password and matchingPassword field values should match. So, with global errors the validator’s isValid method will take a UserDto object (not just a field name) as an argument since it has to compare two of its fields.
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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| 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} |
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
1
2
3
4
5
6
| @RequestMapping(value = "/user/registration", method = RequestMethod.GET)public String showRegistrationForm(WebRequest request, Model model) { UserDto userDto = new UserDto(); model.addAttribute("user", userDto); return "registration";} |
5. Validating Registration Data
Next – let’s look at the validations that the controller will perform when registering a new account:- All required fields are filled (No empty or null fields)
- The email address is valid (well formed)
- The password confirmation field matches the password field
- 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:
1
2
3
4
5
| 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: myaddress@myserver 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
1
2
3
4
5
6
7
8
9
| @Target({TYPE, FIELD, ANNOTATION_TYPE}) @Retention(RUNTIME)@Constraint(validatedBy = EmailValidator.class)@Documentedpublic @interface ValidEmail { String message() default "Invalid email"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {};} |
Example 5.2.2. – The Custom EmailValidator:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| 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(); }} |
1
2
3
4
| @ValidEmail@NotNull@NotEmptyprivate 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
1
2
3
4
5
6
7
8
9
| @Target({TYPE,ANNOTATION_TYPE}) @Retention(RUNTIME)@Constraint(validatedBy = PasswordMatchesValidator.class)@Documentedpublic @interface PasswordMatches { String message() default "Passwords don't match"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {};} |
The custom validator that will be called by this annotation is shown below:
Example 5.3.2. The PasswordMatchesValidator Custom Validator
1
2
3
4
5
6
7
8
9
10
| 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()); } } |
1
2
3
4
| @PasswordMatchespublic class UserDto { ...} |
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| @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;} |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| @Servicepublic 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; }} |
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| @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;} |
- The controller is returning a ModelAndView object which is the convenient class for sending model data (user) tied to the view.
- The controller will redirect to the registration form if there are any errors set at validation time.
- The createUserAccount method calls the UserService for data persistence. We will discuss the UserService implementation in the following section
7. The UserService – Register Operation
Lets finish the implementation of the registration operation int the UserService:Example 7.1. The IUserService Interface
1
2
3
4
| public interface IUserService { User registerNewUserAccount(UserDto accountDto) throws EmailExistsException;} |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| @Servicepublic 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.setRole(new Role(Integer.valueOf(1), 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 hardcoded 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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
| @Service@Transactionalpublic 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.getRole().getRole())); } private Collection<? extends GrantedAuthority>getAuthorities(Integer role){ List<GrantedAuthority> authList = getGrantedAuthorities(getRoles(role)); return authList; } private List<String> getRoles(Integer role) { List<String> roles = new ArrayList<String>(); if (role.intValue() == 1) { roles.add("ROLE_USER"); roles.add("ROLE_ADMIN"); } else if (role.intValue() == 2) { roles.add("ROLE_USER"); } return roles; } private static List<GrantedAuthority> getGrantedAuthorities (List<String> roles) { List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>(); 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
1
2
3
4
| <authentication-manager> <authentication-provider user-service-ref="userDetailsService" /> </authentication-manager> <beans:bean id="userDetailsService" class="org.baeldung.security.MyUserDetailsService"/> |