Implementing Spring Security with Spring Boot and MyBatis
This guide demonstrates how to integrate Spring Security into a Spring Boot application using Gradle, MySQL, and MyBatis. We will cover the essential configurations for authentication and authorization, including password encryption and role-based access control.
Database Setup
First, create a database schema to store user credentials and roles. The following SQL script creates a user table and inserts sample data with distinct roles.
CREATE TABLE `app_user` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`username` VARCHAR(100) UNIQUE NOT NULL,
`password_hash` VARCHAR(255) NOT NULL,
`roles` VARCHAR(200) DEFAULT 'ROLE_USER'
);
INSERT INTO `app_user` (`username`, `password_hash`, `roles`)
VALUES ('admin_user', 'admin_pass', 'ROLE_ADMIN,ROLE_USER');
INSERT INTO `app_user` (`username`, `password_hash`, `roles`)
VALUES ('standard_user', 'user_pass', 'ROLE_USER');
Gradle Dependencies
Configure the build.gradle file to include necessary dependencies for Spring Boot Web, Security, Thymeleaf, MyBatis, and MySQL connectivity.
plugins {
id 'java'
id 'org.springframework.boot' version '2.7.0'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
}
group 'com.example'
version '1.0.0'
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.2.2'
implementation 'mysql:mysql-connector-java:8.0.29'
implementation 'org.springframework.boot:spring-boot-devtools'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
Application Configuration
Define the database connection details and MyBatis configuration in application.yml.
server:
port: 8081
spring:
datasource:
url: jdbc:mysql://localhost:3306/security_db
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
thymeleaf:
cache: false
mode: HTML
mybatis:
mapper-locations: classpath:mapper/*.xml
Security Configuration
Create a configuration class to define security rules. This setup disables CSRF for simplicity, configures public access to static resources, and defines the login/logout behavior.
package com.example.app.config;
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.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class AppSecurityConfig {
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/register", "/css/**", "/js/**", "/images/**").permitAll()
.antMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.defaultSuccessUrl("/home")
.permitAll()
.and()
.logout()
.logoutSuccessUrl("/login?logout")
.invalidateHttpSession(true)
.permitAll();
return http.build();
}
}
Custom User Details Service
Implement UserDetailsService to load user-specific data from the database during the authentication process.
package com.example.app.security;
import com.example.app.model.AppUser;
import com.example.app.service.UserService;
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.HashSet;
import java.util.Set;
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserService userService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
AppUser appUser = userService.findByUsername(username);
if (appUser == null) {
throw new UsernameNotFoundException("User not found with username: " + username);
}
Set<GrantedAuthority> authorities = new HashSet<>();
for (String role : appUser.getRoles().split(",")) {
authorities.add(new SimpleGrantedAuthority(role.trim()));
}
return new User(appUser.getUsername(), appUser.getPassword(), authorities);
}
}
Controller Layer
The controller handles HTTP requests and can enforce method-level security using annotations like @PreAuthorize.
package com.example.app.controller;
import com.example.app.model.AppUser;
import com.example.app.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
@Controller
public class AuthController {
@Autowired
private UserService userService;
@GetMapping("/")
public String home() {
return "index";
}
@GetMapping("/login")
public String login() {
return "login";
}
@GetMapping("/register")
public String showRegistrationForm(Model model) {
model.addAttribute("user", new AppUser());
return "register";
}
@PostMapping("/register")
public String registerUser(@ModelAttribute("user") AppUser user) {
userService.save(user);
return "redirect:/login";
}
@GetMapping("/admin/dashboard")
@PreAuthorize("hasAuthority('ROLE_ADMIN')")
public String adminDashboard() {
return "admin_dashboard";
}
}
Service Layer
The service layer handles bussiness logic, including encoding passwords before saving them to the database.
package com.example.app.service;
import com.example.app.mapper.UserMapper;
import com.example.app.model.AppUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private BCryptPasswordEncoder passwordEncoder;
public AppUser findByUsername(String username) {
return userMapper.findByUsername(username);
}
public void save(AppUser user) {
user.setPassword(passwordEncoder.encode(user.getPassword()));
user.setRoles("ROLE_USER");
userMapper.insert(user);
}
}
Data Access Layer
Define the MyBatis Mapper interface for database operations.
package com.example.app.mapper;
import com.example.app.model.AppUser;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@Mapper
public interface UserMapper {
AppUser findByUsername(@Param("username") String username);
void insert(@Param("user") AppUser user);
}
Entity Class
Create the entity class that maps to the data base table.
package com.example.app.model;
public class AppUser {
private Integer id;
private String username;
private String password;
private String roles;
// Getters and Setters
public Integer getId() { return id; }
public void setId(Integer id) { this.id = id; }
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
public String getRoles() { return roles; }
public void setRoles(String roles) { this.roles = roles; }
}