Update 2020/05/02: I removed useless csrf().disabled()
config and CustomAuthenticatorProvider
class. Also, I changed Optional
logic and used orElseThrow()
method. Thanks @gokcensedat and @utkuozdemir for their valuable comments.
Hello! In this article, I’ll explain how to register users to your application, how to send confirmation mail and handle user logins. My main resource will be official docs and you can access Github repository at the end of the article. I will not explain exception handling parts since our main subject is different.
- Requirements
- 1. Creating a project with Spring Initialzr
- 2. Create User entity
- 3. Create UserService
- 4. Create ConfirmationToken class
- 5. User registration process
- 6. Create confirmation methods
- 7. Spring Mail config
- 8. Create EmailService
- 9. Spring Security Configuration
- 10. Create controller and pages
- Conclusion
Requirements
You need to know Java and must have developed at least one Spring project to understand this article.
1. Creating a project with Spring Initialzr
Create a project with Spring Web, Lombok, Thymeleaf, Spring Security, Java Mail Sender, H2 and Spring Data JPA dependencies with Spring Initialzr. You can use your IDE’s plugin or https://start.spring.io/.
2. Create User entity
Create an entity
package and UserRole
enum inside the package. We will add ADMIN and USER values for now but you can add more according to your needs.
enum UserRole {
ADMIN, USER
}
Now, create a User
class. This class has to implement UserDetails
interface. This interface has basic user methods. You can see an implementation in org.springframework.security.core.userdetails.User
class. You can also extend this class and add your own fields. I will not extend this class because I want to show the whole process. So, let’s create a few fields, implement the required methods and fill them.
@Getter
@Setter
@Builder
@EqualsAndHashCode
@NoArgsConstructor`
@AllArgsConstructor
@Entity(name = "Users")
public class User implements UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String surname;
private String email;
private String password;
@Builder.Default
private UserRole userRole = UserRole.USER;
@Builder.Default
private Boolean locked = false;
@Builder.Default
private Boolean enabled = false;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
final SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(userRole.name());
return Collections.singletonList(simpleGrantedAuthority);
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return email;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return !locked;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return enabled;
}
}
We will not use expired
field for this example, so always return true
and we will also use email
instead of username
. So return email
from getUsername()
method. You can deduce others from the names.
3. Create UserService
Create a user
package and UserService
class inside the package. Then, implement UserDetailsService
which requires you to implement loadByUsername()
. But here’s important, we will use email
instead of username
. So, this method will take email as parameter and return the user. To achieve this, we need to interact with database, so we need a repository. Create a UserRepository
interface inside the same package and add findByEmail()
method.
interface UserRepository extends CrudRepository<User, Long> {
Optional<User> findByEmail(String email);
}
Now, turn back to UserService
, add UserRepository
as a field and complete the method. UserService
will be as below.
@Service
@AllArgsConstructor
public class UserService implements UserDetailsService {
private final UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
final Optional<User> optionalUser = userRepository.findByEmail(email);
if (optionalUser.isPresent()) {
return optionalUser.get();
}
else {
throw new UsernameNotFoundException(MessageFormat.format("User with email {0} cannot be found.", email));
}
}
}
4. Create ConfirmationToken class
We will create a token and a unique link for every user registration and send them with email. Create a ConfirmationToken
class inside user
package and add these fields:
@Entity
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
class ConfirmationToken {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String confirmationToken;
private LocalDate createdDate;
@OneToOne(targetEntity = User.class, fetch = FetchType.EAGER)
@JoinColumn(nullable = false, name = "user_id")
private User user;
ConfirmationToken(User user) {
this.user = user;
this.createdDate = LocalDate.now();
this.confirmationToken = UUID.randomUUID().toString();
}
}
This is a simple entity as you see. Since every user will have a single token, we create a OneToOne relation. createdDate
field will be used to check if the token is expired or not.
5. User registration process
I want you to experience a real-like development process instead of writing all the methods and classes. So let’s think of the process step by step.
- Take user information with the registration form.
- Encode the password with
BCryptPasswordEncoder
and create the user withenabled=false
value as we set default inUser
class. - Create a
ConfirmationToken
and assign this token to the user. - Create a unique url with this token and send it via email.
- When the user clicks the link change
enabled
field totrue
for that user. - Delete the token.
So, we will create ConfirmationTokenRepository
and ConfirmationTokenService
before signUpUser()
method. Because we need this method in signing-up process.
@Repository
interface ConfirmationTokenRepository extends CrudRepository<ConfirmationToken, Long> { }
@Service
@AllArgsConstructor
class ConfirmationTokenService {
private final ConfirmationTokenRepository confirmationTokenRepository;
void saveConfirmationToken(ConfirmationToken confirmationToken) {
confirmationTokenRepository.save(confirmationToken);
}
}
Now, go to UserService
class and create signUpUser()
method.
void signUpUser(User user) {
final String encryptedPassword = bCryptPasswordEncoder.encode(user.getPassword());
user.setPassword(encryptedPassword);
final User createdUser = userRepository.save(user);
final ConfirmationToken confirmationToken = new ConfirmationToken(user);
confirmationTokenService.saveConfirmationToken(confirmationToken);
}
6. Create confirmation methods
We have user registration process. Now, we need confirmation process. First of all, create the method that will delete token after confirmation.
void deleteConfirmationToken(Long id){
confirmationTokenRepository.deleteById(id);
}
Then, create confirmUser()
method.
void confirmUser(ConfirmationToken confirmationToken) {
final User user = confirmationToken.getUser();
user.setEnabled(true);
userRepository.save(user);
confirmationTokenService.deleteConfirmationToken(confirmationToken.getId());
}
7. Spring Mail config
I use application.yml instead of application.properties. (see this) Here’s the Spring Mail config for Gmail. If you use 2-factor, you have to create a password for application. (see this)
spring:
mail:
host: smtp.gmail.com
port: 587
username: <MAIL_ADDRESS>
password: <PASSWORD>
properties:
mail:
smtp:
auth: true
starttls:
enable: true
connectiontimeout: 5000
timeout: 3000
writetimeout: 5000
8. Create EmailService
Create an EmailService
class inside user package and create sendEmail()
method inside it.
@Service
@AllArgsConstructor
public class EmailSenderService {
private JavaMailSender javaMailSender;
@Async
public void sendEmail(SimpleMailMessage email) {
javaMailSender.send(email);
}
}
Now, create the sendConfirmationMail()
inside UserService.
void sendConfirmationMail(String userMail, String token) {
final SimpleMailMessage mailMessage = new SimpleMailMessage();
mailMessage.setTo(userMail);
mailMessage.setSubject("Mail Confirmation Link!");
mailMessage.setFrom("<MAIL>");
mailMessage.setText(
"Thank you for registering. Please click on the below link to activate your account." + "http://localhost:8080/sign-up/confirm?token="
+ token);
emailSenderService.sendEmail(mailMessage);
}
9. Spring Security Configuration
We created the whole process related to Spring Security. Now we will set configuration for all. Create WebSecurityConfig
and WebConfig
inside config package.
@Configuration
@AllArgsConstructor
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private final UserService userService;
private final BCryptPasswordEncoder bCryptPasswordEncoder;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/sign-up/**", "/sign-in/**")
.permitAll()
.anyRequest()
.authenticated()
.and()
.formLogin()
.loginPage("/sign-in")
.permitAll();
}
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService)
.passwordEncoder(bCryptPasswordEncoder);
}
}
@Configuration
public class WebConfig {
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
These configuration classes simply set sign-in and sign-up pages, set encoder and authentication provider. You can learn them with this great article.
10. Create controller and pages
We have all we need for an application except HTML pages. We will create those and make them accessible with controller.
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="en">
<head>
<title>Spring Boot User Authentication</title>
</head>
<body>
<form role="form" th:action="@{/sign-in}" th:method="post">
<label>Username</label>
<input type="text" id="username" name="username">
<label>Password</label>
<input type="password" id="password" name="password">
<input type="submit">
</form>
</body>
</html>
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="en">
<head>
<title>Spring Boot User Authentication</title>
</head>
<body>
<form role="form" th:action="@{/sign-up}" th:method="post" th:object="${user}">
<label>Name</label>
<input type="text" id="name" name="name" th:field="*{name}">
<label>Surname</label>
<input type="text" id="surname" name="surname" th:field="*{surname}">
<label>Email</label>
<input type="text" id="email" name="email" th:field="*{email}">
<label>Password</label>
<input type="password" id="password" name="password" th:field="*{password}">
<input type="submit">
</form>
</body>
</html>
@Controller
@AllArgsConstructor
public class UserController {
private final UserService userService;
private final ConfirmationTokenService confirmationTokenService;
@GetMapping("/sign-in")
String signIn() {
return "sign-in";
}
@GetMapping("/sign-up")
String signUp() {
return "sign-up";
}
@PostMapping("/sign-up")
String signUp(User user) {
userService.signUpUser(user);
return "redirect:/sign-in";
}
@GetMapping("/confirm")
String confirmMail(@RequestParam("token") String token) {
Optional<ConfirmationToken> optionalConfirmationToken = confirmationTokenService.findConfirmationTokenByToken(token);
optionalConfirmationToken.ifPresent(userService::confirmUser);
return "/sign-in";
}
}
Conclusion
If you have followed the article properly, you can go to http://localhost:8080/sign-up to sign up. But do not forget, it is not a production ready application since we didn’t create validations(including token expiration) and provide proper exception handling. If you want to learn about validations you can see https://medium.com/@kamer.dev/validations-in-spring-boot-4f15598c3d6a.
Github Repository: https://github.com/kamer/spring-boot-user-registration