{fbmip:fixed type="top" id="mipfixed" dataSlide="header-fixed-slide" class="fb-header-fixed"}
{fbview:mainmenu menuItemCode='$menuItemCode'/}
{/fbmip:fixed}
{fbmip:img fit='cover' src="$banner2['img_url']" alt="$banner2['title']"/}

在Spring开发中使用邮件进行用户注册验证

  • 发布时间:
  • 浏览:333
  • 来源:平步科技官网

1.概述

本文将继续注册Spring Security系列,其中包括注册过程中缺少的部分 - 验证用户的电子邮件以确认其帐户。

注册确认机制强制用户响应成功注册后发送的“ 确认注册 ”电子邮件,以验证其电子邮件地址并激活其帐户。用户通过单击通过电子邮件发送给他们的唯一激活链接来执行此操作。

遵循此逻辑,在此过程完成之前,新注册的用户将无法登录系统。

2.验证令牌

我们将使用简单的验证令牌作为验证用户的关键工件。

2.1。VerificationToken实体

该VerificationToken实体必须满足以下条件:



它必须链接回用户(通过单向关系)

它将在注册后立即创建

它将在创建后24小时内到期

具有独特的随机生成值

要求2和3是注册逻辑的一部分。另外两个是在一个简单的VerificationToken实体中实现的,如例2.1中的那个:

@Entity

public class VerificationToken {

    private static final int EXPIRATION = 60 * 24;

 

    @Id

    @GeneratedValue(strategy = GenerationType.AUTO)

    private Long id;

     

    private String token;

   

    @OneToOne(targetEntity = User.class, fetch = FetchType.EAGER)

    @JoinColumn(nullable = false, name = "user_id")

    private User user;

     

    private Date expiryDate;

    

    private Date calculateExpiryDate(int expiryTimeInMinutes) {

        Calendar cal = Calendar.getInstance();

        cal.setTime(new Timestamp(cal.getTime().getTime()));

        cal.add(Calendar.MINUTE, expiryTimeInMinutes);

        return new Date(cal.getTime().getTime());

    }

     

    // standard constructors, getters and setters

}

请注意用户的nullable = false以确保VerificationToken < - > 用户关联中的数据完整性和一致性。

2.2。启用的字段添加用户

最初,当用户注册时,此启用字段将设置为false。在帐户验证过程中 - 如果成功 - 它将成为现实。

让我们首先将字段添加到User实体:

public class User {

    ...

    @Column(name = "enabled")

    private boolean enabled;

     

    public User() {

        super();

        this.enabled=false;

    }

    ...

}

请注意我们如何将此字段的默认值设置为false。

3.账户注册期间

让我们在用户注册用例中添加两个额外的业务逻辑:

为用户生成VerificationToken并保留它

发送电子邮件以确认帐户 - 其中包含具有VerificationToken值的确认链接

3.1。使用Spring事件创建令牌并发送验证电子邮件

这两个额外的逻辑不应该由控制器直接执行,因为它们是“附带的”后端任务。

控制器将发布Spring ApplicationEvent以触​​发这些任务的执行。这就像注入ApplicationEventPublisher然后使用它来发布注册完成一样简单。

例3.1。显示这个简单的逻辑:

@Autowired

ApplicationEventPublisher eventPublisher

 

@RequestMapping(value = "/user/registration", method = RequestMethod.POST)

public ModelAndView registerUserAccount(

  @ModelAttribute("user") @Valid UserDto accountDto, 

  BindingResult result, 

  WebRequest request, 

  Errors errors) {

  

    if (result.hasErrors()) {

        return new ModelAndView("registration", "user", accountDto);

    }

     

    User registered = createUserAccount(accountDto);

    if (registered == null) {

        result.rejectValue("email", "message.regError");

    }

    try {

        String appUrl = request.getContextPath();

        eventPublisher.publishEvent(new OnRegistrationCompleteEvent

          (registered, request.getLocale(), appUrl));

    } catch (Exception me) {

        return new ModelAndView("emailError", "user", accountDto);

    }

    return new ModelAndView("successRegister", "user", accountDto);

}

另外需要注意的是围绕事件发布的try catch块。只要在事件发布后执行的逻辑中存在异常,这段代码就会显示错误页面,在这种情况下是发送电子邮件。

3.2。事件和听众

现在让我们看看我们的控制器发出的这个新的OnRegistrationCompleteEvent的实际实现,以及将要处理它的监听器:

例3.2.1。- OnRegistrationCompleteEvent

public class OnRegistrationCompleteEvent extends ApplicationEvent {

    private String appUrl;

    private Locale locale;

    private User user;

 

    public OnRegistrationCompleteEvent(

      User user, Locale locale, String appUrl) {

        super(user);

         

        this.user = user;

        this.locale = locale;

        this.appUrl = appUrl;

    }

     

    // standard getters and setters

}

例3.2.2。- RegistrationListener处理OnRegistrationCompleteEvent

@Component

public class RegistrationListener implements

  ApplicationListener<OnRegistrationCompleteEvent> {

  

    @Autowired

    private IUserService service;

  

    @Autowired

    private MessageSource messages;

  

    @Autowired

    private JavaMailSender mailSender;

 

    @Override

    public void onApplicationEvent(OnRegistrationCompleteEvent event) {

        this.confirmRegistration(event);

    }

 

    private void confirmRegistration(OnRegistrationCompleteEvent event) {

        User user = event.getUser();

        String token = UUID.randomUUID().toString();

        service.createVerificationToken(user, token);

         

        String recipientAddress = user.getEmail();

        String subject = "Registration Confirmation";

        String confirmationUrl 

          = event.getAppUrl() + "/regitrationConfirm.html?token=" + token;

        String message = messages.getMessage("message.regSucc", null, event.getLocale());

         

        SimpleMailMessage email = new SimpleMailMessage();

        email.setTo(recipientAddress);

        email.setSubject(subject);

        email.setText(message + " rn" + "http://localhost:8080" + confirmationUrl);

        mailSender.send(email);

    }

}


这里,confirmRegistration方法将接收OnRegistrationCompleteEvent,从中提取所有必要的用户信息,创建验证令牌,持久化,然后将其作为参数发送到“ 确认注册 ”链接。

如上所述,JavaMailSender抛出的任何javax.mail.AuthenticationFailedException都将由控制器处理。

3.3。处理验证令牌参数

当用户收到“ 确认注册 ”链接时,应单击它。

一旦他们这样做 - 控制器将在生成的GET请求中提取令牌参数的值,并将使用它来启用用户。

让我们看一下例3.3.1中的这个过程:

例3.3.1。- RegistrationController处理注册确认

@Autowired

private IUserService service;

 

@RequestMapping(value = "/regitrationConfirm", method = RequestMethod.GET)

public String confirmRegistration

  (WebRequest request, Model model, @RequestParam("token") String token) {

  

    Locale locale = request.getLocale();

     

    VerificationToken verificationToken = service.getVerificationToken(token);

    if (verificationToken == null) {

        String message = messages.getMessage("auth.message.invalidToken", null, locale);

        model.addAttribute("message", message);

        return "redirect:/badUser.html?lang=" + locale.getLanguage();

    }

     

    User user = verificationToken.getUser();

    Calendar cal = Calendar.getInstance();

    if ((verificationToken.getExpiryDate().getTime() - cal.getTime().getTime()) <= 0) {

        String messageValue = messages.getMessage("auth.message.expired", null, locale)

        model.addAttribute("message", messageValue);

        return "redirect:/badUser.html?lang=" + locale.getLanguage();

    } 

     

    user.setEnabled(true); 

    service.saveRegisteredUser(user); 

    return "redirect:/login.html?lang=" + request.getLocale().getLanguage(); 

}

如果出现以下情况,用户将被重定向到包含相应消息的错误页面:

由于某种原因,VerificationToken不存在

该VerificationToken已过期

见例3.3.2。查看错误页面。

例3.3.2。- badUser.html

<html>

<body>

    <h1 th:text="${param.message[0]}>Error Message</h1>

    <a th:href="@{/registration.html}"

      th:text="#{label.form.loginSignUp}">signup</a>

</body>

</html>

如果未找到错误,则启用该用户。

处理VerificationToken检查和到期方案有两个改进的机会:

我们可以使用Cron作业在后台检查令牌过期

我们可以让用户有机会在到期后获得新令牌

我们将推迟为将来的文章生成新令牌,并假设用户确实在此处成功验证了其令牌。