In this article we are going to build complete Registration/Login System using Spring Boot and Spring Security.
Technology used :-
=> Spring Boot
=> Spring Security
=> Java Mail
=> Email verification with expiry
1. Create Project using Spring Initializer
Follow this link and you will be redirected to the spring initializer with all the dependencies requires for this project.
Download this project and you will get the zip file. unzip it and open with intelliJ Idea.
2. Create appuser package
Inside this package we will configure all the user details in different classes and Interfaces.
- Create a AppUser class and define all the properties of the user like firstName, lastName, email, password, etc.
- Create a AppUserRepository interface and an inherit the JpaRepository class to do all the database operations on the users.
@Transactional(readOnly = true)
public interface AppUserRepository extends JpaRepository<AppUser, Long> {
Optional<AppUser> findByEmail(String email);
@Transactional
@Modifying
@Query("UPDATE AppUser a " +
"SET a.enabled = TRUE WHERE a.email = ?1")
int enableAppUser(String email);
}
- Create a AppUserRole enum to define the role of the user.
public enum AppUserRole {
USER,
ADMIN
}
- Create a AppUserService class to define the service in terms of how user will register and share the data to login.
@Service
@AllArgsConstructor
public class AppUserService implements UserDetailsService {
private static final String USER_NOT_FOUND = "user with email %s not found!";
private final AppUserRepository appUserRepository;
private final BCryptPasswordEncoder bCryptPasswordEncoder;
private final ConfirmationTokenService confirmationTokenService;
@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
return appUserRepository.findByEmail(email)
.orElseThrow(() -> new UsernameNotFoundException(String.format(USER_NOT_FOUND,email)));
}
public String signUpUser(AppUser appUser){
boolean userExists = appUserRepository
.findByEmail(appUser.getEmail())
.isPresent();
if (userExists){
// TODO: CHECK OF ATTRIBUTES ARE THE SAME AND
//TODO: IF EMAIL NOT CONFIRMED SENDD CONFIRMATION MAIL
throw new IllegalStateException("email already taken!");
}
String encodedPassword = bCryptPasswordEncoder.encode(appUser.getPassword());
appUser.setPassword(encodedPassword);
appUserRepository.save(appUser);
String token = UUID.randomUUID().toString();
//TODO: Send confirmation token
ConfirmationToken confirmationToken = new ConfirmationToken(
token,
LocalDateTime.now(),
LocalDateTime.now().plusMinutes(15),
appUser
);
confirmationTokenService.saveConfirmationToken(confirmationToken);
//TODO Send Email
return token;
}
public int enableAppUser(String email) {
return appUserRepository.enableAppUser(email);
}
}
3. Create registration package
Inside this package we will configure all the registration details in different classes and Interfaces that will register a user we have in appuser package.
- Create a UserRegistration Class to map a registration endpoint
@RequestMapping(path = "api/v1/registration")
@AllArgsConstructor
public class UserRegistration {
private final RegistrationService registrationService;
@PostMapping
public String register(@RequestBody RegistrationRequest request){
return registrationService.register(request);
}
@GetMapping(path = "confirm")
public String confirm(@RequestParam("token") String token) {
return registrationService.confirmToken(token);
}
}
- Create a RegistrationRequest class to request all the data from the user.
@AllArgsConstructor
@EqualsAndHashCode
@ToString
public class RegistrationRequest {
private final String firstName;
private final String lastName;
private final String email;
private final String password;
}
- Create a EmailValidator class that will validate the user's email
@Service
@AllArgsConstructor
public class EmailValidator implements Predicate<String> {
@Override
public boolean test(String s) {
return false;
}
}
- Create a RegistrationService class to map the relation between the user and registration endpoint.
@AllArgsConstructor
public class RegistrationService {
private final AppUserService appUserService;
private final EmailValidator emailValidator;
private final EmailSender emailSender;
private final ConfirmationTokenService confirmationTokenService;
public String register(RegistrationRequest request) {
boolean isValidEmail = emailValidator.test(request.getEmail());
if(isValidEmail){
throw new IllegalStateException("Email is not valid!");
}
String token = appUserService.signUpUser(
new AppUser(
request.getFirstName(),
request.getLastName(),
request.getEmail(),
request.getPassword(),
AppUserRole.USER
)
);
String link = "http://localhost:8080/api/v1/registration/confirm/?token=" + token;
emailSender.send(
request.getEmail(),
buildEmail(request.getFirstName(), link) );
return token;
}
@Transactional
public String confirmToken(String token) {
ConfirmationToken confirmationToken = confirmationTokenService
.getToken(token)
.orElseThrow(() ->
new IllegalStateException("token not found"));
if (confirmationToken.getConfirmedAt() != null) {
throw new IllegalStateException("email already confirmed");
}
LocalDateTime expiredAt = confirmationToken.getExpiredAt();
if (expiredAt.isBefore(LocalDateTime.now())) {
throw new IllegalStateException("token expired");
}
confirmationTokenService.setConfirmedAt(token);
appUserService.enableAppUser(
confirmationToken.getAppUser().getEmail());
return "confirmed";
}
private String buildEmail(String name, String link) {
return "<div style=\"font-family:Helvetica,Arial,sans-serif;font-size:16px;margin:0;color:#0b0c0c\">\n" +
"\n" +
"<span style=\"display:none;font-size:1px;color:#fff;max-height:0\"></span>\n" +
"\n" +
" <table role=\"presentation\" width=\"100%\" style=\"border-collapse:collapse;min-width:100%;width:100%!important\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\">\n" +
" <tbody><tr>\n" +
" <td width=\"100%\" height=\"53\" bgcolor=\"#0b0c0c\">\n" +
" \n" +
" <table role=\"presentation\" width=\"100%\" style=\"border-collapse:collapse;max-width:580px\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\" align=\"center\">\n" +
" <tbody><tr>\n" +
" <td width=\"70\" bgcolor=\"#0b0c0c\" valign=\"middle\">\n" +
" <table role=\"presentation\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\" style=\"border-collapse:collapse\">\n" +
" <tbody><tr>\n" +
" <td style=\"padding-left:10px\">\n" +
" \n" +
" </td>\n" +
" <td style=\"font-size:28px;line-height:1.315789474;Margin-top:4px;padding-left:10px\">\n" +
" <span style=\"font-family:Helvetica,Arial,sans-serif;font-weight:700;color:#ffffff;text-decoration:none;vertical-align:top;display:inline-block\">Confirm your email</span>\n" +
" </td>\n" +
" </tr>\n" +
" </tbody></table>\n" +
" </a>\n" +
" </td>\n" +
" </tr>\n" +
" </tbody></table>\n" +
" \n" +
" </td>\n" +
" </tr>\n" +
" </tbody></table>\n" +
" <table role=\"presentation\" class=\"m_-6186904992287805515content\" align=\"center\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\" style=\"border-collapse:collapse;max-width:580px;width:100%!important\" width=\"100%\">\n" +
" <tbody><tr>\n" +
" <td width=\"10\" height=\"10\" valign=\"middle\"></td>\n" +
" <td>\n" +
" \n" +
" <table role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\" style=\"border-collapse:collapse\">\n" +
" <tbody><tr>\n" +
" <td bgcolor=\"#1D70B8\" width=\"100%\" height=\"10\"></td>\n" +
" </tr>\n" +
" </tbody></table>\n" +
" \n" +
" </td>\n" +
" <td width=\"10\" valign=\"middle\" height=\"10\"></td>\n" +
" </tr>\n" +
" </tbody></table>\n" +
"\n" +
"\n" +
"\n" +
" <table role=\"presentation\" class=\"m_-6186904992287805515content\" align=\"center\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\" style=\"border-collapse:collapse;max-width:580px;width:100%!important\" width=\"100%\">\n" +
" <tbody><tr>\n" +
" <td height=\"30\"><br></td>\n" +
" </tr>\n" +
" <tr>\n" +
" <td width=\"10\" valign=\"middle\"><br></td>\n" +
" <td style=\"font-family:Helvetica,Arial,sans-serif;font-size:19px;line-height:1.315789474;max-width:560px\">\n" +
" \n" +
" <p style=\"Margin:0 0 20px 0;font-size:19px;line-height:25px;color:#0b0c0c\">Hi " + name + ",</p><p style=\"Margin:0 0 20px 0;font-size:19px;line-height:25px;color:#0b0c0c\"> Thank you for registering. Please click on the below link to activate your account: </p><blockquote style=\"Margin:0 0 20px 0;border-left:10px solid #b1b4b6;padding:15px 0 0.1px 15px;font-size:19px;line-height:25px\"><p style=\"Margin:0 0 20px 0;font-size:19px;line-height:25px;color:#0b0c0c\"> <a href=\"" + link + "\">Activate Now</a> </p></blockquote>\n Link will expire in 15 minutes. <p>See you soon</p>" +
" \n" +
" </td>\n" +
" <td width=\"10\" valign=\"middle\"><br></td>\n" +
" </tr>\n" +
" <tr>\n" +
" <td height=\"30\"><br></td>\n" +
" </tr>\n" +
" </tbody></table><div class=\"yj6qo\"></div><div class=\"adL\">\n" +
"\n" +
"</div></div>";
}
}
- Now create one more package inside the registration package in which we will configure the token from the user for the successful registration.
- And then inside this token package, we will create a ConfirmationToken class to create the token for the user.
@Setter
@NoArgsConstructor
@Entity
public class ConfirmationToken {
@SequenceGenerator(
name = "confirmation_token_sequence",
sequenceName = "confirmation_token_sequence",
allocationSize = 1
)
@Id
@GeneratedValue(
strategy = GenerationType.SEQUENCE,
generator = "confirmation_token_sequence"
)
private Long id;
@Column(nullable = false)
private String token;
@Column(nullable = false)
private LocalDateTime createdAt;
@Column(nullable = false)
private LocalDateTime expiredAt;
private LocalDateTime confirmedAt;
@ManyToOne
@JoinColumn(
nullable = false,
name= "app_user_id"
)
private AppUser appUser;
public ConfirmationToken(String token,
LocalDateTime createdAt,
LocalDateTime expiredAt,
AppUser appUser) {
this.token = token;
this.createdAt = createdAt;
this.expiredAt = expiredAt;
this.appUser =appUser;
}
}
- Run the project and then you can go to postman and hit this url - localhost:8080/api/v1/registration, you will get the access token
- Create an interface ConfirmationTokenRepository and extends the JpaRepository to do all the database operation like to save the token to specific user in databse.
interface ConfirmationTokenRepository extends
JpaRepository<ConfirmationToken,Long> {
Optional<ConfirmationToken> findByToken(String token);
@Transactional
@Modifying
@Query("UPDATE ConfirmationToken c " +
"SET c.confirmedAt = ?2 " +
"WHERE c.token = ?1")
int updateConfirmedAt(String token,
LocalDateTime confirmedAt);
}
- Create a ConfirmationTokenService class to save this token to the specific users for 15 mins so that he can activate their account within 15 min and will be able to login.
- we have set the token expiry time to 15 mins for the security purpose, we can increase or decrease the expiration time of token according to our purpose.
@AllArgsConstructor
public class ConfirmationTokenService {
private final ConfirmationTokenRepository confirmationTokenRepository;
public void saveConfirmationToken(ConfirmationToken token){
confirmationTokenRepository.save(token);
}
public Optional<ConfirmationToken> getToken(String token) {
return confirmationTokenRepository.findByToken(token);
}
public int setConfirmedAt(String token) {
return confirmationTokenRepository.updateConfirmedAt(
token, LocalDateTime.now());
}
}
Now you can hit the access token url localhost:8080/api/v1/registration/confirm/?token=17de5cc5-2f43-48e3-acd2-c47f26bba311
And access token would be the same which we have created while hitting the registration url
4. Create an email package in which we will configure the property of email sending.
- Create an interface EmailSender, where we will define the send method that will take t2 String parameters "to" and "email"
```public interface EmailSender { void send(String to, String email); }
- Create a class EmailService where we will implement this send method and the property to send an email to a particular user's email who will try to register.
```@Service
@AllArgsConstructor
public class EmailService implements EmailSender{
private final static Logger LOGGER = LoggerFactory.getLogger(EmailService.class);
private final JavaMailSender javaMailSender;
@Override
@Async
public void send(String to, String email) {
try {
MimeMessage mimeMessage = javaMailSender.createMimeMessage();
MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(mimeMessage,"utf-8");
mimeMessageHelper.setText(email,true);
mimeMessageHelper.setTo(to);
mimeMessageHelper.setSubject("Confirm your email");
mimeMessageHelper.setFrom("info@hussaincode.in");
javaMailSender.send(mimeMessage);
}catch (MessagingException e){
LOGGER.error("failed to send email",e);
throw new IllegalStateException("failed to send email");
}
}
}
5. For sending a mail we have used the MailDev Service.
MailDev is a free service which we can use to send an email to user for the confirmation of registration.
You can install the MailDev using below commands.
To install MailDev - $ npm install -g maildev
To run MailDev - $ maildev
- Once you have installed it you will get a url like to access the email service.
MailDev webapp running at http://0.0.0.0:1080
MailDev SMTP Server running at 0.0.0.0:1025
- Once you get this then go to localhost/1080 to access the MailDev.
This is complete Registration/Login Project steps and code. If you have any doubt leave a comment and i will reply.
Get the complete code base of this project on my GitHub