Adding Social Sign In to a Spring MVC Web Application: Registration and Login
The first part of this tutorial described how we can configure Spring Social 1.1.0 and Spring Security 3.2.0 but it left two very important questions unanswered.
These questions are:
- How can a user create a new user account?
- How can a user log in?
It is time to get our hands dirty and answer to these questions. The requirements of our example application are:
- It must be possible to create a “traditional” user account. This means that the user is authenticated by using username and password.
- It must be possible to create a user account by using a SaaS API provider such as Facebook or Twitter. In this case, the user is authenticated by the SaaS API provider.
- It must be possible to log in by using username and password.
- It must be possible to log in by using a SaaS API provider.
Let’s start fulfilling these requirements. The first thing that we have to do is to create a login page for our application.
Creating the Login Page
The login page of our application has three responsibilities which are described in the following:
- It must provide a way to log in by using username and password.
- It must have a link to the registration page. If a user wants to create “traditional” user account, he can do this by clicking this link.
- It must have the links which start the social sign in flow. These links can be used for two purposes:
- If the user in question has a user account, he can log in by using a SaaS API provider.
- If the user doesn’t have a user account, he can create one by using a SaaS API provider.
The application context configuration which we created in the first part of this tutorial specifies some requirements for our login page. These requirements are:
- If an anonymous user tries to access a protected page, he is redirected to url ‘/login’.
- When the login form of our application is submitted, our application must create a POST request to url ‘/login/authenticate’.
- We must include a CSRF token to the POST request which is created when our login form is submitted. The reason for this is that the CSRF protection of Spring Security 3.2.0 is enabled by default when we configure Spring Security by using Java configuration.
- The name of the username parameter is username. This is the default value of the username parameter when Spring Security is configured by using Java configuration
- The name of the password parameter is password. This the default value value of the password parameter when Spring Security is configured by using Java configuration.
- If a form login fails, the user is redirected to url ‘/login?error=bad_credentials’. This means that when the login page is requested and the value of the of error request parameter is ‘bad_credentials’, we must show an error message to the user.
- The SocialAuthenticationFilter processes GET requests send to url ‘/auth/{provider}’. This means that
- We can start the Facebook sign in flow by sending a GET request to url ‘/auth/facebook’.
- We can start the Twitter sign in flow by sending a GET request to url ‘/auth/twitter’.
Let’s start by creating a controller which renders the login page.
Creating the Controller
We can implement the controller which renders the login page by following these steps:
- Create a LoginController class and annotate the created class with the @Controller annotation.
- Add a showLoginPage() method to the controller class. This method returns the name of the rendered view.
- Implement the showLoginPage() method by following these steps:
- Annotate the method with the @RequestMapping annotation and ensure that the showLoginPage() method processes GET requests send to url ‘/login’.
- Return the name of the login view (‘user/login’).
The source code of the LoginController class looks as follows:
import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; @Controller public class LoginController { @RequestMapping(value = "/login", method = RequestMethod.GET) public String showLoginPage() { return "user/login"; } }
Our next step is to create the login page by using JSP. Let’s see how this is done.
Creating the JSP Page
We can create the login page by following these steps:
- Ensure that that the login form and social sign in buttons are shown only to anonymous users. We can do this by following these steps:
- Wrap the login form and social sign in buttons inside the authorize tag of the Spring Security tag library.
- Set the value of the access attribute to isAnonymous().
- Show an error message if log in fails. We can get the localized error message by using the message tag of the Spring tag library if the value of the request parameter called error is ‘bad_credentials’.
- Implement the login form by following these steps:
- Ensure that when the login form is submitted, a POST request is send to url ‘/login/authenticate’.
- Add CSRF token to the request which is send when the login form is submitted. This is required because we enabled the CSRF protection of Spring Security in the first part of this tutorial.
- Add a username field to the login form.
- Add a password field to the login form.
- Add a submit button to the login form.
- Add ‘Create user account’ link below the login form. This link creates a GET request to url ‘/user/register’ (registration page).
- Add social sign buttons to the login page by following these steps:
- Add Facebook sign in button. This button must create a GET request to url ‘/auth/facebook’.
- Add Twitter sign in button. This button must create a GET request to url ‘/auth/twitter’.
- Ensure that a help message is shown if an authenticated user accesses the login page. We can do this by following these steps:
- Wrap the error message area inside the authorize tag of the Spring Security tag library.
- Set the value of the access attribute to isAuthenticated().
- Get the localized error message by using the message tag of the Spring tag library.
The source code of the login.jsp page looks as follows:
<!DOCTYPE html> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> <%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %> <html> <head> <title></title> <link rel="stylesheet" type="text/css" href="${pageContext.request.contextPath}/static/css/social-buttons-3.css"/> </head> <body> <div class="page-header"> <h1><spring:message code="label.user.login.page.title"/></h1> </div> <!-- If the user is anonymous (not logged in), show the login form and social sign in buttons. --> <sec:authorize access="isAnonymous()"> <!-- Login form --> <div class="panel panel-default"> <div class="panel-body"> <h2><spring:message code="label.login.form.title"/></h2> <!-- Error message is shown if login fails. --> <c:if test="${param.error eq 'bad_credentials'}"> <div class="alert alert-danger alert-dismissable"> <button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button> <spring:message code="text.login.page.login.failed.error"/> </div> </c:if> <!-- Specifies action and HTTP method --> <form action="/login/authenticate" method="POST" role="form"> <!-- Add CSRF token --> <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/> <div class="row"> <div id="form-group-email" class="form-group col-lg-4"> <label class="control-label" for="user-email"><spring:message code="label.user.email"/>:</label> <!-- Add username field to the login form --> <input id="user-email" name="username" type="text" class="form-control"/> </div> </div> <div class="row"> <div id="form-group-password" class="form-group col-lg-4"> <label class="control-label" for="user-password"><spring:message code="label.user.password"/>:</label> <!-- Add password field to the login form --> <input id="user-password" name="password" type="password" class="form-control"/> </div> </div> <div class="row"> <div class="form-group col-lg-4"> <!-- Add submit button --> <button type="submit" class="btn btn-default"><spring:message code="label.user.login.submit.button"/></button> </div> </div> </form> <div class="row"> <div class="form-group col-lg-4"> <!-- Add create user account link --> <a href="/user/register"><spring:message code="label.navigation.registration.link"/></a> </div> </div> </div> </div> <!-- Social Sign In Buttons --> <div class="panel panel-default"> <div class="panel-body"> <h2><spring:message code="label.social.sign.in.title"/></h2> <div class="row social-button-row"> <div class="col-lg-4"> <!-- Add Facebook sign in button --> <a href="<c:url value="/auth/facebook"/>"><button class="btn btn-facebook"><i class="icon-facebook"></i> | <spring:message code="label.facebook.sign.in.button"/></button></a> </div> </div> <div class="row social-button-row"> <div class="col-lg-4"> <!-- Add Twitter sign in Button --> <a href="<c:url value="/auth/twitter"/>"><button class="btn btn-twitter"><i class="icon-twitter"></i> | <spring:message code="label.twitter.sign.in.button"/></button></a> </div> </div> </div> </div> </sec:authorize> <!-- If the user is already authenticated, show a help message instead of the login form and social sign in buttons. --> <sec:authorize access="isAuthenticated()"> <p><spring:message code="text.login.page.authenticated.user.help"/></p> </sec:authorize> </body> </html>
Note: Our application uses Twitter Bootstrap 3. The social sign in buttons are created by using a Twitter Bootstrap plugin called Social Buttons for Twitter Bootstrap 3.
We have now created the login page which fulfils our requirements. The relevant part of our login page looks as follows:
Our next step is to implement the registration function. Let’s get started.
Implementing the Registration Function
The registration function of our example application has two requirements:
- It must be possible to create a “normal” user account.
- It must be possible to create a user account by using social sign in.
Also, the application context configuration which we created in the first part of this tutorial specifies one requirement for the registration function:
The url of the registration page must be ‘/signup’. This is the default value of the sign up (also known as registration) page, and at the moment it is not possible to override this url if we configure the application context by using Java configuration. However, since the url ‘/signup’ looks a bit ugly, we will replace this url with the url ‘/user/register’.
Note: It is possible to override the default value of the sign up url if the application context is configured by using XML configuration files (look for the property called signUpUrl).
The user of our example application can end up to the registration page by using one of the following methods:
- He clicks the ‘Create user account link. This link starts the “normal” registration process.
- He clicks the social sign in button which starts the social sign in flow.
Because it is hard to get the general idea from such a shallow description, I have created a diagram which illustrates the steps a user has to follow before he ends up to the registration page of our example application. This diagram has two rules:
- The grey colour represents actions which are the responsibility of our example application.
- The blue colour represents actions which are the responsibility of the SaaS API provider.
This diagram looks as follows:
Let’s move on and start by creating a form object for the registration form.
Creating the Form Object
The form object is a data transfer object which contains the information entered to the registration form and specifies the validation constraints which are used to validate that information.
Before we implement the form object, let’s take a quick look at the validation constraints which we use to validate our form object. These constraints are described in following:
- The @Email annotation ensures that the email address given by the user is well-formed.
- The @NotEmpty annotation ensures that the value of the field cannot be empty or null.
- The @Size annotation ensures that the length of the field value isn’t longer than the maximum length of the field.
Let’s move on and create the form object. We can do this by following these steps:
- Create a class called RegistrationForm.
- Add an email field to the class and specify its validation constraints by following these rules:
- The email must be well-formed.
- The email cannot be empty or null.
- The maximum length of the email is 100 characters.
- Add a firstName field to the class and specify its validation constraints by following these rules:
- The first name cannot be empty or null.
- The maximum length of the first name is 100 characters.
- Add a lastName field to the class and specify its validation constraints by following these rules:
- The last name cannot be empty or null.
- The maximum length of the last name is 100 characters.
- Add a password field to the class.
- Add a passwordVerification field to the class.
- Add a signInProvider field to the class. The type of this field is SocialMediaService.
- Add a isNormalRegistration() method to created class. This method returns true if the value of the signInProvider field is null. If the value of that field is not null, this method returns false.
- Add a isSocialSignIn() method to the created class. This method returns true if the value of the signInProvider field is not null. If the value of that field is null, this method returns false.
The source code of the RegistrationForm class looks as follows:
import org.hibernate.validator.constraints.Email; import org.hibernate.validator.constraints.NotEmpty; import javax.validation.constraints.Size; @PasswordsNotEmpty( triggerFieldName = "signInProvider", passwordFieldName = "password", passwordVerificationFieldName = "passwordVerification" ) @PasswordsNotEqual( passwordFieldName = "password", passwordVerificationFieldName = "passwordVerification" ) public class RegistrationForm { @Email @NotEmpty @Size(max = 100) private String email; @NotEmpty @Size(max = 100) private String firstName; @NotEmpty @Size(max = 100) private String lastName; private String password; private String passwordVerification; private SocialMediaService signInProvider; //Constructor is omitted for the of clarity. public boolean isNormalRegistration() { return signInProvider == null; } public boolean isSocialSignIn() { return signInProvider != null; } //other methods are omitted for the sake of clarity. }
The SocialMediaService is an enum which identifies the SaaS API provider which was used to authenticate the user. Its source code looks as follows:
public enum SocialMediaService { FACEBOOK, TWITTER }
Wait, didn’t we just forget something?
What on earth are those weird annotations such as @PasswordsNotEqual and @PasswordsNotEmpty?
Well, they are custom bean validation constraints. Let’s find out how we can create these constraints.
Creating the Custom Validation Constraints
We have to create two custom validation constraints for our example application. If the user is creating a “normal” user account, we have to ensure that:
- The password and passwordVerification fields of our form object cannot be empty or null.
- The password and passwordVerification fields are equal.
We can create custom validation constraints by following these steps:
- Create a constraint annotation.
- Implement a custom validator class which ensures that the constraint is not broken.
Note: The reference manual of Hibernate validator 4.2 has more information about creating custom validation constraints.
Let’s start by creating the constraint annotations.
Creating the Constraint Annotations
When we create the constraint annotations, we have to always follow these common steps:
- Create an annotation type. Let’s assume that the name of our annotation type is CommonConstraint.
- Annotate the created annotation type with the @Target annotation and set its value to {ElementType.TYPE, ElementType.ANNOTATION_TYPE} (the Javadoc of the ElementType enum). This means that both classes and annotation types can be annotated with the @CommonConstraint annotation.
- Annotate the created annotation type with the @Retention annotation and set its value to RetentionPolicy.RUNTIME. This means that the @CommonConstraint annotation is available at runtime and it can be read by using reflection.
- Annotate the created annotation type with the @Constraint annotation and set the value of its validatedBy attribute. The value of this attribute specifies the class which validates the classes annotated with the @CommonConstraint annotation.
- Annotate the class with the @Documented annotation. This means that the @CommonConstraint annotation is visible in the Javadoc documentation of all classes which are annotated with it.
- Add a message attribute to the annotation type. The type of this attribute is String, and its default value is ‘CommonConstraint’.
- Add a groups attribute to the annotation type. The type of this attribute is an array of type Class<?>, and its default value is empty array. This attribute allows the creation of validation groups.
- Add a payload attribute to the annotation type. The type of this attribute is an array of type Class<? extends Payload>, and its default value is empty array. This attribute is not used by the Bean Validation API but clients of the API can assign custom PayLoad objects to the constraint.
The source code of the @CommonConstraint annotation looks as follows:
import static java.lang.annotation.ElementType.*; import static java.lang.annotation.RetentionPolicy.*; import javax.validation.Constraint; import javax.validation.Payload; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.Target; @Target( { TYPE, ANNOTATION_TYPE }) @Retention(RUNTIME) @Constraint(validatedBy = CommonConstraintValidator.class) @Documented public @interface CommonConstraint { String message() default “CommonConstraint”; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
Let’s move on and find out how we can create the @PasswordsNotEmpty and @PasswordNotEqual annotations. First, we have to create the @PasswordsNotEmpty annotation. We can do this by following these steps:
- Follow the common steps described earlier and make the following changes to the created annotation:
- Rename the annotation type to PasswordsNotEmpty.
- Set the value of the @Constraint annotation’s validatedBy attribute to PasswordsNotEmptyValidator.class.
- Add a triggerFieldName attribute to the annotation type. The type of this attribute is String, and its default value is empty string. This attribute specifies the name of the field which triggers our custom constraint if its value is null.
- Add a passwordFieldName attribute to the annotation type. The type of this attribute is String, and its default value is empty string. This attribute specifies the name of the field which contains the password of the user.
- Add a passwordVerificationFieldName attribute to to the annotation type. The type of this attribute is String, and its default value is empty string. This attribute specifies the name of the field which contains the password verification of the user.
The source code of the @PasswordsNotEmpty annotation looks as follows:
import static java.lang.annotation.ElementType.*; import static java.lang.annotation.RetentionPolicy.*; import javax.validation.Constraint; import javax.validation.Payload; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.Target; @Target( { TYPE, ANNOTATION_TYPE }) @Retention(RUNTIME) @Constraint(validatedBy = PasswordsNotEmptyValidator.class) @Documented public @interface PasswordsNotEmpty { String message() default "PasswordsNotEmpty"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; String triggerFieldName() default ""; String passwordFieldName() default ""; String passwordVerificationFieldName() default ""; }
Second, we have to create the @PasswordsNotEqual annotation. We can do this by following these steps:
- Follow the common steps described earlier and make the following changes to the created annotation:
- Rename the annotation type to PasswordsNotEqual.
- Set the value of the @Constraint annotation’s validatedBy attribute to PasswordsNotEqualValidator.class.
- Add a passwordFieldName attribute to the annotation type. The type of this attribute is String, and its default value is empty string. This attribute specifies the name of the field which contains the password of the user.
- Add a passwordVerificationFieldName attribute to the annotation type. The type of this attribute is String, and its default value is empty string. This attribute specifies the name of the field which contains the password verification of the user.
The source code of the @PasswordsNotEqual annotation looks as follows:
import javax.validation.Constraint; import javax.validation.Payload; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.Target; import static java.lang.annotation.ElementType.ANNOTATION_TYPE; import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.RetentionPolicy.RUNTIME; @Target( { TYPE, ANNOTATION_TYPE }) @Retention(RUNTIME) @Constraint(validatedBy = PasswordsNotEqualValidator.class) @Documented public @interface PasswordsNotEqual { String message() default "PasswordsNotEqual"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; String passwordFieldName() default ""; String passwordVerificationFieldName() default ""; }
We have now created our constraint annotations. Let’s move on and take a look at a utility class which we use when we are implementing the validator classes for our custom constraint annotations.
Creating the Validation Utility Class
The validation utility class provides two static methods which are described in the following:
- The first method is used to add validation errors to a field of the validated object.
- The second method returns the value of the requested field.
We can implement this class by following these steps:
- Create a class called ValidatorUtil.
- Add a addValidationError() method to the ValidatorUtil class. This method takes two parameters which are described in the following:
- The first parameter is the name of the field.
- The second parameter is a ConstraintValidatorContext object.
- Implement the addValidationError() method by following these steps:
- Create a new constraint violation and ensure that the message specified by the constraint annotation is used as a prefix when the constraint violation message is build.
- Add the field to the constraint validation error.
- Create the constraint validation error.
- Add a getFieldValue() method to the ValidatorUtil class. This method returns the field value of the specified field and takes two parameters which are described in the following:
- The first parameter is the object which contains the requested field.
- The second parameter is the name of the requested field.
- Implement the getFieldValue() method by following these steps:
- Get a reference to the Field object which reflects the requested field.
- Ensure that we can access the value of the field even if the field is private.
- Return the field value.
The source code of the ValidatorUtil class looks as follows:
import javax.validation.ConstraintValidatorContext; import java.lang.reflect.Field; public class ValidatorUtil { public static void addValidationError(String field, ConstraintValidatorContext context) { context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate()) .addNode(field) .addConstraintViolation(); } public static Object getFieldValue(Object object, String fieldName) throws NoSuchFieldException, IllegalAccessException { Field f = object.getClass().getDeclaredField(fieldName); f.setAccessible(true); return f.get(object); } }
We are now ready to implement our validator classes. Let’s see how that is done.
Creating the Validator Classes
First, we have to create the validator class which can validate classes annotated with the @PasswordsNotEmpty annotation. We can do this by following these steps:
- Create a PasswordsNotEmptyValidator class and implement the ConstraintValidator interface. The ConstraintValidator interface defines two type parameters which are described in the following:
- The first type parameter is the annotation type. Set the value of this type parameter to PasswordsNotEmpty.
- The second type parameter is the type of element which can be validated by the validator. Set the value of this type parameter to Object (We could set this to RegistrationForm but using the type Object ensures that our validator is not restricted to this example application).
- Add a private validationTriggerFieldName field to the created class and set its type to String.
- Add a private passwordFieldName field to the created class and set its type to String.
- Add a private passwordVerificationFieldName field to the created class and set its type to String.
- Add the initialize(PasswordsNotEmpty constraintAnnotation) method of the ConstraintValidator interface to the validator class and implement it by following these steps:
- Set the value of the validationTriggerFieldName field.
- Set the value of the passwordFieldName field.
- Set the value of the passwordVerificationFieldName field.
- Add a private isNullOrEmpty(String field) method to the created class. This method returns true if the String given as a method parameter is null or empty. Otherwise this method returns false.
- Add a private passwordsAreValid(Object value, ConstraintValidatorContext context) method to the created class. This method returns a true if the password fields are valid and false otherwise. This method takes two method parameters which are described in the following:
- The first method parameter is the validated object.
- The second method parameter is a ConstraintValidatorContext object.
- Implement the passwordsAreValid() method by following these steps:
- Obtain the value of the password field by calling the getFieldValue() method of the ValidatorUtil class. Pass the validated object and the name of the password field as method parameters.
- If the value of the password field is empty or null, add a validation error by calling the addValidationError() method of the ValidatorUtil class. Pass the name of the password field and the ConstraintValidatorContext object as method parameters.
- Obtain the value of the passwordVerification field by calling the getFieldValue() method of the ValidatorUtil class. Pass the validated object and the name of the password verification field as method parameters.
- If the value of the password verification field is empty or null, add a validation error by calling the addValidationError() method of the ValidatorUtil class. Pass the name of the password verification field and the ConstraintValidatorContext object as method parameters.
- If validation errors were found, return false. Otherwise return true.
- Add the isValid(Object value, ConstraintValidatorContext context) method of the ConstraintValidator interface to the validator class and implement it by following these steps:
- Disable the default error message by calling the disableDefaultConstraintViolation() method of the ConstraintValidatorContext interface.
- Add a try-catch structure to the method and catch all checked exceptions. If a checked exception is thrown, catch it and wrap it inside a RuntimeException. This is required because the isValid() method of the ConstraintValidator interface cannot throw checked exceptions Implement the try block by following these steps:
- Get the value of the validation trigger field by calling the getFieldValue() method of the ValidatorUtil class. Pass the validated object and the name of the validation trigger field as method parameters.
- If the value of the validation trigger field is null, call the passwordFieldsAreValid() method and pass the validated object and the ConstraintValidatorContext object as method parameters. Return the boolean value returned by this method.
- If the value of the validation trigger field is not null, return true.
The source code of the PasswordsNotEmptyValidator class looks as follows:
import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; public class PasswordsNotEmptyValidator implements ConstraintValidator<PasswordsNotEmpty, Object> { private String validationTriggerFieldName; private String passwordFieldName; private String passwordVerificationFieldName; @Override public void initialize(PasswordsNotEmpty constraintAnnotation) { validationTriggerFieldName = constraintAnnotation.triggerFieldName(); passwordFieldName = constraintAnnotation.passwordFieldName(); passwordVerificationFieldName = constraintAnnotation.passwordVerificationFieldName(); } @Override public boolean isValid(Object value, ConstraintValidatorContext context) { context.disableDefaultConstraintViolation(); try { Object validationTrigger = ValidatorUtil.getFieldValue(value, validationTriggerFieldName); if (validationTrigger == null) { return passwordFieldsAreValid(value, context); } } catch (Exception ex) { throw new RuntimeException("Exception occurred during validation", ex); } return true; } private boolean passwordFieldsAreValid(Object value, ConstraintValidatorContext context) throws NoSuchFieldException, IllegalAccessException { boolean passwordWordFieldsAreValid = true; String password = (String) ValidatorUtil.getFieldValue(value, passwordFieldName); if (isNullOrEmpty(password)) { ValidatorUtil.addValidationError(passwordFieldName, context); passwordWordFieldsAreValid = false; } String passwordVerification = (String) ValidatorUtil.getFieldValue(value, passwordVerificationFieldName); if (isNullOrEmpty(passwordVerification)) { ValidatorUtil.addValidationError(passwordVerificationFieldName, context); passwordWordFieldsAreValid = false; } return passwordWordFieldsAreValid; } private boolean isNullOrEmpty(String field) { return field == null || field.trim().isEmpty(); } }
Second, we have to create the validator class which validates classes annotated with the @PasswordsNotEqual annotation. We can do this by following these steps:
- Create a PasswordsNotEqualValidator class and implement the ConstraintValidator interface. The ConstraintValidator interface defines two type parameters which are described in the following:
- The first type parameter is the annotation type. Set the value of this type parameter to PasswordsNotEqual.
- The second type parameter is the type of element which can be validated by the validator. Set the value of this type parameter to Object (We could set this to RegistrationForm but using the type Object ensures that our validator is not restricted to this example application).
- Add a private passwordFieldName field to the created class and set its type to String.
- Add a private passwordVerificationFieldName field to the created class and set its type to String.
- Add the initialize(PasswordsNotEqual constraintAnnotation) method of the ConstraintValidator interface to the validator class and implement it by following these steps:
- Set the value of the passwordFieldName field.
- Set the value of the passwordVerificationFieldName field.
- Add a private passwordsAreNotEqual(String password, String passwordVerification) method to the created class. If the password and password verification given as method parameters aren’t equal, this method returns true. Otherwise this method returns false.
- Add the isValid(Object value, ConstraintValidatorContext context) method of the ConstraintValidator interface to the validator class and implement it by following these steps:
- Disable the default error message by calling the disableDefaultConstraintViolation() method of the ConstraintValidatorContext interface.
- Add a try-catch structure to the method and catch all checked exceptions. If a checked exception is thrown, catch it and wrap it inside a RuntimeException. This is required because the isValid() method of the ConstraintValidator interface cannot throw checked exceptions Implement the try block by following these steps:
- Get the value of the password field by calling the getFieldValue() method of the ValidatorUtil class. Pass the validated object and the name of the password field as method parameters.
- Get the value of the password verification field by calling the getFieldValue() method of the ValidatorUtil class. Pass the validated object and the name of the password verification field as method parameters.
- Check if passwords aren’t equal by calling the passwordsAreNotEqual() method. Pass the password and password verification as method parameters.
- If the password and password verification aren’t equal, add validation error to both password and password verification fields by calling the addValidationError() method of the ValidatorUtil class. Return false.
- If password and password verification were are, return true.
The source code of the PasswordsNotEqualValidator looks as follows:
import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; public class PasswordsNotEqualValidator implements ConstraintValidator<PasswordsNotEqual, Object> { private String passwordFieldName; private String passwordVerificationFieldName; @Override public void initialize(PasswordsNotEqual constraintAnnotation) { this.passwordFieldName = constraintAnnotation.passwordFieldName(); this.passwordVerificationFieldName = constraintAnnotation.passwordVerificationFieldName(); } @Override public boolean isValid(Object value, ConstraintValidatorContext context) { context.disableDefaultConstraintViolation(); try { String password = (String) ValidatorUtil.getFieldValue(value, passwordFieldName); String passwordVerification = (String) ValidatorUtil.getFieldValue(value, passwordVerificationFieldName); if (passwordsAreNotEqual(password, passwordVerification)) { ValidatorUtil.addValidationError(passwordFieldName, context); ValidatorUtil.addValidationError(passwordVerificationFieldName, context); return false; } } catch (Exception ex) { throw new RuntimeException("Exception occurred during validation", ex); } return true; } private boolean passwordsAreNotEqual(String password, String passwordVerification) { return !(password == null ? passwordVerification == null : password.equals(passwordVerification)); } }
That is it. We have now implemented our custom validation constraints. Let’s find out how we can render the registration page.
Rendering the Registration Page
The requirements of our registration page are following:
- The url of the registration page must be ‘/user/register’.
- If the user is creating a “normal” user account, our application must render an empty registration form.
- If the user is using social sign in, the information provided by the SaaS API provider must be used to pre-populate the form fields of the registration form.
Let’s start by finding out how we can redirect user to the registration page.
Redirecting User to the Registration Page
Before we can start implementing the controller method which renders the registration page, we have to implement a controller which redirects user to the correct url. The requirements of this controller are following:
- It must process GET requests send to url ‘/signup’.
- It must redirect requests to url ‘/user/register’.
Note: If you are configuring the application context of your application by using XML configuration files, you can skip this step. This step is required only if you configure the application context of your application by using Java configuration. The reason for this is that at the moment you can configure the sign up url only if you are using XML configuration (Search for a property called signUpUrl).
We can implement this controller by following these steps:
- Create a SignUpController class and annotate the class with the @Controller annotation.
- Add a public redirectRequestToRegistrationPage() method to the created class. The return type of this method is String.
- Implement the redirectRequestToRegistrationPage() method by following these steps:
- Annotate the method with the @RequestMapping annotation and ensure that the method processes GET requests send to url ‘/signup’.
- Return a String ‘redirect:/user/register’. This will redirect the request to url ‘/user/register’.
The source code of the SignUpController class looks as follows:
import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; @Controller public class SignUpController { @RequestMapping(value = "/signup", method = RequestMethod.GET) public String redirectRequestToRegistrationPage() { return "redirect:/user/register"; } }
Let’s move on and find out how we can implement the controller method which renders the registration page.
Implementing the Controller Method
The controller method which renders the registration page has one important responsibility: It creates the form object and pre-populates its fields. If the user is creating a “normal” user account, this controller method creates an empty form object. On the other hand, if the user is creating a user account by using social sign in, this controller method sets the field values of the form object by using the information provided by the used SaaS API provider. We can implement the controller method which renders the registration page by following these steps:
- Create the controller class and annotate it with the @Controller annotation.
- Annotate the class with the @SessionAttributes annotation and set its value to ‘user’. We use this annotation to ensure that a model attribute called ‘user’ (our form object) is stored to the session.
- Add a private createRegistrationDTO() method to the class. This method takes a Connection object as a method parameter and returns a RegistrationForm object. We can implement this method by following these steps:
- Create a new RegistrationForm object.
- If the Connection object given as a method parameter is not null, the user is creating a new user account by using social sign in. If this is the case, we have to
- Get a UserProfile object by calling the fetchUserProfile() method of the Connection class. This object contains the user information returned by the SaaS API provider.
- Set the email, first name, and the last name to the form object. We can the get this information by calling the methods of the UserProfile class.
- Get a ConnectionKey object by calling the getKey() method of the Connection class. This object contains id of the used social sign in provider and a provider specific user id.
- Set the sign in provider to the form object by following these steps:
- Get the sign in provider by calling the getProviderId() method of the ConnectionKey class.
- Transform the String returned by the getProviderId() method to uppercase.
- Get the correct value of the SocialMediaService enum by calling its nameOf() method. Pass the sign in provider (in uppercase) as a method parameter (This means that the values of the SocialMediaService enum depends from the sign in provider ids).
- Set the returned value to the form object.
- Return the form object.
- The controller method which renders the registration page is called showRegistrationForm(). Add this method to the controller class and implement it by following these steps:
- Annotate the method with the @RequestMapping annotation and ensure that controller method processes GET requests send to url ‘/user/register’.
- Add a WebRequest object as a method parameter. We use the WebRequest as a method parameter because it gives us an easy access to request metadata.
- Add a Model object as a method parameter.
- Get a Connection object by calling the static getConnection() method of the ProviderSignInUtils class. Pass the WebRequest object as a method parameter. This method returns null if the WebRequest object doesn’t contain SaaS API provider metadata (this means that user is creating a normal user account). If the metadata is found, this method creates a Connection object by using that information and returns the created object.
- Get the form object by calling the private createRegistrationDTO() method. Pass the Connection object as a method parameter.
- Set the form object to model as a model attribute called ‘user’.
- Return the name of the registration form view (‘user/registrationForm’).
The relevant part of the UserController class looks as follows:
import org.springframework.social.connect.Connection; import org.springframework.social.connect.ConnectionKey; import org.springframework.social.connect.UserProfile; import org.springframework.social.connect.web.ProviderSignInUtils; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.SessionAttributes; import org.springframework.web.context.request.WebRequest; @Controller @SessionAttributes("user") public class RegistrationController { @RequestMapping(value = "/user/register", method = RequestMethod.GET) public String showRegistrationForm(WebRequest request, Model model) { Connection<?> connection = ProviderSignInUtils.getConnection(request); RegistrationForm registration = createRegistrationDTO(connection); model.addAttribute("user", registration); return "user/registrationForm"; } private RegistrationForm createRegistrationDTO(Connection<?> connection) { RegistrationForm dto = new RegistrationForm(); if (connection != null) { UserProfile socialMediaProfile = connection.fetchUserProfile(); dto.setEmail(socialMediaProfile.getEmail()); dto.setFirstName(socialMediaProfile.getFirstName()); dto.setLastName(socialMediaProfile.getLastName()); ConnectionKey providerKey = connection.getKey(); dto.setSignInProvider(SocialMediaService.valueOf(providerKey.getProviderId().toUpperCase())); } return dto; } }
The next thing that we have to do is to create the JSP page. Let’s move on and find out how this is done.
Creating the JSP Page
We can create the JSP page which contains the registration form by following these steps:
- Ensure that the registration form is shown only to anonymous users. We can do this by following these steps:
- Wrap the login form and social sign in buttons inside the authorize tag of the Spring Security tag library.
- Set the value of the access attribute to isAnonymous().
- Implement the registration form by following these steps:
- Ensure that when the registration form is submitted, a POST request is send to url ‘/user/register’.
- Add a CSRF token to the request. This is required because we enabled the CSRF protection of Spring Security in the first part of this tutorial.
- If the sign in provider is found from the form object, add it to the form as a hidden field.
- Add a firstName field to the form and ensure that the validation errors concerning the firstName field are shown.
- Add a lastName field to the form and ensure that the validation errors concerning the lastName field are shown.
- Add an email field to the form and ensure that the validation errors concerning the email field are shown.
- If the user is creating a normal user account (the value of the form object’s signInProvider field is null), follow these steps:
- Add a password field to the form and ensure that the validation errors concerning the password field are shown.
- Add a passwordVerification field to the form and ensure that validation errors concerning the passwordVerification field are shown.
- Add a submit button to the form
- Ensure that a help message is shown if an authenticated user accesses the registration page. We can do this by following these steps:
- Wrap the error message area inside the authorize tag of the Spring Security tag library.
- Set the value of the access attribute to isAuthenticated().
- Get the localized error message by using the message tag of the Spring tag library.
Note: The Spring 3.2 reference manual has more information about the form tags of the Spring JSP tag library.
The source code of the registrationForm.jsp page looks as follows:
<!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" %> <%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %> <html> <head> <title></title> <script type="text/javascript" src="${pageContext.request.contextPath}/static/js/app/user.form.js"></script> </head> <body> <div class="page-header"> <h1><spring:message code="label.user.registration.page.title"/></h1> </div> <!-- If the user is anonymous (not logged in), show the registration form. --> <sec:authorize access="isAnonymous()"> <div class="panel panel-default"> <div class="panel-body"> <!-- Ensure that when the form is submitted, a POST request is send to url '/user/register'. --> <form:form action="/user/register" commandName="user" method="POST" enctype="utf8" role="form"> <!-- Add CSRF token to the request. --> <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/> <!-- If the user is using social sign in, add the signInProvider as a hidden field. --> <c:if test="${user.signInProvider != null}"> <form:hidden path="signInProvider"/> </c:if> <div class="row"> <div id="form-group-firstName" class="form-group col-lg-4"> <label class="control-label" for="user-firstName"><spring:message code="label.user.firstName"/>:</label> <!-- Add the firstName field to the form and ensure that validation errors are shown. --> <form:input id="user-firstName" path="firstName" cssClass="form-control"/> <form:errors id="error-firstName" path="firstName" cssClass="help-block"/> </div> </div> <div class="row"> <div id="form-group-lastName" class="form-group col-lg-4"> <label class="control-label" for="user-lastName"><spring:message code="label.user.lastName"/>:</label> <!-- Add the lastName field to the form and ensure that validation errors are shown. --> <form:input id="user-lastName" path="lastName" cssClass="form-control"/> <form:errors id="error-lastName" path="lastName" cssClass="help-block"/> </div> </div> <div class="row"> <div id="form-group-email" class="form-group col-lg-4"> <label class="control-label" for="user-email"><spring:message code="label.user.email"/>:</label> <!-- Add the email field to the form and ensure that validation errors are shown. --> <form:input id="user-email" path="email" cssClass="form-control"/> <form:errors id="error-email" path="email" cssClass="help-block"/> </div> </div> <!-- If the user is creating a normal user account, add password fields to the form. --> <c:if test="${user.signInProvider == null}"> <div class="row"> <div id="form-group-password" class="form-group col-lg-4"> <label class="control-label" for="user-password"><spring:message code="label.user.password"/>:</label> <!-- Add the password field to the form and ensure that validation errors are shown. --> <form:password id="user-password" path="password" cssClass="form-control"/> <form:errors id="error-password" path="password" cssClass="help-block"/> </div> </div> <div class="row"> <div id="form-group-passwordVerification" class="form-group col-lg-4"> <label class="control-label" for="user-passwordVerification"><spring:message code="label.user.passwordVerification"/>:</label> <!-- Add the passwordVerification field to the form and ensure that validation errors are shown. --> <form:password id="user-passwordVerification" path="passwordVerification" cssClass="form-control"/> <form:errors id="error-passwordVerification" path="passwordVerification" cssClass="help-block"/> </div> </div> </c:if> <!-- Add the submit button to the form. --> <button type="submit" class="btn btn-default"><spring:message code="label.user.registration.submit.button"/></button> </form:form> </div> </div> </sec:authorize> <!-- If the user is authenticated, show a help message instead of registration form. --> <sec:authorize access="isAuthenticated()"> <p><spring:message code="text.registration.page.authenticated.user.help"/></p> </sec:authorize> </body> </html>
Let’s move on and find out how we can process the submission of the registration form.
Processing the Form Submissions of the Registration Form
Our next step is to process the form submissions of the registration form. We can do this by following these steps:
- Validate the information entered to the registration form. If the information is not valid, we render the registration form and show validation error messages to the user.
- Ensure that the email address given by the user is unique. If the email address is not unique, we render the registration form and show an error message to the user.
- Create a new user account and log in the user.
- Redirect the user to the front page.
This process is illustrated in the following diagram:
Let’s start by implementing the controller method which processes the form submissions of the registration form.
Implementing the Controller Method
The controller method which processes the form submissions of the registration form has the following responsibilities:
- It ensures that the information entered to the registration form is valid.
- It informs the user if the email address entered to the registration form is found from the database.
- It passes the form object forward to the service layer.
- It persists the connection to the UserConnection table if the user is creating a new user account by using social sign in.
- It logs the user in after a new user account has been created.
We can implement this controller method by making the following changes to the RegistrationController class:
- Add a private UserService field to the controller class.
- Add a constructor which takes a UserService object as a constructor argument to the RegistrationController class and implement it by following these steps:
- Annotate the constructor with the @Autowired annotation. This ensures that the dependencies of this bean are injected by using constructor injection.
- Set the value of service field.
- Add a private addFieldError() method to the controller class. This method is used to add binding errors to the binding result. The method parameters of this method are described in the following:
- The objectName parameter is the name of the form object.
- The fieldName parameter is the name of the form field which contains invalid value.
- The fieldValue parameter contains the value of the form field.
- The errorCode parameter is the error code of the field error.
- The result parameter is a BindingResult object.
- Implement the addFieldError() method by following these steps:
- Create a new FieldError object by using the method parameters.
- Add the created FieldError object to the binding result by calling the AddError() method of the BindingResult class.
- Add a private createUserAccount() method to the controller class. This method returns the created User object, and takes a RegistrationForm and BindingResult objects as method parameters. If the email address is found from the database, this method returns null. Implement this method by following these steps:
- Add a try-catch structure to the method and catch DuplicateEmailException objects.
- Implement the try block by calling the registerNewUserAccount() method of the UserService interface. Pass the RegistrationForm object as a method parameter. Return the information of the created user account.
- Implement the catch block by calling the private addFieldError() method. Pass the required information as method parameters. This ensures that the user receives an error message which informs him that the email address entered to the registration form is found from the database. Return null.
- Add a public registerUserAccount() method to the controller class and implement it by following these steps:
- Annotate the method with the @RequestMapping annotation and ensure that the method processes POST request send to url ‘/user/register’.
- Add a RegistrationForm object as a method parameter and annotate it with the following annotations:
- Annotate the method parameter with the @Valid annotation. This ensures that the information of this object is validated before the controller method is called.
- Annotate the method parameter with the @ModelAttribute annotation and set its value to ‘user’ (this is the name of the form object).
- Add a BindingResult object as a method parameter.
- Add a WebRequest object as a method parameter. This object is required because we need to access the metadata of the request after the a new user account has been created.
- If the binding result has errors, return the name of the form view.
- Call the private createUserAccount() method and pass the RegistrationForm and BindingResult objects as method parameters.
- If the User object returned by the createUserAccount() method is null, it means that the email address was found from the database. Return the name of the form view.
- Log the created user in by calling the static loginInUser() method of the SecurityUtil class. Pass the created User object as a method parameter.
- Call the static handlePostSignUp() method of the ProviderSignInUtils class. Pass the email address of the created user and the WebRequest object as method parameters. If the user created user account by using social sign in, this method persists the connection to the UserConnection table. If the user created a normal user account, this method doesn’t do anything.
- Redirect the user to the front page of our application by returning a String ‘redirect:/’. This will redirect the request to url ‘/’.
The relevant part of the UserController class looks as follows:
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.social.connect.web.ProviderSignInUtils; import org.springframework.stereotype.Controller; import org.springframework.validation.BindingResult; import org.springframework.validation.FieldError; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.SessionAttributes; import org.springframework.web.context.request.WebRequest; import javax.validation.Valid; @Controller @SessionAttributes("user") public class RegistrationController { private UserService service; @Autowired public RegistrationController(UserService service) { this.service = service; } @RequestMapping(value ="/user/register", method = RequestMethod.POST) public String registerUserAccount(@Valid @ModelAttribute("user") RegistrationForm userAccountData, BindingResult result, WebRequest request) throws DuplicateEmailException { if (result.hasErrors()) { return "user/registrationForm"; } User registered = createUserAccount(userAccountData, result); if (registered == null) { return "user/registrationForm"; } SecurityUtil.logInUser(registered); ProviderSignInUtils.handlePostSignUp(registered.getEmail(), request); return "redirect:/"; } private User createUserAccount(RegistrationForm userAccountData, BindingResult result) { User registered = null; try { registered = service.registerNewUserAccount(userAccountData); } catch (DuplicateEmailException ex) { addFieldError( "user", "email", userAccountData.getEmail(), "NotExist.user.email", result); } return registered; } private void addFieldError(String objectName, String fieldName, String fieldValue, String errorCode, BindingResult result) { FieldError error = new FieldError( objectName, fieldName, fieldValue, false, new String[]{errorCode}, new Object[]{}, errorCode ); result.addError(error); } }
The SecurityUtil class has one static method called loginInUser(). This method takes the information of the created user as a method parameter, and logs the user in programmatically. We can implement this method by following these steps:
- Create a new ExampleUserDetails object by using the information of the created user.
- Create a new UsernamePasswordAuthenticationToken object and pass the following arguments to its constructor:
- The first argument is the principal (aka logged in user). Pass the created ExampleUserDetails object as the first constructor argument.
- The second argument contains the credentials of the user. Pass null as the second constructor argument.
- The third argument contains the the authorities of the user. We can get the authorities by calling the getAuthorities() method of the ExampleUserDetails class.
- Set created Authentication object into security context by following these steps:
- Get the SecurityContext object by calling the static getContext() method of the SecurityContextHolder class.
- Call the static setAuthentication() method of the SecurityContext class and pass the created UsernamePasswordAuthenticationToken object as a method parameter.
The source code of the SecurityUtil class looks as follows:
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; public class SecurityUtil { public static void logInUser(User user) { ExampleUserDetails userDetails = ExampleUserDetails.getBuilder() .firstName(user.getFirstName()) .id(user.getId()) .lastName(user.getLastName()) .password(user.getPassword()) .role(user.getRole()) .socialSignInProvider(user.getSignInProvider()) .username(user.getEmail()) .build(); Authentication authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authentication); } }
Note: It is not a good idea to log in a user who has created a normal user account. Typically you want to send a confirmation email which is used to verify his email address. However, the example application works this way because it simplifies the registration process. Let’s move on and find out how we can create the domain model of our example application.
Creating the Domain Model
The domain model of our application consists of two classes and two enums which are described in the following:
- The BaseEntity class is a superclass of all entity classes of our application.
- The User class is the only entity class of our application. It contains the information of a single user.
- The Role enum specifies the user roles of our application.
- The SocialMediaService enum specifies the SaaS API providers which are supported by our example application.
Note: Our example application doesn’t really need a separate base class for entities because it has only one entity. However, I decided to add it anyway because this is often a good idea in real life applications. Let’s move on and find out how we can create the domain model. First, we have to create a BaseEntity class. It contains the fields which are shared by all entity classes and two callback methods which are used to store values to some of those fields. We can implement this class by following these steps:
- Create an abstract BaseEntity class which has one type parameter called ID. This parameter is the type of the entity’s private key.
- Annotate the class with the @MapperSuperclass annotation. This means that the mapping information of the BaseEntity class is applied to its subclasses.
- Add a DateTime field called creationTime to the class and configure it by following these steps:
- Annotate the field with the @Column annotation and configure the name of the database column. The value of the nullable attribute to false.
- Annotate the field with the @Type annotation and set the value of the type attribute to ‘org.jadira.usertype.dateandtime.joda.PersistentDateTime’ (Javadoc here). This marks the field as a custom type and configures the type class which makes it possible to persist DateTime objects with Hibernate.
- Add a DateTime field called modificationTime to the class and configure it by using these steps:
- Annotate the field with the @Column annotation and set the name of the database column. Ensure that this column is not nullable.
- Annotate the field with the @Type annotation and set the value of the type attribute to ‘org.jadira.usertype.dateandtime.joda.PersistentDateTime’ (check step 3 for more details about this).
- Add a long field called version to the class and annotate the field with the @Version annotation. This enables optimistic locking and states the value of the version field serves as optimistic lock value.
- Add an abstract getId() method to the class. This method returns the id of the actual entity.
- Add a public prePersist() method to the class and annotate the method with the @PrePersist annotation. This method is called before the entity manager persists the object, and it sets the current time as the value of the creationTime and the modificationTime fields.
- Add a public preUpdate() method to the class and annotate the method with the @PreUpdate annotation. This method is called before the database UPDATE operation is performed. The implementation of this method sets the current time as the value of the modificationTime field.
The source code of the BaseEntity class looks as follows:
import org.hibernate.annotations.Type; import org.joda.time.DateTime; import javax.persistence.*; @MappedSuperclass public abstract class BaseEntity<ID> { @Column(name = "creation_time", nullable = false) @Type(type="org.jadira.usertype.dateandtime.joda.PersistentDateTime") private DateTime creationTime; @Column(name = "modification_time", nullable = false) @Type(type="org.jadira.usertype.dateandtime.joda.PersistentDateTime") private DateTime modificationTime; @Version private long version; public abstract ID getId(); //Other getters are omitted for the sake of clarity. @PrePersist public void prePersist() { DateTime now = DateTime.now(); this.creationTime = now; this.modificationTime = now; } @PreUpdate public void preUpdate() { this.modificationTime = DateTime.now(); } }
Second, we have to create the User class. We can create this class following these steps:
- Create a User class which extends the BaseEntity class and give the type of its private key (Long) as a type parameter.
- Annotate the created class with the @Entity annotation.
- Annotate the created class with the @Table annotation and ensure that the user information is stored to a database table called ‘users’.
- Add a private id field to the class and set its type to Long. Configure the field by following these steps:
- Annotate the field with the @Id annotation. This annotation is used to specify the primary key of the entity.
- Annotate the field with the @GeneratedValue annotation and set the value of the strategy attribute to GenerationType.AUTO. This means that the persistence provider will pick the appropriate key generation strategy for the used database.
- Add a private email field to the class and set its type to String. Annotate the field with the @Column annotation and configure the field by following these rules:
- The email address is stored to the ‘email’ column of the ‘users’ table.
- The maximum length of the email address is 100 characters.
- The email address cannot be null.
- The email address must be unique.
- Add a private firstName field to the class and set its type to String. Annotate the field with the @Column annotation and configure the field by following these rules:
- The first name is stored to the ‘first_name’ column of the ‘users’ table.
- The maximum length of the first name is 100 characters.
- The first name cannot be null.
- Add a private lastName field to the class and set its to type to String. Annotate the field with the @Column annotation and and configure the field by following these rules:
- The last name is stored to the ‘last_name’ column of the ‘users’ table.
- The maximum length of the last name is 100 characters.
- The last name cannot be null.
- Add a private password field to the class and set its type to String. Annotate the field with the @Column annotation and configure the field by following these rules:
- The password is stored to the ‘password’ column of the ‘users’ table.
- The maximum length of the password is 255 characters.
- Add a private role field to the class and set its type to Role. Annotate the field with the @Enumerated annotation and set its value to EnumType.STRING. This means the value of this field is persisted as enumerated type and that a String value is stored to the database. Annotate the field with the @Column annotation and configure the field by following these rules:
- The role is stored to the ‘role’ column of the ‘users’ table.
- The maximum length of the role is 20 characters.
- The role cannot be null.
- Add a private signInProvider field to the class and set its type to SocialMediaService. Annotate the field with the @Enumerated annotation and set its value to EnumType.STRING (check step 9 for more details about this). Annotate the field with the @Column annotation and configure the field by following these rules:
- The sign in provider is stored to the ‘sign_in_provider’ field of the ‘users’ table.
- The maximum length of the sign in provider is 20 characters.
- Add a public static inner class called Builder to the User class. Implement this class by following these steps:
- Add a User field to the class. This field holds a reference to the constructed User object.
- Add a constructor to the class. This constructor creates a new User object and sets the role of the created user to Role.ROLE_USER.
- Add methods used to set the field values of created User object to the builder class. Each method sets the value given as a method parameter to the correct field and returns a reference to User.Builder object.
- Add a build() method to the builder class. This method returns the created User object.
- Add a public static getBuilder() method to the User class. This method returns a new User.Builder object.
Note: You can get more information about the builder pattern by reading a blog post called The builder pattern in practice.
The source code of the User class looks as follows:
import javax.persistence.*; @Entity @Table(name = "users") public class User extends BaseEntity<Long> { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; @Column(name = "email", length = 100, nullable = false, unique = true) private String email; @Column(name = "first_name", length = 100,nullable = false) private String firstName; @Column(name = "last_name", length = 100, nullable = false) private String lastName; @Column(name = "password", length = 255) private String password; @Enumerated(EnumType.STRING) @Column(name = "role", length = 20, nullable = false) private Role role; @Enumerated(EnumType.STRING) @Column(name = "sign_in_provider", length = 20) private SocialMediaService signInProvider; //The constructor and getters are omitted for the sake of clarity public static Builder getBuilder() { return new Builder(); } public static class Builder { private User user; public Builder() { user = new User(); user.role = Role.ROLE_USER; } public Builder email(String email) { user.email = email; return this; } public Builder firstName(String firstName) { user.firstName = firstName; return this; } public Builder lastName(String lastName) { user.lastName = lastName; return this; } public Builder password(String password) { user.password = password; return this; } public Builder signInProvider(SocialMediaService signInProvider) { user.signInProvider = signInProvider; return this; } public User build() { return user; } } }
The Role is an enum which specifies the user roles of our application. Its source code looks as follows:
public enum Role { ROLE_USER }
The SocialMediaService is an enum which identifies the SaaS API provider which was used to authenticate the user. Its source code looks as follows:
public enum SocialMediaService { FACEBOOK, TWITTER }
Next we will find out how we can implement the service class which creates new user accounts and persists them to the database.
Creating the Service Class
First, we have to create an interface which declares the method used to add new user accounts to the database. This method is described in the following: The registerNewUserAccount() method takes a RegistrationForm object as method parameter and returns a User object. If the email address stored to the email field of the RegistrationForm object is found from the database, this method throws a DuplicateEmailException. The source code of the UserService interface looks as follows:
public interface UserService { public User registerNewUserAccount(RegistrationForm userAccountData) throws DuplicateEmailException; }
Second, we have to implement the UserService interface. We can do it by following these steps:
- Create a class which implements the UserService interface and annotate this class with the @Service annotation.
- Add a PasswordEncoder field to the created class.
- Add a UserRepository field to to created class.
- Add a constructor which takes PasswordEncoder and UserRepository objects as constructor arguments to the service class. Implement the constructor by following these steps:
- Annotate the constructor with the @Autowired annotation. This ensures that the dependencies of this bean are injected by using constructor injection.
- Set the values of passwordEncoder and repository fields.
- Add a private emailExist() method to the service class. This method takes a email address as a method argument and returns a boolean. Implement this method by following these steps:
- Get the user whose email address is equal to the email address given as a method parameter by calling the findByEmail() method of the UserRepository interface. Pass the email address as a method parameter.
- If a user is found, return true.
- If a user is not found, return false.
- Add a private encodePassword() method to service class. This method takes a RegistrationForm object as a method parameter and returns the encoded password. Implement this method by following these steps:
- Find out if the user is creating a normal user account. We can get this information by calling the isNormalRegistration() method of the RegistrationForm class. If this method returns true, obtain the encoded password by calling the encode() method of the PasswordEncoder class. Pass the cleartext password as a method parameter. Return the encoded password.
- If the user is creating a user account by using social sign in, return null.
- Add a registerNewUserAccount() method to the service class and implement it by following these steps:
- Annotate the method with the @Transactional annotation. This means that the method is executed “inside” a read-write transaction.
- Find out if the email address is found from the database. We can do this by calling the private emailExist() method. Pass the RegistrationForm object as a method parameter. If this method returns true, throw a new DuplicateEmailException.
- Obtain the encoded password by calling the private encodePassword() method. Pass the RegistrationForm object as a method parameter.
- Get the builder object by calling the getBuilder() method of the User class and set the following information to the created User object:
- Email address
- First name
- Last name
- Password
- Find out if the user is creating a new user account by using social sign in. We can do this by calling the <em<issocialsignin() method of the egistrationForm class. If this method returns true, set the used social sign in provider by calling the signInProvider() method of the User.Builder class. Pass the used sign in provider as a method parameter. </em<issocialsignin()
- Create the User object.
- Persist the User object to the database by calling the save() method of the UserRepository interface. Pass the created User object as a method parameter.
- Return the persisted object.
The source code of the RepositoryUserService class looks as follows:
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service public class RepositoryUserService implements UserService { private PasswordEncoder passwordEncoder; private UserRepository repository; @Autowired public RepositoryUserService(PasswordEncoder passwordEncoder, UserRepository repository) { this.passwordEncoder = passwordEncoder; this.repository = repository; } @Transactional @Override public User registerNewUserAccount(RegistrationForm userAccountData) throws DuplicateEmailException { if (emailExist(userAccountData.getEmail())) { throw new DuplicateEmailException("The email address: " + userAccountData.getEmail() + " is already in use."); } String encodedPassword = encodePassword(userAccountData); User.Builder user = User.getBuilder() .email(userAccountData.getEmail()) .firstName(userAccountData.getFirstName()) .lastName(userAccountData.getLastName()) .password(encodedPassword); if (userAccountData.isSocialSignIn()) { user.signInProvider(userAccountData.getSignInProvider()); } User registered = user.build(); return repository.save(registered); } private boolean emailExist(String email) { User user = repository.findByEmail(email); if (user != null) { return true; } return false; } private String encodePassword(RegistrationForm dto) { String encodedPassword = null; if (dto.isNormalRegistration()) { encodedPassword = passwordEncoder.encode(dto.getPassword()); } return encodedPassword; } }
We still have to create the Spring Data JPA repository for our example application. Let’s find out how we can do this.
Creating the Spring Data JPA Repository
Our last step is to create a Spring Data JPA repository which is used to
- Persist new User objects to the database.
- Find a User object from the database by using email address as a search criteria.
We can create a Spring Data JPA repository which fulfils these requirements by following these steps:
- Create the repository interface and extend the JpaRepository interface. Give the type of the entity (User) and type of its private key (Long) as type parameters. This gives us access to the methods declared by the JpaRepository interface. One of those methods is the save() method which is used to persist User objects to the database.
- Add a findByEmail() method to the created repository interface. This method takes an email address as a method parameter and returns a User object whose email is equal to the email address given as a method parameter. If no user is found, this method returns null.
Note: If you want to get more information about Spring Data JPA, you can take a look at my Spring Data JPA tutorial. The source code of the UserRepository interface looks as follows:
import org.springframework.data.jpa.repository.JpaRepository; public interface UserRepository extends JpaRepository<User, Long> { public User findByEmail(String email); }
That was it! Let’s move on and spend a moment to summarize what we have achieved during this blog post.
The Summary
We have now implemented the requirements of our example application. This means that
- We have created a registration function which supports both “normal” user accounts and user accounts created by using social sign.
- The users of our application can log in by using username and password.
- The users of our application can log in by using social sign in.
Let’s refresh our memories and take a look at the registration process. This process is illustrated in the following figure:
This blog post has taught us the following things:
- We learned how we can start the social sign in flow.
- We learned how we can pre-populate the field of our registration form by using the information provided by the SaaS API provider.
- We learned how we can create custom validation constraints which ensures that information entered to the registration form is valid.
The next part of this tutorial describes how we can write unit tests for the web layer of our application.
P.S. The example application of this blog post is available at Github.
I wanted to first say THANK YOU!
Can google or linkedin be used as a Provider in your example?