Security Example in Spring Boot

Implementation of Spring Security in Spring Boot application is the key point to learn for spring boot developers. Because Authentication and Authorization is the backbone of the whole application.

Getting started with the Spring Security Series, this is the first part, in this article we are going to focus on the authentication part with minimal registration. For the implementation of registration flow with email verification, customizing password encoding, setting up password strengths and rules will be explored in another separate article for each. 

This article will be the base of the spring security series, the other security features will be explained on the basis of this implementation, so be focused and let's understand. The code contains proper naming & brief comments that makes it very comprehensive. If you feel any difficulty or find any issue please let us know by our contact us page

The main goal of this article is to implement optional login means login either with email or mobile number and password.

 

 

1. Initialize the project with dependencies listed below:

  1. Spring Web
  2. Spring Security
  3. Thymeleaf
  4. Thymeleaf Extra Security5
  5. Spring Data JPA
  6. Spring Boot Dev Tools
  7. Lombok
  8. MySql

You can use any built tool. To know what to choose go to   Maven or Gradle

2. Now create a User model

Create an Entity class User.java in the models' package. You may aware of the annotations used in creating entity class. If not then we have explained below the code.

@Getter
@Setter
@NoArgsConstructor
@Entity
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    Long id;
    @Column(unique = true)
    String uniqueId = UUID.randomUUID().toString();
    String name;
    @Column(unique = true)
    String email;
    @Column(unique = true)
    String mobile;
    String password;
    String role;
    String profilePic;
    LocalDateTime createdOn;
    LocalDateTime updatedOn;

    @PrePersist
    public void creationSpot(){
        this.createdOn=LocalDateTime.now();
        this.updatedOn=LocalDateTime.now();
    }
    @PreUpdate
    public void updationSpot(){
        this.updatedOn=LocalDateTime.now();
    }

}

Annotations used in the above code are self-explanatory but keeping in mind for beginners we would like to describe as follows:

  1. @Getter, @Setter, and @NoArgsConstructor are the annotations of Lombok dev tools used to generate getters, setters, and a constructor with no argument respectively. 
  2. Other annotations are defined well in this article Annotations related to Data Persistency 

3. Creating a repository  for performing database operations for the entity User

For working with JPA we need to create an interface and extend the JpaRepository by telling the entity name and Id type as we have defined in the entity. To do this, create UserRepository.java and define two methods to fetch user details from the database by email id and mobile number, see below code.

public interface UserRepository extends JpaRepository<User,Long> {
    Optional<User> findByEmail(String email);
    Optional<User> findByMobile(String mobile);
//..other methods
}

4. Implementing the UserDetails for authentication

     For authentication, we need to implement the UserDetails interface of the spring security core. Its data will be written in login principal after successful login. We can implement it in the User entity also, but to keep things organized well let's make it separately.

@Getter
@Setter
public class CustomUserPrincipal implements UserDetails, Serializable {

    private String  uniqueId;
    private String name;
    private String email;
    private String mobile;
    private String profilePic;
    @JsonIgnore
    private String username;
    @JsonIgnore
    private String password;
    @JsonIgnore
    private Collection<? extends GrantedAuthority> authorities;

   public CustomUserPrincipal(String uniqueId, String loggedInName, String profilePic, String username, String email, String mobile, String password, 
                                                          Collection<? extends GrantedAuthority> authorities) {
        this.uniqueId = uniqueId;
        this.name =loggedInName;
        this.email=email;
        this.mobile=mobile;
        this.profilePic=profilePic;
        this.username = username;
        this.password = password;
        this.authorities = authorities;
    }

    public static CustomUserPrincipal createWithEmail(User user) {
        List<GrantedAuthority> authorities = new ArrayList<>();
        authorities.add(new SimpleGrantedAuthority(user.getRole().toString()));
        return new CustomUserPrincipal(
                user.getUniqueId(),
                user.getName(),
                user.getProfilePic(),
                user.getEmail(),
                user.getEmail(),
                user.getMobile(),
                user.getPassword(),
                authorities
        );
    }
    public static CustomUserPrincipal createWithMobile(User user) {
        List<GrantedAuthority> authorities = new ArrayList<>();
        authorities.add(new SimpleGrantedAuthority(user.getRole().toString()));
        return new CustomUserPrincipal(
                user.getUniqueId(),
                user.getName(),
                user.getProfilePic(),
                user.getMobile(),
                user.getEmail(),
                user.getMobile(),
                user.getPassword(),
                authorities
          );
    }
    @Override
    public String getUsername() {  return username; }

    @Override
    public String getPassword() { return password; }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() { return authorities; }

    @Override
    public boolean isAccountNonExpired() { return true; }

    @Override
    public boolean isAccountNonLocked() { return true; }

    @Override
    public boolean isCredentialsNonExpired() {  return true; }

    @Override
    public boolean isEnabled() {  return true; }
}

5. Implement the UserDetailsService for handling UserDetails

This is required to implement because the spring security calls method loadUserByUsername(username) that are specified in the UserDetailsService interface. 

And also we define a method that authenticates users first time just after registration by using the UsernameAuthenticationToken class.

@Service
@Log
public class SecurityService implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;
    @Autowired
    private AuthenticationManager authenticationManager;

    @Override
    @Transactional
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // Let people login with either email or mobile
        CustomUserPrincipal userPrincipal = null;
        Optional<User> user = null;

        /* check user if user enters email as username*/
        user = userRepository.findByEmail(username);
        if (user.isPresent())
            userPrincipal = CustomUserPrincipal.createWithEmail(user.get());
        else {
            /* check user if user not present with email, username may be mobile no.*/
            user = userRepository.findByMobile(username);
            if (user.isPresent())
                userPrincipal = CustomUserPrincipal.createWithMobile(user.get());
        }
        /* check that user is present with email or mobile */
        if (userPrincipal == null)
            throw new UsernameNotFoundException("User not found with username: " + username);
        else
            return userPrincipal;
    }

/* Username Password Authentication method to allow user to log in just after registration*/
    public void loginFirstTime(User user,String plainPassowrd) {
        log.info("Auto login .....!");
        UserDetails userDetails = loadUserByUsername(user.getEmail());
        UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(userDetails, plainPassowrd, userDetails.getAuthorities());
        authenticationManager.authenticate(usernamePasswordAuthenticationToken);
        if (usernamePasswordAuthenticationToken.isAuthenticated()) {
            SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
            log.info(String.format("Auto login %s successfully!", user.getEmail()));
        } else System.err.println("auto login failed");
    }
}

6. Define the security configurations by extending WebSecurityConfigurerAdapter

Here we are defining the security rules of the application

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(
        securedEnabled = true,
        jsr250Enabled = true,
        prePostEnabled = true
)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private SecurityService customSecurityService;

    /* this will handle the login form, will call the loadUserByUsername method to get user details
       then match password */
    @Override
    public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
        authenticationManagerBuilder.userDetailsService(customSecurityService).passwordEncoder(passwordEncoder());
    }

    @Override
    public void configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity
                .authorizeRequests()
                /* permitting these pages to be opened without login */
                .antMatchers("/","/login","/register","/assets/**").permitAll()
                /* assets will contain css, js and fonts, etc */
                /* other pages having any url pattern will require login */
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/login")
                /* always redirect to profile page after successful login  */
                .defaultSuccessUrl("/profile", true)
                .and()
                .logout()
                .permitAll();
        httpSecurity.csrf().disable();
        httpSecurity.headers().frameOptions().disable();
    }

/* these beans will be created for autowiring in components/services */
    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

7. Create user service to handle user operations

Inside the user service, we will create methods for registration, get security principal, and user details of the logged-in user, and update the method to change security principal details without logout.

@Service
public class UserService {
    @Autowired
    private SecurityService securityService;
    @Autowired
    private BCryptPasswordEncoder passwordEncoder;

    @Autowired
    private UserRepository userRepository;

    public ModelAndView registerUser(User user) {
        ModelAndView mv = new ModelAndView("profile");
        Optional<User> checkUser = userRepository.findByEmail(user.getEmail());
        if(!checkUser.isPresent()){
            checkUser = userRepository.findByMobile(user.getMobile());
            if(!checkUser.isPresent()) {
                String plainPassowrd = user.getPassword();
                user.setPassword(passwordEncoder.encode(plainPassowrd));
                //setting the default role as string
                user.setRole("ROLE_USER");
                /* role based authorization will be demonstrated in part 2 of this series
                 keep visiting easytutorials.live */
                user = userRepository.save(user);

                /* auto login first time */
                securityService.loginFirstTime(user,plainPassowrd);
            }else{
                mv.setViewName("register");
                mv.addObject("error","A user is already present with this mobile number");
            }
        }else{
            mv.setViewName("register");
            mv.addObject("error","User already registered with this email");
        }
        return mv;
    }


/* This method will use to change the authentication principal details without logout */
    public void updateAuthenticationDetails(User updatedUser) {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        CustomUserPrincipal user = (CustomUserPrincipal) auth.getPrincipal();
        user.setName(updatedUser.getName());
        user.setProfilePic(updatedUser.getProfilePic());
        user.setEmail(updatedUser.getEmail());
        user.setMobile(updatedUser.getMobile());
        Authentication newAuth = new UsernamePasswordAuthenticationToken(user, auth.getCredentials(), auth.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(newAuth);
    }

    /* if you need something with logged in user principal details */
    public CustomUserPrincipal getLoggedInUserPrincipal(){
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        CustomUserPrincipal user = (CustomUserPrincipal) auth.getPrincipal();
        return user;
    }

    /* if you need all details of logged in user */
    public User getLoggedInUser(){
        return userRepository.findByEmail(getLoggedInUserPrincipal().getEmail()).get();
    }
}

8. Create a controller to control authentication

@Controller
public class AuthController {

    @Autowired
    private UserService userService;

    @GetMapping(value = {"/", "/login"})
    public String login(Model model, String error, String logout, Principal principal) {
        if (principal != null)
            return "redirect:/profile";
        if (error != null)
            model.addAttribute("error", error);

        if (logout != null)
            model.addAttribute("message", "You have been logged out successfully.");

        return "login";
    }

    @GetMapping("/register")
    public String register(Model model, Principal principal) {
        if (principal != null) {
            return "redirect:/profile";
        }
        return "register";
    }

    @PostMapping("/register")
    public ModelAndView register(User user) {
        ModelAndView mv = userService.registerUser(user);
        return mv;
    }

    /* to check the authentication principal details */
    @GetMapping("/principal")
    @ResponseBody
    public Principal principal(Principal principal) {
        return principal;
    }

    /* you can only open this page if user is authenticated, why? see SecurityConfig.java */
    @GetMapping("/profile")
    public String profile(Model model) {
        /* simply open the profile page
         *  and display the name, email, mobile number and profile picture
         *  through authentication principal, how? see profile.html */
        return "/profile";
    }
}

9. Application properties

spring.datasource.url=jdbc:mysql://localhost:3306/springsecurity?createDatabaseIfNotExist=true
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
server.port=8888

10. Create a registration page

we are showing you here the minimum required lines 

<!-- minimal design of registration form, you can design yourself or visit github link provided below -->
<form action="/register" method="post">
  <input type="text" placeholder="Your Name" name="name" required>
  <input type="email" placeholder="Your Email" name="email"   required>
  <input type="text" placeholder="Mobile number"  name="mobile"  required>
  <input type="password" placeholder="New password" name="password" required>
  <button type="submit">Register</button>
</form>

11. Create a login page

Minimal HTML code required for the login form. You can find the source code link of this project below. 

<!-- minimal design of login form, you can design yourself or visit github link provided below -->
  <form action="/login" method="post">
      <input type="text" placeholder="Email or mobile"   name="username" required>
      <input type="password" placeholder="Enter password"  name="password"  required>
      <button type="submit">Sign In</button>
      <p>Do not have account? <a href="/register">REGISTER HERE</a></p>
 </form>

12. Create profile page (the first page after login)

<!--  showing you the essential part to write the authentication detail in html page using thymeleaf -->
<div class="card">
   <div class="profile-pic" th:with="pic=${#authentication.principal.profilePic}">
    <img th:src="${pic!=null?'data:image/png;base64,'+pic:'/assets/user-icon.png'}">
   </div>
   <br>
   <p class="text-center"><strong th:text="${#authentication.principal.name}"></strong></p>
   <p>Email : <span  th:text="${#authentication.principal.email}"></span></p>
   <p>Mobile:<span th:text="${#authentication.principal.mobile}"></span></p>         
    </div>
</div>

13. Screens

 Login Page 

spring boot login page

 

Profile Page

 

Spring Security Profile Page

 

Database table data

 

Thanks for reading this article, I hope you understand well the above explanation.

And as always you can find the source code on GitHub


×