www.baeldung.com/registratio…
Eugen Paraschiv
From stackGC
1, an overview of the
This article continues the missing part of the registration process in the previous Spring Security series – validating a user’s email to confirm an account.
The registration confirmation mechanism forces the user to perform the actions in the confirmation registration email to verify their email address and activate their account after successful registration. The user completes the activation by clicking the unique activation link in the E-mail.
According to this logic, newly registered users cannot log in to the system until the process is completed.
2. Verify the Token
We will use a simple authentication token as the credential to authenticate the user.
2.1 VerificationToken Entity
VerificationToken entities must meet the following criteria:
- It must point to User (through a one-way relationship)
- It will be created immediately after registration
- It will expire within 24 hours of creation
- There is a unique, randomly generated value
Points 2 and 3 are part of the registration logic. The remaining two implementations are in simple VerificationToken entities, as in example 2.1.
Example 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());
}
// Omit the constructor, getter, and setter
}
Copy the code
Note that nullable = false on User ensures data integrity and consistency in the VerificationToken <-> User association.
2.2. Add the Enabled field to User
This Enabled field will be set to false when the user registers. During account authentication, it is set to true if it passes.
Add a field to the User entity:
public class User {...@Column(name = "enabled")
private boolean enabled;
public User(a) {
super(a);this.enabled=false; }... }Copy the code
Note that we also set the default value of this field to false.
3. During account registration
Add two additional business logic to the user registration use case:
- Generate and save a VerificationToken for the User
- Send an email for account confirmation — which contains a confirmation link with a VerificationToken value
3.1. Use Spring Event to create a token and send an authentication email
These two additional logic should not be performed directly by the controller because they are parallel background tasks.
The controller will issue a Spring ApplicationEvent to trigger the execution of these tasks. This and injection ApplicationEventPublisher and use it to distribute as simple registration.
Example 3.1 shows this simple logic:
Example 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);
}
Copy the code
Another thing to note is the try catch block that wraps events. This code represents displaying an error page whenever there is an exception in the logic executed after the publishing event. The logic here is to send an email.
3.2 events and Listeners
Now let’s look at OnRegistrationCompleteEvent actual implementation, as well as to deal with its listeners:
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
}
Copy the code
3.2.2 – OnRegistrationCompleteEvent RegistrationListener processing
@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); }}Copy the code
Here, will receive OnRegistrationCompleteEvent confirmRegistration method, extract all the necessary User information, create the authentication token, save it, then upon confirmation of registration link will be sent as a parameter.
As mentioned above, any javax.mail JavaMailSender trigger. Mail. AuthenticationFailedException will be handled by controller.
3.3. Handle the validation token parameters
Click on the confirmation link when the user receives it.
Once clicked, the controller will extract the value of the token parameter in the GET request and will use it to enable User.
Example 3.3.1 – RegistrationController handles registration confirmation
@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();
}
Copy the code
The user will be redirected to the error page and displayed with the appropriate message if:
- The VerificationToken does not exist for some reason
- VerificationToken expired
See the error page in example 3.3.2.
In 3.3.2 rainfall distribution on 10-12 – 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>
Copy the code
If no errors are found, the user is enabled.
There are two areas that can be improved in the process of handling VerificationToken checking and expiration:
- We can use the Cron job to check in the background if the token is expired
- Once expired, we can give the user the opportunity to get a new token
We’ll defer the process of generating a new token to a later article, and now assume that the user did successfully validate the token here.
4. Add account activation checks to the login process
We need to add code to check whether the user is enabled:
Example 4.1 shows the loadUserByUsername method of MyUserDetailsService.
Example 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 newRuntimeException(e); }}Copy the code
As you can see, MyUserDetailsService now does not use the Enabled flag of User.
Now add a AuthenticationFailureHandler from definition from MyUserDetailsService exception message. Our CustomAuthenticationFailureHandler as shown in example 4.2:
Example – CustomAuthenticationFailureHandler 4.2:
@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); }}Copy the code
Login.html needs to be modified to display error messages.
Example 4.3 – Displaying error messages at login.html:
<div th:if="${param.error ! = null}"
th:text="${session[SPRING_SECURITY_LAST_EXCEPTION]}">error</div>
Copy the code
5, adapt persistent layer
Let’s look at some actual implementations involving validation tokens and user actions.
Covers the following:
- A new VerificationTokenRepository
- New methods in IUserInterface and their implementation requirements for new CRUD operations
Examples 5.1-5.3 show the new interface and implementation:
5.1 – VerificationTokenRepository example
public interface VerificationTokenRepository
extends JpaRepository<VerificationToken.Long> {
VerificationToken findByToken(String token);
VerificationToken findByUser(User user);
}
Copy the code
Example 5.2 – IUserService interface
public interface IUserService {
User registerNewUserAccount(UserDto accountDto)
throws EmailExistsException;
User getUser(String verificationToken);
void saveRegisteredUser(User user);
void createVerificationToken(User user, String token);
VerificationToken getVerificationToken(String VerificationToken);
}
Copy the code
Example 5.3 – UserService
@Service
@Transactional
public class UserService implements IUserService {
@Autowired
private UserRepository repository;
@Autowired
private VerificationTokenRepository tokenRepository;
@Override
public User registerNewUserAccount(UserDto accountDto)
throws EmailExistsException {
if (emailExist(accountDto.getEmail())) {
throw new EmailExistsException(
"There is an account with that email adress: "
+ accountDto.getEmail());
}
User user = new User();
user.setFirstName(accountDto.getFirstName());
user.setLastName(accountDto.getLastName());
user.setPassword(accountDto.getPassword());
user.setEmail(accountDto.getEmail());
user.setRole(new Role(Integer.valueOf(1), user));
return repository.save(user);
}
private boolean emailExist(String email) {
User user = repository.findByEmail(email);
if(user ! =null) {
return true;
}
return false;
}
@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 = newVerificationToken(token, user); tokenRepository.save(myToken); }}Copy the code
6, summary
This article introduced the registration process — the email-based account activation process.
The account activation logic is to send authentication tokens to users via email so that they can send information back to the controller for authentication.
The registration sample and implementation of the Spring Security tutorial can be found in the GitHub project.
Original project source code
- Github.com/eugenp/spri…