Custom Login with Spring Boot and Spring Security – UserDetailsService and Custom Login Page

This guide demonstrates how to impelment a custom login mechanism using Spring Boot 2.3.4 and Spring Security. Instead of relying on the default auto‑configuration, you will define your own UserDetailsService, configure a custom login page (JSP), and tailor the WebSecurityConfigurerAdapter to your needs.

  1. Maven Dependencies and Application Properties

The pom.xml includes the necessary staretrs for web, security, JSP support, JDBC, and MyBatis.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.4.RELEASE</version>
    </parent>
    <groupId>com.example</groupId>
    <artifactId>custom-security</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.3</version>
        </dependency>
        <dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-jasper</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>jstl</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

Application properties (example application.properties):

server.port=8888
spring.mvc.view.prefix=/WEB-INF/jsp/
spring.mvc.view.suffix=.jsp
spring.datasource.url=jdbc:mysql://127.0.0.1:7799/spring?useSSL=false&serverTimezone=CST
spring.datasource.username=lzf
spring.datasource.password=123
spring.datasource.hikari.connection-timeout=180000
spring.datasource.hikari.maximum-pool-size=6

  1. Custom UserDetailsService Implementation

Create a service that implements UserDetailsService. The loadUserByUsername method fetches a user (with roles) from the database and returns a Spring Security UserDetails. No {bcrypt} prefix is required in the password field when using BCryptPasswordEncoder.

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.User;
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.List;

@Service
public class CustomUserDetailsService implements UserDetailsService {

    @Autowired
    private MyUserService userService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        UserRolePojo userRole = userService.getUserRoleDetail(username);
        if (userRole == null) {
            throw new UsernameNotFoundException("User not found: " + username);
        }
        return buildUserDetails(userRole);
    }

    private UserDetails buildUserDetails(UserRolePojo userRole) {
        List<GrantedAuthority> authorities = new ArrayList<>();
        for (RolePojo role : userRole.getRoleList()) {
            authorities.add(new SimpleGrantedAuthority(role.getName()));
        }
        return new User(
            userRole.getUserPojo().getName(),
            userRole.getUserPojo().getPassword(),
            authorities
        );
    }
}

Database tables and sample data:

CREATE TABLE `users` (
  `id` int NOT NULL AUTO_INCREMENT,
  `name` varchar(40) NOT NULL,
  `password` varchar(70) NOT NULL,
  `create_time` varchar(20) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE `role` (
  `id` int NOT NULL AUTO_INCREMENT,
  `name` varchar(40) NOT NULL,
  PRIMARY KEY (`id`)
);

CREATE TABLE `user_role_detail` (
  `id` int NOT NULL AUTO_INCREMENT,
  `user_id` int NOT NULL,
  `role_id` int NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE INDEX `uid_user_role_detail` (`user_id`, `role_id`)
);

-- Password is BCrypt of "123"
INSERT INTO `users` (`name`, `password`, `create_time`) VALUES
('lzf', '$2a$10$Mg8XzxbqsOMQAxrPD8d9hOELzDyGc7lShVdSb7vOLWwEplWlga7cO', '2020-08-11 12:00:00'),
('wth', '$2a$10$Mg8XzxbqsOMQAxrPD8d9hOELzDyGc7lShVdSb7vOLWwEplWlga7cO', '2020-08-11 12:00:00');

INSERT INTO `role` (`name`) VALUES ('ADMIN'), ('MANAGER'), ('LEADER'), ('CEO');

INSERT INTO `user_role_detail` (user_id, role_id)
SELECT u.id, r.id FROM users u, role r WHERE u.name = 'lzf';
INSERT INTO `user_role_detail` (user_id, role_id)
SELECT u.id, r.id FROM users u, role r WHERE u.name = 'wth' AND r.name != 'ADMIN';

  1. Security Configuration (WebSecurityConfigurerAdapter)

In the security configuration class, define the login page, the login processing URL, and permit unrestricted access to static resources and the login endpoints. Use a BCrypt password encoder.

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private CustomUserDetailsService userDetailsService;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .formLogin()
                .loginPage("/loginPage")          // custom login page endpoint
                .loginProcessingUrl("/doLogin")   // URL that form submits to
                .permitAll()
            .and()
            .authorizeRequests()
                .antMatchers("/loginPage", "/doLogin", "/error404", "/plugin/**").permitAll()
                .anyRequest().authenticated()
            .and()
            .csrf().disable();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        PasswordEncoder encoder = new BCryptPasswordEncoder();
        auth.userDetailsService(userDetailsService).passwordEncoder(encoder);
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/plugin/**", "/css/**", "/images/**");
    }
}

The login page endpoint (/loginPage) must be implemented in a controller. For example:

@Controller
public class LoginController {
    @GetMapping("/loginPage")
    public ModelAndView showLogin() {
        return new ModelAndView("login");
    }
}

  1. Custom Login Page (JSP)

Place login.jsp under /WEB-INF/jsp/. The form’s action must match the loginProcessingUrl defined in the security configuration. The username and password field names must be username and password (defaults used by UsernamePasswordAuthenticationFilter).

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>

<html>
<head>
    <meta charset="UTF-8">
    <title>Custom Login</title>
</head>
<body>
    <h2>Sign In (JSP)</h2>
    <form method="post" action="/doLogin">
        <label>Username: <input type="text" name="username" /></label><br/>
        <label>Password: <input type="password" name="password" /></label><br/>
        <input type="submit" value="Login" />
    </form>
</body>
</html>

Note about AJAX login: If you prefer to submit the form via AJAX, the URL must be /login by default (defined in UsernamePasswordAuthenticationFilter). To use a custom URL, you would need to extend AbstractAuthenticationProcessingFilter and configure it in the security chain. For most cases, the standard form submission is sufficient.

  1. Directory Structure

The project should follow this layout under src/main/:

webapp
├── WEB-INF
│   ├── jsp
│   │   └── login.jsp
│   └── plugin          (e.g., jquery, bootstrap)
├── css
├── images

  1. Starting the Application

The main Spring Boot class does not need scanBasePackages; simply use the default scanning:

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SecurityApplication {
    public static void main(String[] args) {
        SpringApplication.run(SecurityApplication.class, args);
    }
}

Now the application will serve the custom login page at /loginPage and authenticate against the database using the CustomUserDetailsService.

Tags: Spring Boot Spring Security UserDetailsService JSP BCryptPasswordEncoder

Posted on Wed, 02 Sep 2026 16:30:18 +0000 by matifibrahim