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.
- 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
- 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';
- 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");
}
}
- 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.
- Directory Structure
The project should follow this layout under src/main/:
webapp
├── WEB-INF
│ ├── jsp
│ │ └── login.jsp
│ └── plugin (e.g., jquery, bootstrap)
├── css
├── images
- 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.