将社交登录添加到Spring MVC Web应用程序:注册和登录

本教程的第一部分描述了如何配置Spring Social 1.1.0和Spring Security 3.2.0,但它留下了两个非常重要的问题尚未解答。

这些问题是:

  • 用户如何创建新用户帐户?
  • 用户如何登录?

现在该弄脏我们的手并回答这些问题了。 我们的示例应用程序的要求是:

  • 必须有可能创建一个“传统”用户帐户。 这意味着使用用户名和密码对用户进行身份验证。
  • 必须可以通过使用诸如Facebook或Twitter的SaaS API提供程序来创建用户帐户。 在这种情况下,用户将通过SaaS API提供程序进行身份验证。
  • 必须可以使用用户名和密码登录。
  • 必须可以使用SaaS API提供程序登录。

让我们开始满足这些要求。 我们要做的第一件事是为我们的应用程序创建一个登录页面。

创建登录页面

我们的应用程序的登录页面具有以下三个职责:

  1. 它必须提供一种使用用户名和密码登录的方法。
  2. 它必须具有指向注册页面的链接。 如果用户想要创建“传统”用户帐户,则可以通过单击此链接来执行此操作。
  3. 它必须具有启动社交登录流程的链接。 这些链接可用于两个目的:
    • 如果有问题的用户具有用户帐户,则可以使用SaaS API提供程序登录。
    • 如果用户没有用户帐户,则可以使用SaaS API提供程序来创建一个。

我们在本教程的第一部分中创建的应用程序上下文配置为登录页面指定了一些要求。 这些要求是:

  1. 如果匿名用户尝试访问受保护的页面,则会将其重定向到URL'/ login'。
  2. 提交我们的应用程序的登录表单后,我们的应用程序必须创建一个POST请求以url'/ login / authenticate'。
  3. 我们必须在提交登录表单时创建的POST请求中包含CSRF令牌 。 原因是当我们使用Java配置来配置Spring Security时,默认情况下启用了Spring Security 3.2.0CSRF保护
  4. username参数的名称是username 。 使用Java配置配置Spring Security时,这是username参数的默认值
  5. password参数的名称为password 。 使用Java配置配置Spring Security时,password参数的默认值。
  6. 如果表单登录失败,则将用户重定向到url'/ login?error = bad_credentials'。 这意味着当请求登录页面并且错误请求参数的值是'bad_credentials'时,我们必须向用户显示错误消息。
  7. SocialAuthenticationFilter处理GET请求发送到url'/ auth / {provider}'的过程。 这意味着
    • 我们可以通过将GET请求发送到url'/ auth / facebook'来启动Facebook登录流程。
    • 我们可以通过将GET请求发送到网址“ / auth / twitter”来启动Twitter登录流程。

让我们从创建一个呈现登录页面的控制器开始。

创建控制器

通过执行以下步骤,我们可以实现呈现登录页面的控制器:

  1. 创建一个LoginController类,并使用@Controller注释对创建的类进行注释。
  2. showLoginPage()方法添加到控制器类。 此方法返回渲染视图的名称。
  3. 通过执行以下步骤来实现showLoginPage()方法:
    1. 使用@RequestMapping注释对方法进行注释,并确保showLoginPage()方法处理发送到url'/ login'的GET请求。
    2. 返回登录视图的名称(“用户/登录”)。

LoginController类的源代码如下所示:

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";
    }
}

下一步是使用JSP创建登录页面。 让我们看看这是如何完成的。

创建JSP页面

我们可以按照以下步骤创建登录页面:

  1. 确保仅对匿名用户显示登录表单和社交登录按钮。 我们可以按照以下步骤进行操作:
    1. 在Spring Security标签库authorize标签内包装登录表单和社会登录按钮。
    2. 访问属性的值设置为isAnonymous()
  2. 如果登录失败,则显示错误消息。 如果名为error的请求参数的值为'bad_credentials' ,则可以使用Spring标记库message标记获取本地化的错误消息。
  3. 通过执行以下步骤来实现登录表单:
    1. 确保在提交登录表单后,将POST请求发送到URL'/ login / authenticate'。
    2. 将CSRF令牌添加到提交登录表单时发送的请求中。 这是必需的,因为我们在本教程的第一部分中启用了Spring Security的CSRF保护。
    3. 用户名字段添加到登录表单。
    4. 在登录表单中添加一个密码字段。
    5. 在登录表单中添加一个提交按钮。
  4. 在登录表单下方添加“创建用户帐户”链接。 此链接创建一个GET请求,以请求url'/ user / register'(注册页面)。
  5. 通过执行以下步骤,将社交符号按钮添加到登录页面:
    1. 添加Facebook登录按钮。 此按钮必须创建一个GET请求以url'/ auth / facebook'。
    2. 添加Twitter登录按钮。 此按钮必须创建一个GET请求以url'/ auth / twitter'。
  6. 如果经过身份验证的用户访问登录页面,请确保显示帮助消息。 我们可以按照以下步骤进行操作:
    1. 将错误消息区域包装在Spring Security标签库authorize标签内
    2. 访问属性的值设置为isAuthenticated()
    3. 通过使用Spring标记库message标记获取本地化的错误消息。

login.jsp页面的源代码如下所示:

<!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>

注意:我们的应用程序使用Twitter Bootstrap 3 。 社交登录按钮是使用称为Twitter Bootstrap 3的Twitter Bootstrap插件(称为Social Buttons)创建的。

现在,我们已经创建了满足我们要求的登录页面。 登录页面的相关部分如下所示:

社会登录形式

我们的下一步是实现注册功能。 让我们开始吧。

实施注册功能

我们的示例应用程序的注册功能有两个要求:

  1. 必须有可能创建一个“普通”用户帐户。
  2. 必须可以通过使用社交登录来创建用户帐户。

另外,我们在本教程的第一部分中创建的应用程序上下文配置为注册功能指定了一个要求:

注册页面的网址必须为“ / signup”。 这是注册(也称为注册)页面的默认值,目前,如果我们使用Java配置来配置应用程序上下文,则无法覆盖此url。 但是,由于url'/ signup'看起来有点丑陋,因此我们将其替换为url'/ user / register'。

注意 :如果使用XML配置文件配置了应用程序上下文,则可以覆盖注册URL的默认值(查找名为signUpUrl的属性)。

通过使用以下方法之一,示例应用程序的用户可以进入注册页面:

  1. 他单击“创建用户帐户”链接。 此链接开始“正常”注册过程。
  2. 他单击社交登录按钮,这将启动社交登录流程。

因为很难从如此简短的描述中获得总体思路,所以我创建了一个图,该图说明了用户在结束示例应用程序的注册页面之前必须遵循的步骤。 此图有两个规则:

  1. 灰色代表动作,这些动作是我们示例应用程序的责任。
  2. 蓝色代表由SaaS API提供程序负责的操作。

该图如下所示:

注册

让我们继续前进,首先为注册表单创建一个表单对象。

创建表单对象

表单对象是一个数据传输对象,其中包含输入到注册表中的信息,并指定用于验证该信息的验证约束。

在实现表单对象之前,让我们快速看一下用于验证表单对象的验证约束。 这些约束描述如下:

  • @Email批注可确保用户提供的电子邮件地址格式正确。
  • @NotEmpty批注确保该字段的值不能为空或null。
  • @Size批注可确保字段值的长度不超过字段的最大长度。

让我们继续创建表单对象。 我们可以按照以下步骤进行操作:

  1. 创建一个名为RegistrationForm的类。
  2. 电子邮件字段添加到该类,并按照以下规则指定其验证约束:
    1. 电子邮件必须格式正确。
    2. 电子邮件不能为空或为空。
    3. 电子邮件的最大长度为100个字符。
  3. 向这些类添加一个firstName字段,并按照以下规则指定其验证约束:
    1. 名字不能为空或为空。
    2. 名字的最大长度为100个字符。
  4. 按照以下规则,将lastName字段添加到类中并指定其验证约束:
    1. 姓氏不能为空或为空。
    2. 姓氏的最大长度为100个字符。
  5. 密码字段添加到该类。
  6. passwordVerification字段添加到该类。
  7. signInProvider字段添加到该类。 该字段的类型为SocialMediaService
  8. isNormalRegistration()方法添加到创建的类。 如果signInProvider字段的值为null,则此方法返回true。 如果该字段的值不为null,则此方法返回false。
  9. isSocialSignIn()方法添加到创建的类。 如果signInProvider字段的值不为null,则此方法返回true。 如果该字段的值为null,则此方法返回false。

RegistrationForm类的源代码如下所示:

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.
}

SocialMediaService是一个枚举,用于标识用于认证用户的SaaS API提供程序。 其源代码如下:

public enum SocialMediaService {
    FACEBOOK,
    TWITTER
}

等一下,我们不是就忘了什么吗?

那些奇怪的注释(例如@PasswordsNotEqual@PasswordsNotEmpty)到底是什么?

好吧,它们是自定义bean验证约束。 让我们找出如何创建这些约束。

创建自定义验证约束

我们必须为示例应用程序创建两个自定义验证约束。 如果用户正在创建“普通”用户帐户,我们必须确保:

  1. 我们的表单对象的passwordpasswordVerification字段不能为空或为null。
  2. passwordpasswordVerification字段相等。

我们可以按照以下步骤创建自定义验证约束:

  1. 创建一个约束注释
  2. 实现一个自定义验证器类,以确保约束不被打破。

注意: Hibernate Validator 4.2的参考手册具有有关创建自定义验证约束的更多信息。

让我们从创建约束注释开始。

创建约束注释

创建约束注释时,我们必须始终遵循以下常见步骤:

  1. 创建注释类型。 假设注释类型的名称为CommonConstraint
  2. @Target注释注释创建的注释类型,并将其值设置为{ElementType.TYPE,ElementType.ANNOTATION_TYPE}ElementType枚举的Javadoc)。 这意味着可以使用@CommonConstraint批注对类和批注类型进行批注。
  3. @Retention批注注释创建的批注类型,并将其值设置为RetentionPolicy.RUNTIME 。 这意味着@CommonConstraint批注在运行时可用,并且可以通过反射来读取。
  4. 使用@Constraint批注对创建的批注类型进行批注,并设置其validatedBy属性的值。 此属性的值指定用于验证用@CommonConstraint注释注释的类的类。
  5. @Documented注释对类进行注释。 这意味着@CommonConstraint注释在所有带有注释的类的Javadoc文档中都是可见的。
  6. 消息属性添加到注释类型。 该属性的类型为String ,其默认值为'CommonConstraint'。
  7. 属性添加到注释类型。 该属性的类型为Class <?>类型的数组,其默认值为空数组。 此属性允许创建验证组
  8. 有效负载属性添加到注释类型。 此属性的类型是Class <?类型的数组 扩展Payload> ,其默认值为空数组。 Bean验证API不会使用此属性,但是API的客户端可以将自定义PayLoad对象分配给约束。

@CommonConstraint批注的源代码如下所示:

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 {};
}

让我们继续前进,了解如何创建@PasswordsNotEmpty@PasswordNotEqual批注。 首先,我们必须创建@PasswordsNotEmpty批注。 我们可以按照以下步骤进行操作:

  1. 请遵循前面介绍的常见步骤,并对创建的注释进行以下更改:
    1. 将注释类型重命名为PasswordsNotEmpty
    2. @Constraint批注的validatedBy属性的值设置为PasswordsNotEmptyValidator.class
  2. triggerFieldName属性添加到注释类型。 此属性的类型为String ,其默认值为空字符串。 此属性指定字段的名称,如果其值为null,则触发我们的自定义约束。
  3. passwordFieldName属性添加到注释类型。 此属性的类型为String ,其默认值为空字符串。 此属性指定包含用户密码的字段的名称。
  4. passwordVerificationFieldName属性添加到注释类型。 此属性的类型为String ,其默认值为空字符串。 此属性指定包含用户密码验证的字段的名称。

@PasswordsNotEmpty批注的源代码如下所示:

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 "";
}

其次,我们必须创建@PasswordsNotEqual批注。 我们可以按照以下步骤进行操作:

  1. 请遵循前面介绍的常见步骤,并对创建的注释进行以下更改:
    1. 将注释类型重命名为PasswordsNotEqual
    2. @Constraint批注的validatedBy属性的值设置为PasswordsNotEqualValidator.class
  2. passwordFieldName属性添加到注释类型。 此属性的类型为String ,其默认值为空字符串。 此属性指定包含用户密码的字段的名称。
  3. passwordVerificationFieldName属性添加到注释类型。 此属性的类型为String ,其默认值为空字符串。 此属性指定包含用户密码验证的字段的名称。

@PasswordsNotEqual批注的源代码如下所示:

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 "";
}

现在,我们已经创建了约束注释。 让我们继续来看一下在为自定义约束注释实现验证器类时使用的实用程序类。

创建验证实用程序类

验证实用程序类提供了两种静态方法,下面将对其进行介绍:

  • 第一种方法用于将验证错误添加到已验证对象的字段。
  • 第二种方法返回所请求字段的值。

我们可以按照以下步骤实现此类:

  1. 创建一个名为ValidatorUtil的类。
  2. 将一个addValidationError()方法添加到ValidatorUtil类。 此方法采用以下两个参数:
    1. 第一个参数是字段的名称。
    2. 第二个参数是ConstraintValidatorContext对象。
  3. 通过执行以下步骤来实现addValidationError()方法:
    1. 创建一个新的约束冲突消息,并确保在构建约束冲突消息时,将由约束注释指定的消息用作前缀。
    2. 将字段添加到约束验证错误。
    3. 创建约束验证错误。
  4. 将一个getFieldValue()方法添加到ValidatorUtil类。 此方法返回指定字段的字段值,并采用以下两个参数:
    1. 第一个参数是包含请求字段的对象。
    2. 第二个参数是请求字段的名称。
  5. 通过执行以下步骤来实现getFieldValue()方法:
    1. 获取对反映所请求字段的Field对象的引用。
    2. 确保即使字段是私有的,我们也可以访问该字段的值。
    3. 返回字段值。

ValidatorUtil类的源代码如下所示:

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);
    }
}

现在,我们准备实现验证器类。 让我们看看如何完成。

创建验证器类

首先,我们必须创建验证器类,该类可以验证使用@PasswordsNotEmpty注释注释的类。 我们可以按照以下步骤进行操作:

  1. 创建一个PasswordsNotEmptyValidator类,并实现ConstraintValidator接口。 ConstraintValidator接口定义两个类型参数,下面将对其进行描述:
    1. 第一个类型参数是注释类型。 将此类型参数的值设置为PasswordsNotEmpty
    2. 第二个类型参数是可以由验证器验证的元素的类型。 将此类型参数的值设置为Object (我们可以将其设置为RegistrationForm,但是使用Object类型可确保我们的验证器不限于此示例应用程序)。
  2. 在创建的类中添加一个私人的validateTriggerFieldName字段,并将其类型设置为String
  3. 在创建的类中添加一个专用的passwordFieldName字段,并将其类型设置为String
  4. 在创建的类中添加一个专用的passwordVerificationFieldName字段,并将其类型设置为String
  5. ConstraintValidator接口的initialize(PasswordsNotEmptyconstraintAnnotation)方法添加到验证器类,并通过以下步骤实现该方法:
    1. 设置validationTriggerFieldName字段的值。
    2. 设置passwordFieldName字段的值。
    3. 设置passwordVerificationFieldName字段的值。
  6. 将私有的isNullOrEmpty(String field)方法添加到创建的类。 如果作为方法参数给出的String为null或为空,则此方法返回true。 否则,此方法返回false。
  7. 向创建的类添加一个专用的passwordsAreValid(Object value,ConstraintValidatorContext context)方法。 如果密码字段有效,则此方法返回true,否则返回false。 此方法采用以下两个方法参数:
    1. 第一个方法参数是经过验证的对象。
    2. 第二个方法参数是ConstraintValidatorContext对象。
  8. 通过执行以下步骤来实现passwordsAreValid()方法:
    1. 通过调用ValidatorUtil类的getFieldValue()方法获取密码字段的值。 将验证的对象和密码字段的名称作为方法参数传递。
    2. 如果密码字段的值为空或为null,请通过调用ValidatorUtil类的addValidationError()方法来添加验证错误。 将密码字段的名称和ConstraintValidatorContext对象作为方法参数传递。
    3. 通过调用ValidatorUtil类的getFieldValue()方法获取passwordVerification字段的值。 将验证的对象和密码验证字段的名称作为方法参数传递。
    4. 如果密码验证字段的值为空或为null,请通过调用ValidatorUtil类的addValidationError()方法来添加验证错误。 将密码验证字段的名称和ConstraintValidatorContext对象作为方法参数传递。
    5. 如果发现验证错误,则返回false。 否则返回true。
  9. ConstraintValidator接口的isValid(Object value,ConstraintValidatorContext上下文)方法添加到验证器类,并通过以下步骤实现它:
    1. 通过调用ConstraintValidatorContext接口的disableDefaultConstraintViolation()方法来禁用默认错误消息。
    2. 向该方法添加一个try-catch结构,并捕获所有检查的异常。 如果引发了检查的异常,请捕获该异常并将其包装在RuntimeException中 。 这是必需的,因为ConstraintValidator接口的isValid()方法无法引发已检查的异常,请按照以下步骤实现try块:
      1. 通过调用ValidatorUtil类的getFieldValue()方法来获取验证触发器字段的值。 将验证对象和验证触发器字段的名称作为方法参数传递。
      2. 如果验证触发器字段的值为null,则调用passwordFieldsAreValid()方法并将验证对象和ConstraintValidatorContext对象作为方法参数传递。 返回此方法返回的布尔值。
      3. 如果验证触发器字段的值不为null,则返回true。

PasswordsNotEmptyValidator类的源代码如下所示:

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();
    }
}

其次,我们必须创建验证器类,以验证带有@PasswordsNotEqual注释的类。 我们可以按照以下步骤进行操作:

  1. 创建一个PasswordsNotEqualValidator类,并实现ConstraintValidator接口。 ConstraintValidator接口定义两个类型参数,下面将对其进行描述:
    1. 第一个类型参数是注释类型。 将此类型参数的值设置为PasswordsNotEqual
    2. 第二个类型参数是可以由验证器验证的元素的类型。 将此类型参数的值设置为Object (我们可以将其设置为RegistrationForm,但是使用Object类型可确保我们的验证器不限于此示例应用程序)。
  2. 在创建的类中添加一个专用的passwordFieldName字段,并将其类型设置为String
  3. 在创建的类中添加一个专用的passwordVerificationFieldName字段,并将其类型设置为String
  4. ConstraintValidator接口的initialize(PasswordsNotEqual ConsulantAnnotation)方法添加到验证器类,并通过以下步骤实现该方法:
    1. 设置passwordFieldName字段的值。
    2. 设置passwordVerificationFieldName字段的值。
  5. 向创建的类添加一个专用的passwordsAreNotEqual(String password,String passwordVerification)方法。 如果作为方法参数给出的密码和密码验证不相等,则此方法返回true。 否则,此方法返回false。
  6. ConstraintValidator接口的isValid(Object value,ConstraintValidatorContext上下文)方法添加到验证器类,并通过以下步骤实现它:
    1. 通过调用ConstraintValidatorContext接口的disableDefaultConstraintViolation()方法来禁用默认错误消息。
    2. 向该方法添加一个try-catch结构,并捕获所有检查的异常。 如果引发了检查的异常,请捕获该异常并将其包装在RuntimeException中 。 这是必需的,因为ConstraintValidator接口的isValid()方法无法引发已检查的异常,请按照以下步骤实现try块:
      1. 通过调用ValidatorUtil类的getFieldValue()方法来获取密码字段的值。 将验证的对象和密码字段的名称作为方法参数传递。
      2. 通过调用ValidatorUtil类的getFieldValue()方法来获取密码验证字段的值。 将验证的对象和密码验证字段的名称作为方法参数传递。
      3. 通过调用passwordsAreNotEqual()方法来检查密码是否不相等。 将密码和密码验证作为方法参数传递。
      4. 如果密码和密码验证不相等,请通过调用ValidatorUtil类的addValidationError()方法将验证错误添加到密码和密码验证字段中。 返回false。
      5. 如果密码和密码验证分别是,则返回true。

PasswordsNotEqualValidator的源代码如下所示:

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));
    }
}

这就对了。 现在,我们已经实现了自定义验证约束。 让我们找出如何呈现注册页面。

渲染注册页面

我们的注册页面的要求如下:

  1. 注册页面的网址必须为“ / user / register”。
  2. 如果用户正在创建“普通”用户帐户,则我们的应用程序必须提供空白的注册表格。
  3. 如果用户使用社交登录,则必须使用SaaS API提供程序提供的信息来预填充注册表单的表单字段。

让我们从发现如何将用户重定向到注册页面开始。

将用户重定向到注册页面

在我们开始实现呈现注册页面的controller方法之前,我们必须实现一个将用户重定向到正确url的controller。 该控制器的要求如下:

  • 它必须处理发送到url'/ signup'的GET请求。
  • 它必须将请求重定向到URL“ /用户/注册”。

注意:如果要使用XML配置文件配置应用程序的应用程序上下文,则可以跳过此步骤。 仅当您使用Java配置来配置应用程序的应用程序上下文时,才需要执行此步骤。 这样做的原因是,目前只有在使用XML配置时 (搜索名为signUpUrl的属性),您才可以配置注册URL。

我们可以按照以下步骤实现此控制器:

  1. 创建一个SignUpController类,并使用@Controller注释对该类进行注释。
  2. 将公共的redirectRequestToRegistrationPage()方法添加到创建的类中。 此方法的返回类型为String
  3. 通过执行以下步骤来实现redirectRequestToRegistrationPage()方法:
    1. 使用@RequestMapping注释对方法进行注释,并确保该方法处理发送到url'/ signup'的GET请求。
    2. 返回字符串 “ redirect:/ user / register”。 这会将请求重定向到url'/ user / register'

SignUpController类的源代码如下所示:

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";
    }
}

让我们继续前进,找出如何实现呈现注册页面的controller方法。

实施控制器方法

呈现注册页面的控制器方法具有一项重要职责:创建表单对象并预填充其字段。 如果用户正在创建“普通”用户帐户,则此控制器方法将创建一个空的表单对象。 另一方面,如果用户正在使用社交登录来创建用户帐户,则此控制器方法将使用所使用的SaaS API提供程序提供的信息来设置表单对象的字段值。 我们可以通过执行以下步骤来实现呈现注册页面的controller方法:

  1. 创建控制器类,并使用@Controller注释对其进行注释。
  2. @SessionAttributes批注注释该类,并将其值设置为'user'。 我们使用此注释来确保将名为“用户”(我们的表单对象)的模型属性存储到会话中。
  3. 将私有createRegistrationDTO()方法添加到该类。 此方法将Connection对象作为方法参数,并返回RegistrationForm对象。 我们可以通过执行以下步骤来实现此方法:
    1. 创建一个新的RegistrationForm对象。
    2. 如果作为方法参数给出的Connection对象不为null,则用户正在使用社交登录创建新的用户帐户。在这种情况下,我们必须
      1. 通过调用Connection类的fetchUserProfile()方法来获取UserProfile对象。 该对象包含SaaS API提供程序返回的用户信息。
      2. 将电子邮件,名字和姓氏设置为表单对象。 我们可以通过调用UserProfile类的方法来获取此信息。
      3. 通过调用Connection类的getKey()方法来获取ConnectionKey对象。 该对象包含使用的社交登录提供者的ID和提供者特定的用户ID。
      4. 通过执行以下步骤,将登录提供程序设置为表单对象:
        1. 通过调用ConnectionKey类的getProviderId()方法获取登录提供程序。
        2. getProviderId()方法返回的String转换为大写。
        3. 通过调用社会媒体服务枚举的nameOf()方法来获取正确的值。 将登录提供程序(大写)作为方法参数传递(这意味着SocialMediaService枚举的值取决于登录提供程序ID)。
        4. 将返回值设置为表单对象。
    3. 返回表单对象。
  4. 呈现注册页面的控制器方法称为showRegistrationForm() 。 将此方法添加到控制器类并通过以下步骤实现它:
    1. 使用@RequestMapping注释对方法进行注释,并确保控制器方法处理发送到URL'/ user / register'的GET请求。
    2. 添加一个WebRequest对象作为方法参数。 我们使用WebRequest作为方法参数,因为它使我们可以轻松访问请求元数据。
    3. 添加一个Model对象作为方法参数。
    4. 通过调用ProviderSignInUtils类的静态getConnection()方法来获取Connection对象。 将WebRequest对象作为方法参数传递。 如果WebRequest对象不包含SaaS API提供程序元数据(表示用户正在创建普通用户帐户),则此方法返回null。 如果找到元数据,则此方法使用该信息创建Connection对象,并返回创建的对象。
    5. 通过调用私有的createRegistrationDTO()方法来获取表单对象。 将Connection对象作为方法参数传递。
    6. 将表单对象设置为要建模的模型属性,称为“用户”。
    7. 返回注册表单视图的名称(“ user / registrationForm”)。

UserController类的相关部分如下所示:

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;
    }
}

接下来要做的就是创建JSP页面。 让我们继续前进,找出实现方法。

创建JSP页面

通过执行以下步骤,我们可以创建包含注册表单的JSP页面:

  1. 确保仅向匿名用户显示注册表单。 我们可以按照以下步骤进行操作:
    1. 在Spring Security标签库authorize标签内包装登录表单和社会登录按钮。
    2. 访问属性的值设置为isAnonymous()
  2. 通过执行以下步骤来实施注册表:
    1. 确保在提交注册表后,将POST请求发送到URL'/ user / register'。
    2. 将CSRF令牌添加到请求中 。 这是必需的,因为我们在本教程的第一部分中启用了Spring Security的CSRF保护。
    3. 如果从表单对象中找到登录提供程序,请将其作为隐藏字段添加到表单中。
    4. 在表单中添加一个firstName字段,并确保显示与firstName字段有关的验证错误。
    5. 在表单中添加一个lastName字段,并确保显示与lastName字段有关的验证错误。
    6. 电子邮件字段添加到表单,并确保显示有关电子邮件字段的验证错误。
    7. 如果用户正在创建普通用户帐户(表单对象的signInProvider字段的值为null),请按照下列步骤操作:
      1. 在表单中添加一个密码字段,并确保显示有关密码字段的验证错误。
      2. passwordVerification字段添加到表单,并确保显示与passwordVerification字段有关的验证错误。
    8. 将提交按钮添加到表单
  3. 如果经过身份验证的用户访问注册页面,请确保显示帮助消息。 我们可以按照以下步骤进行操作:
    1. 将错误消息区域包装在Spring Security标签库authorize标签内
    2. 访问属性的值设置为isAuthenticated()
    3. 通过使用Spring标记库message标记获取本地化的错误消息。

注意: Spring 3.2参考手册提供了有关Spring JSP标签库form标签的更多信息。

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. 我们可以按照以下步骤进行操作:

  1. 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.
  2. 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.
  3. Create a new user account and log in the user.
  4. Redirect the user to the front page.

This process is illustrated in the following diagram:

social-registrationform-submit

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:

  1. Add a private UserService field to the controller class.
  2. Add a constructor which takes a UserService object as a constructor argument to the RegistrationController class and implement it by following these steps:
    1. Annotate the constructor with the @Autowired annotation. This ensures that the dependencies of this bean are injected by using constructor injection.
    2. Set the value of service field.
  3. 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:
    1. The objectName parameter is the name of the form object.
    2. The fieldName parameter is the name of the form field which contains invalid value.
    3. The fieldValue parameter contains the value of the form field.
    4. The errorCode parameter is the error code of the field error.
    5. The result parameter is a BindingResult object.
  4. Implement the addFieldError() method by following these steps:
    1. Create a new FieldError object by using the method parameters.
    2. Add the created FieldError object to the binding result by calling the AddError() method of the BindingResult class.
  5. 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. 通过执行以下步骤来实现此方法:
    1. Add a try-catch structure to the method and catch DuplicateEmailException objects.
    2. 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.
    3. 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.
  6. Add a public registerUserAccount() method to the controller class and implement it by following these steps:
    1. Annotate the method with the @RequestMapping annotation and ensure that the method processes POST request send to url '/user/register'.
    2. Add a RegistrationForm object as a method parameter and annotate it with the following annotations:
      1. Annotate the method parameter with the @Valid annotation. This ensures that the information of this object is validated before the controller method is called.
      2. Annotate the method parameter with the @ModelAttribute annotation and set its value to 'user' (this is the name of the form object).
    3. Add a BindingResult object as a method parameter.
    4. 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.
    5. If the binding result has errors, return the name of the form view.
    6. Call the private createUserAccount() method and pass the RegistrationForm and BindingResult objects as method parameters.
    7. 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.
    8. Log the created user in by calling the static loginInUser() method of the SecurityUtil class. Pass the created User object as a method parameter.
    9. 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.
    10. 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. 我们可以通过执行以下步骤来实现此方法:

  1. Create a new ExampleUserDetails object by using the information of the created user.
  2. Create a new UsernamePasswordAuthenticationToken object and pass the following arguments to its constructor:
    1. The first argument is the principal (aka logged in user). Pass the created ExampleUserDetails object as the first constructor argument.
    2. The second argument contains the credentials of the user. Pass null as the second constructor argument.
    3. The third argument contains the the authorities of the user. We can get the authorities by calling the getAuthorities() method of the ExampleUserDetails class.
  3. Set created Authentication object into security context by following these steps:
    1. Get the SecurityContext object by calling the static getContext() method of the SecurityContextHolder class.
    2. 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. 我们可以按照以下步骤实现此类:

  1. Create an abstract BaseEntity class which has one type parameter called ID . This parameter is the type of the entity's private key.
  2. Annotate the class with the @MapperSuperclass annotation. This means that the mapping information of the BaseEntity class is applied to its subclasses.
  3. Add a DateTime field called creationTime to the class and configure it by following these steps:
    1. Annotate the field with the @Column annotation and configure the name of the database column. The value of the nullable attribute to false.
    2. 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.
  4. Add a DateTime field called modificationTime to the class and configure it by using these steps:
    1. Annotate the field with the @Column annotation and set the name of the database column. Ensure that this column is not nullable.
    2. 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).
  5. 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.
  6. Add an abstract getId() method to the class. This method returns the id of the actual entity.
  7. 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.
  8. 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:

  1. Create a User class which extends the BaseEntity class and give the type of its private key ( Long ) as a type parameter.
  2. Annotate the created class with the @Entity annotation.
  3. Annotate the created class with the @Table annotation and ensure that the user information is stored to a database table called 'users'.
  4. Add a private id field to the class and set its type to Long . Configure the field by following these steps:
    1. Annotate the field with the @Id annotation. This annotation is used to specify the primary key of the entity.
    2. 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.
  5. 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:
    1. The email address is stored to the 'email' column of the 'users' table.
    2. The maximum length of the email address is 100 characters.
    3. The email address cannot be null.
    4. The email address must be unique.
  6. 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:
    1. The first name is stored to the 'first_name' column of the 'users' table.
    2. The maximum length of the first name is 100 characters.
    3. The first name cannot be null.
  7. 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:
    1. The last name is stored to the 'last_name' column of the 'users' table.
    2. The maximum length of the last name is 100 characters.
    3. The last name cannot be null.
  8. 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:
    1. The password is stored to the 'password' column of the 'users' table.
    2. The maximum length of the password is 255 characters.
  9. 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:
    1. The role is stored to the 'role' column of the 'users' table.
    2. The maximum length of the role is 20 characters.
    3. The role cannot be null.
  10. 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:
    1. The sign in provider is stored to the 'sign_in_provider' field of the 'users' table.
    2. The maximum length of the sign in provider is 20 characters.
  11. Add a public static inner class called Builder to the User class. Implement this class by following these steps:
    1. Add a User field to the class. This field holds a reference to the constructed User object.
    2. 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 .
    3. 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.
    4. Add a build() method to the builder class. This method returns the created User object.
  12. 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. 其源代码如下:

public enum Role {
    ROLE_USER
}

The SocialMediaService is an enum which identifies the SaaS API provider which was used to authenticate the user. 其源代码如下:

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:

  1. Create a class which implements the UserService interface and annotate this class with the @Service annotation.
  2. Add a PasswordEncoder field to the created class.
  3. Add a UserRepository field to to created class.
  4. Add a constructor which takes PasswordEncoder and UserRepository objects as constructor arguments to the service class. 通过执行以下步骤来实现构造函数:
    1. Annotate the constructor with the @Autowired annotation. This ensures that the dependencies of this bean are injected by using constructor injection.
    2. Set the values of passwordEncoder and repository fields.
  5. Add a private emailExist() method to the service class. This method takes a email address as a method argument and returns a boolean . 通过执行以下步骤来实现此方法:
    1. 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.
    2. If a user is found, return true.
    3. If a user is not found, return false.
  6. Add a private encodePassword() method to service class. This method takes a RegistrationForm object as a method parameter and returns the encoded password. 通过执行以下步骤来实现此方法:
    1. 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.
    2. If the user is creating a user account by using social sign in, return null.
  7. Add a registerNewUserAccount() method to the service class and implement it by following these steps:
    1. Annotate the method with the @Transactional annotation. This means that the method is executed “inside” a read-write transaction.
    2. 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 .
    3. Obtain the encoded password by calling the private encodePassword() method. Pass the RegistrationForm object as a method parameter.
    4. Get the builder object by calling the getBuilder() method of the User class and set the following information to the created User object:
      1. 电子邮件地址
      2. 名字
      3. 密码
    5. 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()
    6. Create the User object.
    7. 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.
    8. Return the persisted object.

RepositoryUserService类的源代码如下所示:

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. 让我们找出如何做到这一点。

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:

  1. 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.
  2. 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);
}

就是这样! Let's move on and spend a moment to summarize what we have achieved during this blog post.

摘要

We have now implemented the requirements of our example application. 这意味着

  • 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:

social-signin-flow

这篇博客文章告诉我们以下内容:

  • 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.

PS The example application of this blog post is available at Github .


翻译自: https://www.javacodegeeks.com/2013/10/adding-social-sign-in-to-a-spring-mvc-web-application-registration-and-login.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值