Spring Security 2023

This tutorial will guide you to secure a Spring MVC application using Spring Security. You will learn how to :

  1. Secure a spring boot app with email & password.
  2. Load user from a database.
  3. Add custom login page.
  4. Add user registration page.

You can also check my?Full Spring Boot REST API Tutorial

Creating A Spring MVC Application

Create a spring MVC application from?Spring Initializr?with Java version 17. Create a new package named controllers. Then create a new controller named?HomeController.java?inside our newly created controllers package. Write the following code inside HomeController.java :

package com.example.demo.controllers;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@RequestMapping("")
public class HomeController {

    @ResponseBody
    @RequestMapping(value = "/",method = RequestMethod.GET)
    public String index(){
        return "hello world";
    }

    @ResponseBody
    @RequestMapping(value = "/about",method = RequestMethod.GET)
    public String about(){
        return "about";
    }
}        

We have created two urls :?“/”?&?“/about”. Now run the application through IDE or through command line with the following command :

mvn spring-boot:run         

If you visit?https://localhost:8080/?, Then you will get the following page.

Or you can visit?https://localhost:8080/about?and get a similar page.

Securing Routes

Now we want to secure our url routes with username & password. Add the following dependencies in pom.xml file :


  <dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-security</artifactId>
  </dependency>

  <dependency>
   <groupId>org.springframework.security</groupId>
   <artifactId>spring-security-test</artifactId>
   <scope>test</scope>
  </dependency>        

Now, re run the application. If you want to visit the previous two urls, you will be first redirected to a login page. Use the username :?user?. Spring boot will create a password to login, you can find it in the terminal :

To log out , got to?https://localhost:8080/logout?. Each time you run the application, spring boot will generate a different password for you. We don’t want to use a username & password provided by Spring. We will provide our own username & password. To Customize spring security, create a new package named “config” and create a Java class named?SecurityConfig.java?and add the following code :

package com.example.demo.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .authorizeHttpRequests()
                .anyRequest().authenticated();
        http
                .formLogin();

        return http.build();
    }

    @SuppressWarnings("deprecation")
    @Bean
    public NoOpPasswordEncoder passwordEncoder() {
        return (NoOpPasswordEncoder) NoOpPasswordEncoder.getInstance();
    }

}        

We used two annotations : “@Configuration” and “@EnableWebSecurity”. These annotations tells spring security to use our custom security configuration instead of default one. We created a Bean of?SecurityFilterChain?which implements our custom filter logic.

http
     .authorizeHttpRequests()
     .anyRequest().authenticated();        

These lines tells spring boot to authenticate every http requests.

http.formLogin();        

This line tells spring boot to use form login.

We also created another Bean of?NoOpPasswordEncoder. We are using this bean because we don’t want any encoding (e.g. Bcrypt) to our passwords. That means, we are storing plain text into database as passwords. In practice, we don’t store plain text passwords into database. But if we run the application, still we have to use default username & password provided by spring security. Before providing custom username & password, we need to create a?User?model class. Create a package named “model” and create a class named?User?:

package com.example.demo.model;


import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.Date;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {

    private long id;

    private String email;

    private String password;

    private String firstName;

    private String lastName;

    private String profilePhotoUrl;

    private String country;
 
    private String occupation;

    private Date dateOfBirth;

    private String gender;

    private String role;

    public User(String email, String password, String firstName, String lastName) {
        this.email = email;
        this.password = password;
        this.firstName = firstName;
        this.lastName = lastName;
    }
}        

We have used several annotations with User class. “@Data” , “@NoArgsConstructor” & “@AllArgsConstructor”. These annotations creates getter, setter methods & constructors. To use these annotations add the following dependency to pom.xml file :

  <dependency>
   <groupId>org.projectlombok</groupId>
   <artifactId>lombok</artifactId>
   <version>1.18.24</version>
   <scope>provided</scope>
  </dependency>        

UserDetailsService & UserRepository

Spring security uses an interface called?UserDetailsService?to load user details and match the user with the user input. By default spring will match with default username & password provided by spring security. To provide our custom usernames & passwords, we have to implement our own custom user details service. We also need to create a repository to load user details from database. First create a repository named?UserRepository.java?in “repositories” package :

package com.example.demo.repositories;


import com.example.demo.model.User;
import org.springframework.stereotype.Repository;

@Repository
public class UserRepository {
    public User findUserByEmail(String email){
        return new User(email,"123456","Farhan","Tanvir");
    }
}        

We created a method inside?UserRepository?class to find users by email addresses. This method will search a user by email from database and return it. In our example we are returning a static?User?object with password : “123456”. We are using a plain text password because we didn’t use any encoding for passwords.?UserDetailsService?will use this password to verify the user. Let’s create our custom?UserDetailsService.?Create a new class?CustomUserDetailsService?inside a new package “services” :

package com.example.demo.services;

import com.example.demo.model.User;
import com.example.demo.repositories.UserRepository;
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 java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

@Service
public class CustomUserDetailsService implements UserDetailsService {

    private UserRepository userRepository;

    public CustomUserDetailsService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
        User user = userRepository.findUserByEmail(email);
        List<String> roles = Arrays.asList(user.getRole());
        UserDetails userDetails =
                org.springframework.security.core.userdetails.User.builder()
                        .username(user.getEmail())
                        .password(user.getPassword())
                        .roles("USER")
                        .build();

        return userDetails;
    }
}        

We inject an?UserRepository?object using dependency injection and override the?loadUserByUsername()?method, which returns an UserDetails object. In our implementation,?loadUserByUsername()?get user by the email address from?UserRepository?, construct an?UserDetails?object from it and then returns. Spring security will internally call this method with user provided email, and then match the password from?UserDetails?object with user provided password.

Now, save & run the application. As we are always returning a static user with password : “123456” , it doesn’t matter which email/username we input. Use password “123456” to login otherwise you will get an authentication error.

The whole process is summarized in the above image.

Loading User Details From Database

Our UserRepository doesn’t return user from a database, instead it always returns a static user with a hard coded password. Now we will add a PostgreSQL database in out application and it will store & load user from database by email.

Add the following dependencies to?pom.xml?file :

  <dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-jdbc</artifactId>
  </dependency>
  
  <dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-data-jpa</artifactId>
  </dependency>
  
  <dependency>
   <groupId>org.postgresql</groupId>
   <artifactId>postgresql</artifactId>
   <scope>runtime</scope>
  </dependency>        

We have added 3 dependencies for : JDBC, JPA & PostgreSQL. JDBC is a low level standard for working on database. JDBC executes raw SQL queries. On the other hand, JPA is an ORM (Object Relational Mapping) which maps java classes to database table. JPA is a high level API for interacting with database. In this article, we will use?JPA?for sql queries.

First create a postgresql database in local or in cloud & create an “users”?table. Run the following sql query to create a table named “users” :

CREATE TABLE IF NOT EXISTS users (
    id serial PRIMARY KEY,
    country VARCHAR(255),
    date_of_birth TIMESTAMP NOT NULL,
    email VARCHAR(255),
    first_name VARCHAR(255),
    gender VARCHAR(255),
    last_name VARCHAR(255),
    occupation VARCHAR(255),
    password VARCHAR(255),
    profile_photo_url VARCHAR(255),
    role VARCHAR(255)
);        

Let’s insert an user into our database by running following query :

INSERT INTO users(first_name, last_name, email, password, profile_photo_url, country, occupation, gender, role, date_of_birth)
VALUES ('James','Smith','[email protected]','james123','','Australia','Doctor','Male','USER',timestamp '1987-01-10');        

We have inserted an user named “James Smith” into our database. Let’s insert one more user :

INSERT INTO users(first_name, last_name, email, password, profile_photo_url, country, occupation, gender, role, date_of_birth)
VALUES ('Yousuf','Hussain','[email protected]','yousuf22','','Egypt','Teacher','Male','USER',timestamp '1984-01-11');        

Configuring Database In Spring Boot

In the src > main > resources directory, there is a file named “application.properties” .?Write the following properties to this file :


# PostgreSQL connection settings
spring.datasource.url=jdbc:postgresql://localhost:5432/database_name
spring.datasource.username=your_database_username
spring.datasource.password=your_database_password
spring.datasource.driver-class-name=org.postgresql.Driver

# HikariCP settings
spring.datasource.hikari.minimumIdle=5
spring.datasource.hikari.maximumPoolSize=20
spring.datasource.hikari.idleTimeout=30000
spring.datasource.hikari.maxLifetime=2000000
spring.datasource.hikari.connectionTimeout=30000
spring.datasource.hikari.poolName=HikariPoolBooks

# JPA settings
spring.jpa.properties.hibernate.dialect= org.hibernate.dialect.PostgreSQLDialect
spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true
spring.jpa.properties.hibernate.jdbc.batch_size=15
spring.jpa.properties.hibernate.order_inserts=true
spring.jpa.properties.hibernate.generate_statistics=true
spring.jpa.show-sql=true
spring.oracle.persistence.unit=postgresql        

Here, I have created a postgresql database in local host and used its connection parameters. Use your own database password & connecction parameters.

Using JPA For Database Queries

JPA maps java classes to database tables. We will use our?User?class for?users?table in db. Update our?User?class as following :


package com.example.springmvcdemo.model;


import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.Date;

@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "USERS")
public class User {

    @Id
    @Column(name = "ID")
    @GeneratedValue(strategy= GenerationType.IDENTITY)
    private Long id;

    @Column(name = "EMAIL")
    private String email;

    @Column(name = "PASSWORD")
    private String password;

    @Column(name = "FIRST_NAME")
    private String firstName;

    @Column(name = "LAST_NAME")
    private String lastName;

    @Column(name = "PROFILE_PHOTO_URL")
    private String profilePhotoUrl;

    @Column(name = "COUNTRY")
    private String country;

    @Column(name = "OCCUPATION")
    private String occupation;

    @Column(name = "DATE_OF_BIRTH")
    private Date dateOfBirth;

    @Column(name = "GENDER")
    private String gender;

    @Column(name = "ROLE")
    private String role;

    public User(String email, String password, String firstName, String lastName) {
        this.email = email;
        this.password = password;
        this.firstName = firstName;
        this.lastName = lastName;
    }
}        

We have added some annotations with our class and properties. The?“@Entity”?annotation tells?jpa?to use this class as a table. The?“@Table”?annotations tells?jpa?which table to map this class with. The?“@Column”?annotation over properties tells corresponding column names for the class properties. Now let’s update our?UserRepository?class. Instead of returning a static user, now it will fetch user from database. Update the?UserRepository.java?as following :

package com.example.demo.repositories;

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

@Repository
public interface UserRepository extends JpaRepository<User,Long> {

    @Query("SELECT u FROM User u WHERE u.email = :email")
    User findUserByEmail(@Param("email") String email);
}        

We declared UserRepository as an interface which extends JpaRepository interface. We updated our?findUserByEmail()?which with a “@Query” annotation. We specify sql query in this annotation. We inject the method variable “email” into sql with “:email”.

Now, save & run the code. Try to login with a user credencial that we inserted into database before. For example :

username : [email protected]
password : james123

If you enter wrong password, you will get a bad credencial error. If you enter an email that doesn’t exist in the database, then you will get an error message like this :

UserRepository?didn’t find any user with that email. So, it returned a null user. To handle this error properly, let’s update the?loadUserByUsername()?method in?CustomUserDetailsService?class :

@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
    User user = userRepository.findUserByEmail(email);
    if(user == null){
        throw new UsernameNotFoundException("No user found with email");
    }
    List<String> roles = Arrays.asList(user.getRole());
    UserDetails userDetails =
            org.springframework.security.core.userdetails.User.builder()
                    .username(user.getEmail())
                    .password(user.getPassword())
                    .roles("USER")
                    .build();

    return userDetails;
}        

We checked if user is null and then threw an?UsernameNotFoundException?if user is null. If we save & run this application again, the application will show a bad credential error message if no user is found with this email.

Adding Custom Login Page

So far, we have been using spring provided login page. When we visit “/login” route, spring take us to its own created login page. We will now create a custom login page and later will create a user registration page.

First, create a new controller class?AuthController.java?in controllers package.

package com.example.demo.controllers;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@RequestMapping("")
public class AuthController {

    @RequestMapping(value = "/login",method = RequestMethod.GET)
    public String login(){
        return "login";
    }

}        

We are creating our own login route. Notice, we didn’t use the “@ResponseBody” annotation, because we want to return an html page instead of string data. When we hit the “/login” route, our application will look for a?login.html?page in resources folder. We will use?Thymleaf?for rendering html page. We need to add?thymleaf?dependency in?pom.xml?file

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>        

Next create a html page “login.html” in?resources > templates?directory. The directory structure will look like this :

Add the following code to login.html file

<!DOCTYPE html>
<html xmlns:th="https://www.thymeleaf.org">
<head>
    <title>Login</title>
</head>
<body>
<h1>Login</h1>
<p th:if="${param.error}" class="error">Wrong email or password</p>
<form th:action="@{/login}" method="post">
    <label for="email">Email</label>:
    <input type="text" id="email" name="username" autofocus="autofocus" /> <br />
    <label for="password">Password</label>:
    <input type="password" id="password" name="password" /> <br />
    <input type="submit" value="Log in" />
</form>
</body>
</html>        

We also need to tell our application to use our custom login page. To do this, update the SecurityConfig.java class :

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
            .authorizeHttpRequests()
            .requestMatchers("/login").permitAll()
            .anyRequest().authenticated();
    http
            .formLogin()
            .loginPage("/login");

    return http.build();
}        

First we tell the security filter chain to allow “/login” route without any authentication:

.requestMatchers("/login").permitAll()        

Then we tell the application our custom login page route

.loginPage("/login");        

Now run the application, you will see the following page if you visit?“/login”?url :

Logout Controller

We need to add a?“/logout”?controller if we add custom login page. Update the AuthController as follows :


/../
@Controller
@RequestMapping("")
public class AuthController {

    @RequestMapping(value = "/login",method = RequestMethod.GET)
    public String login(){
        return "login";
    }

    @RequestMapping(value = "/logout",method = RequestMethod.GET)
    public String logout(HttpServletRequest request){
        HttpSession session = request.getSession();
        session.invalidate();
        SecurityContext securityContext = SecurityContextHolder.getContext();
        securityContext.setAuthentication(null);
        return "redirect:/login";
    }
}        

In the logout() method, we are invalidating current session and setting Spring SecurityContext authentication to null. Finally we are redirecting to login page.

Adding User Registration Controller

So far we have manually inserted user into user table. We will now create controller to register new users. First create a UserService in the services package for inserting user into database:

package com.example.springmvcdemo.services;

import com.example.springmvcdemo.model.User;
import com.example.springmvcdemo.repositories.UserRepository;
import org.springframework.stereotype.Service;
@Service
public class UserService {
    private final UserRepository userRepository;
    public UserService(UserRepository userRepository){
        this.userRepository = userRepository;
    }
    public User getUserByEmail(String email){
        User user = userRepository.findUserByEmail(email);
        return user;
    }
    public User createUser(User user){
        User newUser = userRepository.save(user);
        userRepository.flush();
        return newUser;
    }
}        

createUser( )?method will take an User object as parameter & store the user into database with?userRepository.save( )?method.

getUserByEmail( )?method will take an email as parameter & find a user from database by email.

Now, add a new route in AuthController for user registration :

/../

@Controller
@RequestMapping("")
public class AuthController {

    @RequestMapping(value = "/login",method = RequestMethod.GET)
    public String login(){
        return "login";
    }

    @RequestMapping(value = "/register",method = RequestMethod.GET)
    public String register(HttpServletRequest request, HttpServletResponse response, Model model){
        User user = new User();
        model.addAttribute("user",user);
        return "register";
    }

    /../

}        

We have created a new route “/register”. This controller method will return a html page for user registration. Inside the method, we created an object of User class & added it to the model attributes. This user object can be accessed from registration view page created with thymleaf. Now, create a new html page register.html in templates folder :

<!DOCTYPE html>
<html xmlns:th="https://www.thymeleaf.org">

<head>
    <title>Sign Up</title>
</head>
<body>
<h1>Sign Up</h1>
<p th:if="${param.error}" class="error">Registration failed</p>
<form th:action="@{/register}" method="post" th:object="${user}">
    <label for="email">Email </label>:
    <input type="text" id="email"  th:field="*{email}" /> <br />
    <label for="password">Password </label>:
    <input type="password" id="password" th:field="*{password}"/> <br />
    <label for="first_name">First Name </label>:
    <input type="text" id="first_name"  th:field="*{firstName}" /> <br />
    <label for="last_name">Last Name </label>:
    <input type="text" id="last_name"  th:field="*{lastName}" /> <br />
    <input type="submit" value="Sign Up" /> <br />
</form>

<p>Already have an account? </p> <br/>
<a href="/login">Login</a>
</body>
</html>        

Notice, this html form send a POST request to “/register” url. Finally, we also need to allow “/register” url in SecurityContext :

http
    .authorizeHttpRequests()
    .requestMatchers("/login**","/logout**").permitAll()
    .anyRequest().authenticated();        

Now run the application , and visit “/register”

Now, we need to add another controller method to handle post request from the registration page. We will also log in user after a successful registration. So, we need to add an AuthenticationManager Bean for manual login. Update the SecurityContext :


@Configuration
@EnableWebSecurity
public class SecurityConfig {

    private final CustomUserDetailsService userDetailsService;

    public SecurityConfig(CustomUserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }

    /../

    @Bean
    public AuthenticationManager authenticationManager(HttpSecurity http, NoOpPasswordEncoder noOpPasswordEncoder)
            throws Exception {
        AuthenticationManagerBuilder authenticationManagerBuilder = http.getSharedObject(AuthenticationManagerBuilder.class);
        authenticationManagerBuilder.userDetailsService(userDetailsService).passwordEncoder(noOpPasswordEncoder);
        return authenticationManagerBuilder.build();
    }

    /../

}        

Then add new controller method to AuthController :

package com.example.springmvcdemo.controllers;

import com.example.springmvcdemo.model.User;
import com.example.springmvcdemo.services.UserService;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
@RequestMapping("")
public class AuthController {

    private final UserService userService;

    private final AuthenticationManager authenticationManager;

    public AuthController(UserService userService, AuthenticationManager authenticationManager) {
        this.userService = userService;
        this.authenticationManager = authenticationManager;
    }

    /../

    @RequestMapping(value = "/register",method = RequestMethod.GET)
    public String register(HttpServletRequest request, HttpServletResponse response, Model model){
        User user = new User();
        model.addAttribute("user",user);
        return "register";
    }


    @RequestMapping(value = "/register",method = RequestMethod.POST)
    public String createNewUser(HttpServletRequest request, HttpServletResponse response, @ModelAttribute("user")User user){

        try {

            user.setRole("USER");

            User newUser = userService.createUser(user);
            if(newUser == null){
                return "redirect:/register?error";
            }

            Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(user.getEmail(),user.getPassword()));
            SecurityContext securityContext = SecurityContextHolder.getContext();
            securityContext.setAuthentication(authentication);
            HttpSession session = request.getSession(true);
            session.setAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY,securityContext);

            return "redirect:/";

        } catch (Exception e){
            return "redirect:/register?error";
        }

    }

    /../

}        

Now save and run the application. You will be able to register new user.



要查看或添加评论,请登录

社区洞察

其他会员也浏览了