1. Overview
1.概述
This article continues the ongoing Registration with Spring Security series with one of the missing pieces of the registration process – verifying the user’s email to confirm their account.
本文继续正在进行的用Spring Security注册系列,介绍注册过程中缺少的一个环节–验证用户的电子邮件以确认其帐户。
The registration confirmation mechanism forces the user to respond to a “Confirm Registration” email sent after successful registration to verify his email address and activate their account. The user does this by clicking a unique activation link sent to them over email.
注册确认机制迫使用户在成功注册后回复一封”确认注册“的电子邮件,以验证其电子邮件地址并激活其账户。用户通过点击通过电子邮件发送给他们的独特的激活链接来实现这一目的。
Following this logic, a newly registered user will not be able to log into the system until this process is completed.
按照这一逻辑,新注册的用户在完成这一过程之前将无法登录系统。
2. A Verification Token
2.一个验证令牌
We will make use of a simple verification token as the key artifact through which a user is verified.
我们将使用一个简单的验证令牌作为验证用户的关键工具。
2.1. The VerificationToken Entity
2.1.VerificationToken 实体
The VerificationToken entity must meet the following criteria:
VerificationToken实体必须满足以下条件。
- It must link back to the User (via a unidirectional relation)
- It will be created right after registration
- It will expire within 24 hours following its creation
- Has a unique, randomly generated value
Requirements 2 and 3 are part of the registration logic. The other two are implemented in a simple VerificationToken entity like the one in Example 2.1.:
要求2和3是注册逻辑的一部分。另外两个是在一个简单的VerificationToken实体中实现的,比如例2.1中的实体。
Example 2.1.
示例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
}
Note the nullable = false on the User to ensure data integrity and consistency in the VerificationToken<->User association.
注意User上的nullable = false,以确保VerificationToken<–User关联中的数据完整性和一致性。
2.2. Add the enabled Field to User
2.2.将enabled字段添加到User
Initially, when the User is registered, this enabled field will be set to false. During the account verification process – if successful – it will become true.
最初,当User被注册时,这个enabled字段将被设置为false。在账户验证过程中–如果成功–它将变成true。
Let us start by adding the field to our User entity:
让我们先把这个字段添加到我们的 User实体中。
public class User {
...
@Column(name = "enabled")
private boolean enabled;
public User() {
super();
this.enabled=false;
}
...
}
Note how we also set the default value of this field to false.
请注意,我们还将这个字段的默认值设置为false。
3. During Account Registration
3.帐户注册期间
Let’s add two additional pieces of business logic to the user registration use case:
让我们在用户注册用例中增加两个额外的业务逻辑。
- Generate the VerificationToken for the User and persist it
- Send out the email message for account confirmation – which includes a confirmation link with the VerificationToken’s value
3.1. Using a Spring Event to Create the Token and Send the Verification Email
3.1.使用Spring事件来创建令牌并发送验证邮件
These two additional pieces of logic should not be performed by the controller directly because they are “collateral” back-end tasks.
这两个额外的逻辑不应该由控制器直接执行,因为它们是 “附带的 “后端任务。
The controller will publish a Spring ApplicationEvent to trigger the execution of these tasks. This is as simple as injecting the ApplicationEventPublisher and then using it to publish the registration completion.
控制器将发布一个Spring ApplicationEvent来触发这些任务的执行。这就像注入ApplicationEventPublisher一样简单,然后用它来发布注册完成。
Example 3.1. shows this simple logic:
例3.1.显示了这个简单的逻辑。
Example 3.1.
示例3.1.
@Autowired
ApplicationEventPublisher eventPublisher
@PostMapping("/user/registration")
public ModelAndView registerUserAccount(
@ModelAttribute("user") @Valid UserDto userDto,
HttpServletRequest request, Errors errors) {
try {
User registered = userService.registerNewUserAccount(userDto);
String appUrl = request.getContextPath();
eventPublisher.publishEvent(new OnRegistrationCompleteEvent(registered,
request.getLocale(), appUrl));
} catch (UserAlreadyExistException uaeEx) {
ModelAndView mav = new ModelAndView("registration", "user", userDto);
mav.addObject("message", "An account for that username/email already exists.");
return mav;
} catch (RuntimeException ex) {
return new ModelAndView("emailError", "user", userDto);
}
return new ModelAndView("successRegister", "user", userDto);
}
One additional thing to notice is the try catch block surrounding the publishing of the event. This piece of code will display an error page whenever there is an exception in the logic executed after the publishing of the event, which in this case is the sending of the email.
需要注意的另一件事是围绕事件发布的try catch块。这段代码将显示一个错误页面,只要在事件发布后执行的逻辑中出现异常,在本例中就是发送电子邮件的过程。
3.2. The Event and the Listener
3.2.事件和听众
Let’s now see the actual implementation of this new OnRegistrationCompleteEvent that our controller is sending out, as well as the listener that is going to handle it:
现在让我们看看这个新的OnRegistrationCompleteEvent的实际实现,我们的控制器正在发送,以及将处理它的监听器。
Example 3.2.1. – The 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
}
Example 3.2.2. – The RegistrationListener Handles the OnRegistrationCompleteEvent
示例 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?token=" + token;
String message = messages.getMessage("message.regSucc", null, event.getLocale());
SimpleMailMessage email = new SimpleMailMessage();
email.setTo(recipientAddress);
email.setSubject(subject);
email.setText(message + "\r\n" + "http://localhost:8080" + confirmationUrl);
mailSender.send(email);
}
}
Here, the confirmRegistration method will receive the OnRegistrationCompleteEvent, extract all the necessary User information from it, create the verification token, persist it, and then send it as a parameter in the “Confirm Registration” link.
这里,confirmRegistration方法将接收OnRegistrationCompleteEvent,从中提取所有必要的User信息,创建验证令牌,持久化它,然后将它作为参数发送到”ConfirmRegistration“链接。
As was mentioned above, any javax.mail.AuthenticationFailedException thrown by JavaMailSender will be handled by the controller.
如上所述,任何由JavaMailSender抛出的javax.mail.AuthenticationFailedException都将由控制器处理。
3.3. Processing the Verification Token Parameter
3.3.处理验证令牌参数
When the user receives the “Confirm Registration” link they should click on it.
当用户收到”确认注册“链接时,他们应该点击它。
Once they do – the controller will extract the value of the token parameter in the resulting GET request and will use it to enable the User.
一旦他们这样做–控制器将在产生的GET请求中提取令牌参数的值,并将使用它来启用User。
Let’s see this process in Example 3.3.1.:
让我们在例3.3.1中看看这个过程。
Example 3.3.1. – RegistrationController Processing the Registration Confirmation
示例3.3.1.- RegistrationController处理注册确认。
@Autowired
private IUserService service;
@GetMapping("/regitrationConfirm")
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();
}
The user will be redirected to an error page with the corresponding message if:
如果是这样,用户将被重定向到一个带有相应信息的错误页面。
- The VerificationToken does not exist, for some reason or
- The VerificationToken has expired
See Example 3.3.2. to see the error page.
见例3.3.2。来看看错误页面。
Example 3.3.2. – The badUser.html
示例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>
If no errors are found, the user is enabled.
如果没有发现错误,用户就被启用。
There are two opportunities for improvement in handling the VerificationToken checking and expiration scenarios:
在处理 VerificationToken检查和过期情况时,有两个改进的机会。
- We can use a Cron Job to check for token expiration in the background
- We can give the user the opportunity to get a new token once it has expired
We’ll defer the generation of a new token for a future article and assume that the user does indeed successfully verify their token here.
我们将把生成新令牌的问题推迟到以后的文章中讨论,并假设用户确实在这里成功验证了他们的令牌。
4. Adding Account Activation Checking to the Login Process
4.在登录过程中增加账户激活检查
We need to add the code that will check if the user is enabled:
我们需要添加代码,以检查用户是否被启用。
Let’s see this in Example 4.1. which shows the loadUserByUsername method of MyUserDetailsService.
让我们在例4.1中看看,它显示了MyUserDetailsService的loadUserByUsername方法。
Example 4.1.
示例4.1.
@Autowired
UserRepository userRepository;
public UserDetails loadUserByUsername(String email)
throws UsernameNotFoundException {
boolean enabled = true;
boolean accountNonExpired = true;
boolean credentialsNonExpired = true;
boolean accountNonLocked = true;
try {
User user = userRepository.findByEmail(email);
if (user == null) {
throw new UsernameNotFoundException(
"No user found with username: " + email);
}
return new org.springframework.security.core.userdetails.User(
user.getEmail(),
user.getPassword().toLowerCase(),
user.isEnabled(),
accountNonExpired,
credentialsNonExpired,
accountNonLocked,
getAuthorities(user.getRole()));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
As we can see, now MyUserDetailsService not uses the enabled flag of the user – and so it will only allow enabled the user to authenticate.
我们可以看到,现在MyUserDetailsService没有使用用户的enabled标志–因此它只允许启用用户进行认证。
Now, we will add an AuthenticationFailureHandler to customize the exception messages coming from MyUserDetailsService. Our CustomAuthenticationFailureHandler is shown in Example 4.2.:
现在,我们将添加一个AuthenticationFailureHandler来定制来自MyUserDetailsService的异常信息。我们的CustomAuthenticationFailureHandler显示在例4.2.:中。
Example 4.2. – CustomAuthenticationFailureHandler:
示例 4.2. – CustomAuthenticationFailureHandler:。
@Component
public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
@Autowired
private MessageSource messages;
@Autowired
private LocaleResolver localeResolver;
@Override
public void onAuthenticationFailure(HttpServletRequest request,
HttpServletResponse response, AuthenticationException exception)
throws IOException, ServletException {
setDefaultFailureUrl("/login.html?error=true");
super.onAuthenticationFailure(request, response, exception);
Locale locale = localeResolver.resolveLocale(request);
String errorMessage = messages.getMessage("message.badCredentials", null, locale);
if (exception.getMessage().equalsIgnoreCase("User is disabled")) {
errorMessage = messages.getMessage("auth.message.disabled", null, locale);
} else if (exception.getMessage().equalsIgnoreCase("User account has expired")) {
errorMessage = messages.getMessage("auth.message.expired", null, locale);
}
request.getSession().setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, errorMessage);
}
}
We will need to modify login.html to show the error messages.
我们需要修改login.html以显示错误信息。
Example 4.3. – Display error messages at login.html:
示例4.3。- 在login.html处显示错误信息:。
<div th:if="${param.error != null}"
th:text="${session[SPRING_SECURITY_LAST_EXCEPTION]}">error</div>
5. Adapting the Persistence Layer
5.适应持久层
Let’s now provide the actual implementation of some of these operations involving the verification token as well as the users.
现在让我们提供其中一些涉及验证令牌以及用户的操作的实际实现。
We’ll cover:
我们将涵盖。
- A new VerificationTokenRepository
- New methods in the IUserInterface and its implementation for new CRUD operations needed
Examples 5.1 – 5.3. show the new interfaces and implementation:
例5.1 – 5.3.显示了新的接口和实现。
Example 5.1. – The VerificationTokenRepository
示例 5.1. – VerificationTokenRepository。
public interface VerificationTokenRepository
extends JpaRepository<VerificationToken, Long> {
VerificationToken findByToken(String token);
VerificationToken findByUser(User user);
}
Example 5.2. – The IUserService Interface
示例 5.2. – IUserService 接口
public interface IUserService {
User registerNewUserAccount(UserDto userDto)
throws UserAlreadyExistException;
User getUser(String verificationToken);
void saveRegisteredUser(User user);
void createVerificationToken(User user, String token);
VerificationToken getVerificationToken(String VerificationToken);
}
Example 5.3. The UserService
示例5.3. 用户服务
@Service
@Transactional
public class UserService implements IUserService {
@Autowired
private UserRepository repository;
@Autowired
private VerificationTokenRepository tokenRepository;
@Override
public User registerNewUserAccount(UserDto userDto)
throws UserAlreadyExistException {
if (emailExist(userDto.getEmail())) {
throw new UserAlreadyExistException(
"There is an account with that email adress: "
+ userDto.getEmail());
}
User user = new User();
user.setFirstName(userDto.getFirstName());
user.setLastName(userDto.getLastName());
user.setPassword(userDto.getPassword());
user.setEmail(userDto.getEmail());
user.setRole(new Role(Integer.valueOf(1), user));
return repository.save(user);
}
private boolean emailExist(String email) {
return userRepository.findByEmail(email) != null;
}
@Override
public User getUser(String verificationToken) {
User user = tokenRepository.findByToken(verificationToken).getUser();
return user;
}
@Override
public VerificationToken getVerificationToken(String VerificationToken) {
return tokenRepository.findByToken(VerificationToken);
}
@Override
public void saveRegisteredUser(User user) {
repository.save(user);
}
@Override
public void createVerificationToken(User user, String token) {
VerificationToken myToken = new VerificationToken(token, user);
tokenRepository.save(myToken);
}
}
6. Conclusion
6.结论
In this article, we’ve expanded the registration process to include an email based account activation procedure.
在这篇文章中,我们扩大了注册程序,包括基于电子邮件的账户激活程序。
The account activation logic requires sending a verification token to the user via email so that they can send it back to the controller to verify their identity.
帐户激活逻辑需要通过电子邮件向用户发送一个验证令牌,以便他们将其发回给控制器以验证其身份。
The implementation of this Registration with Spring Security tutorial can be found in the GitHub project – this is an Eclipse based project, so it should be easy to import and run as it is.
在GitHub项目中可以找到该注册教程的实现 – 这是一个基于Eclipse的项目,因此应该很容易导入并按原样运行。