Saturday, December 15, 2018

How to make login/out functionality in Spring boot

Prerequisites

I am using
  • vscode for IDE.
  • Ubuntu18 for OS
  • Java 1.8

Preparations

Go to start.spring.io and download a project.
My configuration is the following:
Artifact ID: login-demo
Dependencies:
  • Web,
  • Security (for login functionality),
  • Thymeleaf (for view),
  • JPA (for database access)
  • MySQL (database driver)

To add these dependencies, search for dependency in the "Search for dependencies" and click the suggestion. After configuring everything, click on "Generate Project". The downloaded zip will be used after creating a database.

Create a database

Install mariadb and create a database to store user information. (You can use mysql57 too if you prefer mysql.)
If you are Ubuntu user like me, you can see here for how to install mariadb-server. If you are a Windows user, get an installer from here.
Once you start mariadb, create a dabase and a user table like this:
create database logindemo;
use logindemo;
CREATE TABLE users (
    `id` bigint(20) unsigned NOT NULL UNIQUE AUTO_INCREMENT,
    `name` VARCHAR(20) NOT NULL UNIQUE,  
    `password` CHAR(128)
);
INSERT INTO users (name, password)
VALUES ('test', '$2a$10$Fonnn4/zL3wGBvcyYbhIu.bz6q8CKuSyCQyIYqrpNdeVfO1J.Gn0S');

Start the project

Extract the downloaded zip from start.spring.io and open the unziped project by vscode. You can see here for how to open with vscode.
After opening the project, you will find application.properties here:
/login-demo/src/main/resources/application.properties
Write as follows inside:
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/logindemo
#your username of mariadb
spring.datasource.username=root
#your password of mariadb
spring.datasource.password=root
spring.jpa.show-sql=true
spring.messages.basename=messages
Then press F5 on your keyboard. If the app starts without error, it is success.

If it started without error, press Shift + F5 and stop the app.

Hello World page behind a login prompt

Create controller/HelloWorldController.java in logindemo directory.

Write as follows in the controller and save it:
package com.example.logindemo.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class HelloWorldController {
    @GetMapping(value = "/")
    public String index(Model model) {
        return "pages/index";
    }
}
Create pages/index.html in login-demo/src/main/resources/templates like this:

And write as follows in the index.html and save it:
<h1>Hello World</h1>
Now start the app again by pressing F5. And look at http://localhost:8080.
You will be asked to login:

Of course you can not login yet whatever you write.
Once you confirmed that it is working, stop the app by pressing Shift+F5.

User Model

At first, we need to make a model file of User table. Create User.java in login-demo/src/main/java/com/example/logindemo/model.

Then write as follows in the User.java:
package com.example.logindemo.model;

import javax.persistence.Entity;
import javax.persistence.Table;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;
    private String name;
    private String password;

    public long getId() {
        return this.id;
    }

    public void setId(long id) {
        this.id = id;
    }

    public String getName() {
        return this.name;
    }

    public String getUsername() {
        return this.name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getPassword() {
        return this.password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

}
Also we need to make UserRepository to access the database. Create repository/UserRepository.java:

And write as follows inside:
package com.example.logindemo.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import org.springframework.data.jpa.repository.Query;
import com.example.logindemo.model.User;

@Repository
public interface UserRepository extends JpaRepository<User,Long>{
    @Query("select u from User u where u.name = :name")
    User findByName(String name);
}

UserDetailsService

We need a UserDetailsService. Create service/UserDetailsServiceImpl.java.

and write as follows:
package com.example.logindemo.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.HashSet;
import java.util.Set;

import com.example.logindemo.repository.UserRepository;
import com.example.logindemo.model.User;

@Service
public class UserDetailsServiceImpl implements UserDetailsService{
    @Autowired
    private UserRepository userRepository;

    @Override
    @Transactional(readOnly = true)
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByName(username);
        if(user == null){
            throw new UsernameNotFoundException("This user name \""+ username +"\" was not found in our database.");
        }
        Set<GrantedAuthority> grantedAuthorities = new HashSet<>();
        //grantedAuthorities.add(new SimpleGrantedAuthority(user.getRole().getName()));

        return new org.springframework.security.core.userdetails.User(user.getName(), user.getPassword(), grantedAuthorities);
    }
}

Security Config

Create config/SecurityConfig.java like this:

You can configure the login functionality from this file. If you want to use customized login page, uncomment .loginPage("/login") and create a login.html in templatesfolder.
Write as follows inside the SecurityConfig.java:
package com.example.logindemo.config;

import org.springframework.security.authentication.AuthenticationManager;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http.csrf().disable()
                .authorizeRequests()
                    //.antMatchers("/create_user", "/about", "/non_login/**").permitAll()
                    //.antMatchers("/admin/**").hasAnyRole("ADMIN")
                    //.antMatchers("/user/**").hasAnyRole("USER")
                    .anyRequest().authenticated()
                .and()
                 .formLogin()
                //  .loginPage("/login")
                    .permitAll()
                    .and()
                .logout()
                    .permitAll();
                    //.logoutRequestMatcher(new AntPathRequestMatcher("/login?logout"));
    }

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

    @Bean
    public AuthenticationManager customAuthenticationManager() throws Exception {
        return authenticationManager();
    }

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
    }

}

Check if it works

After pressing F5 and starting the app on the vscode, go to http://localhost:8080/login and fill the necessary information and submit.
The user name: test
The password: 12345678

You can login and see the hello world behind the login prompt now: