Integrating Spring Security with Spring Boot: A Comprehensive Guide

Environment Setup

  • JDK version: 17
  • Build tool: Gradle
  • Spring Boot version: 2.7.18 (Note: Spring Boot 3 introduces significant changes)
  • Spring Security version: 5.7.11

Core Components and Authentication Flow

Spring Security implements authentication and authorization through a chain of filters. Each filter has a specific responsibility, and only the required filters are activated based on functionality needs.

These security filters aren't directly added to the web filter chain. Instead, a FilterChainProxy acts as a delegate, managing which security filters are included.

Key Component Overview

Authenticasion (Principal)

Represents user identity information. Key implementations include:

  • AbstractAuthenticationToken
  • RememberMeAuthenticationToken (for remember-me authentication)
  • UsernamePasswordAuthenticationToken (for username/password authentication)

AuthenticationManager

Serves as a proxy for authentication providers. ProviderManager is the primary implementation that delegates to multiple AuthenticationProvider instances.

AuthenticationProvider

Performs actual authentication tasks. Notable implementations:

  • AbstractUserDetailsAuthenticationProvider
  • DaoAuthenticationProvider
  • RememberMeAuthenticationProvider

UserDetailsService

Defines user information sources with a single method: loadUserByUsername. Main implementations:

  • UserDetailsManager
  • InMemoryUserDetailsManager
  • JdbcUserDetailsManager
  • Custom implementations

UserDetails

Provides detailed user identity information. Common implementations:

  • User (built-in)
  • Custom implementations

SecurityContextHolder

Helper class for storing and retrieving authentication information.

FilterChainProxy

The entry point for Spring Security filters, managing multiple filter chains.

AbstractHttpConfigurer

Core component for building filters with init() and configure() methods. Key implementations:

  • FormLoginConfigurer
  • CorsConfigurer
  • CsrfConfigurer
  • HttpBasicConfigurer
  • LogoutConfigurer

User Configuration Methods: Memory, JDBC, and MyBatis

In-Memory Configuration

User information configured in memory through code:

@Configuration
public class SecurityConfiguration {
    @Bean
    public InMemoryUserDetailsManager userDetailsManager(){
        UserDetails user1 = User.withUsername("user1").password("{noop}password1").roles("USER").build();
        UserDetails user2 = User.withUsername("user2").password("{noop}password2").roles("ADMIN").build();
        return new InMemoryUserDetailsManager(user1, user2);
    }
}

JDBC-Based Configuration

Dependencies

implementation 'mysql:mysql-connector-java:8.0.32'
implementation 'org.springframework.boot:spring-boot-starter-jdbc'

Database Schema

Located at: org/springframework/security/core/userdetails/jdbc/users.ddl Remove _ignorecase suffix from the script.

JDBC Manager Configuration

@Autowired
private DataSource dataSource;

@Bean
public JdbcUserDetailsManager jdbcUserDetailsManager(){
    JdbcUserDetailsManager manager = new JdbcUserDetailsManager(dataSource);
    if(!manager.userExists("dbuser1")){
        manager.createUser(User.withUsername("dbuser1").password("{noop}dbpass1").roles("USER").build());
    }
    if(!manager.userExists("dbuser2")){
        manager.createUser(User.withUsername("dbuser2").password("{noop}dbpass2").roles("ADMIN").build());
    }
    return manager;
}

MyBatis Integration

Additional Dependency

implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:+'

Custom User Entity

public class SecurityUser implements UserDetails {
    private Long userId;
    private String userName;
    private String userPass;
    private Boolean isActive;
    private Boolean notExpired;
    private Boolean notLocked;
    private Boolean credentialsValid;
    private List<UserRole> assignedRoles = new ArrayList<>();
    
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<SimpleGrantedAuthority> authorities = new ArrayList<>();
        for (UserRole role : assignedRoles) {
            authorities.add(new SimpleGrantedAuthority(role.getRoleName()));
        }
        return authorities;
    }

    @Override
    public String getPassword() { return userPass; }

    @Override
    public String getUsername() { return userName; }

    @Override
    public boolean isAccountNonExpired() { return notExpired; }

    @Override
    public boolean isAccountNonLocked() { return notLocked; }

    @Override
    public boolean isCredentialsNonExpired() { return credentialsValid; }

    @Override
    public boolean isEnabled() { return isActive; }
    
    // Getters and setters omitted for brevity
    
    public static class UserRole {
        private Long roleId;
        private String roleName;
        private String displayName;
        
        // Getters and setters omitted
    }
}

UserDetailsService Implementation

@Service
public class DatabaseUserDetailsService implements UserDetailsService {
    @Autowired
    private UserMapper userMapper;
    
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        SecurityUser user = userMapper.findUserByName(username);
        if (user == null) {
            throw new UsernameNotFoundException("User not found: " + username);
        }
        user.setAssignedRoles(userMapper.findRolesByUserId(user.getUserId()));
        return user;
    }
}

MyBatis Mapper Interface

@Mapper
public interface UserMapper {
    @Select("SELECT r.* FROM user_roles ur LEFT JOIN roles r ON ur.role_id = r.role_id WHERE ur.user_id = #{userId}")
    List<SecurityUser.UserRole> findRolesByUserId(@Param("userId") Long userId);

    @Select("SELECT * FROM users WHERE user_name = #{username} LIMIT 1")
    SecurityUser findUserByName(String username);
}

Database Tables

CREATE TABLE roles (
    role_id INT PRIMARY KEY AUTO_INCREMENT,
    role_name VARCHAR(32),
    display_name VARCHAR(32)
);

CREATE TABLE users (
    user_id INT PRIMARY KEY AUTO_INCREMENT,
    user_name VARCHAR(32),
    user_pass VARCHAR(255),
    is_active TINYINT(1),
    not_expired TINYINT(1),
    not_locked TINYINT(1),
    credentials_valid TINYINT(1)
);

CREATE TABLE user_roles (
    mapping_id INT PRIMARY KEY AUTO_INCREMENT,
    user_id INT,
    role_id INT,
    INDEX idx_user (user_id),
    INDEX idx_role (role_id)
);

INSERT INTO roles VALUES 
(1,'ROLE_ADMIN','Administrator'),
(2,'ROLE_USER','Regular User');

INSERT INTO users VALUES 
(1,'admin','{noop}admin123',1,1,1,1),
(2,'user','{noop}user123',1,1,1,1);

INSERT INTO user_roles VALUES 
(1,1,1),
(2,1,2),
(3,2,2);

Custom Authentication: CAPTCHA Implementation

Design Approach

Spring Security doesn't natively support CAPTCHA verification, but we can extend it by creating a custom authentication provider that inherits from DaoAuthenticationProvider.

Implementation steps:

  1. Create a custom authentication provider extending DaoAuthenticationProvider
  2. Implement CAPTCHA validation logic
  3. Retrieve stored CAPTCHA from session
  4. Extract user input from request parameters
  5. Compare values and proceed with authentication if valid
  6. Throw exception for invalid CAPTCHA
  7. Register custom provider with AuthenticationManager

Implementation

Dependencies

implementation 'com.github.penggle:kaptcha:2.3.2'

CAPTCHA Configuraton

@Bean
public Producer captchaProducer() {
    Properties props = new Properties();
    props.setProperty("kaptcha.image.width", "150");
    props.setProperty("kaptcha.image.height", "50");
    props.setProperty("kaptcha.textproducer.char.string", "0123456789");
    props.setProperty("kaptcha.textproducer.char.length", "4");
    Config config = new Config(props);
    DefaultKaptcha kaptcha = new DefaultKaptcha();
    kaptcha.setConfig(config);
    return kaptcha;
}

CAPTCHA Endpoint

@Autowired
private Producer captchaProducer;

@RequestMapping("/captcha")
public void generateCaptcha(HttpServletResponse response, HttpSession session) throws IOException {
    response.setContentType("image/jpeg");
    String captchaText = captchaProducer.createText();
    session.setAttribute("CAPTCHA_VALUE", captchaText);
    BufferedImage captchaImage = captchaProducer.createImage(captchaText);
    try (ServletOutputStream output = response.getOutputStream()) {
        ImageIO.write(captchaImage, "jpeg", output);
    }
}

Custom Authentication Provider

public class CaptchaAuthenticationProvider extends DaoAuthenticationProvider {
    @Override
    public Authentication authenticate(Authentication authRequest) throws AuthenticationException {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        String storedCaptcha = (String) request.getSession().getAttribute("CAPTCHA_VALUE");
        String inputCaptcha = request.getParameter("captcha_code");
        
        if (!StringUtils.equals(storedCaptcha, inputCaptcha)) {
            throw new AuthenticationServiceException("Invalid CAPTCHA");
        }
        return super.authenticate(authRequest);
    }
}

Login Page Template


<html>
<head>
    <meta charset="UTF-8">
    <title>Login</title>
</head>
<body>
<div th:text="${SPRING_SECURITY_LAST_EXCEPTION}"></div>
<form action="/login" method="post">
    Username: <input name="username" type="text"><br>
    Password: <input name="password" type="password"><br>
    CAPTCHA: <input name="captcha_code" type="text"><br>
    <img src="/captcha">
    <button type="submit">Sign In</button>
</form>
</body>
</html>

Security Filter Chain Configuration

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http.authorizeHttpRequests(authz -> authz
            .antMatchers("/captcha").permitAll()
            .anyRequest().authenticated())
        .formLogin(form -> form
            .loginPage("/login.html")
            .loginProcessingUrl("/login")
            .failureForwardUrl("/login.html")
            .permitAll())
        .csrf().disable();
    return http.build();
}

Authentication Manager Setup

@Bean
public UserDetailsService userService(){
    UserDetails user = User.withUsername("testuser").password("{noop}testpass").roles("USER").build();
    return new InMemoryUserDetailsManager(user);
}

@Bean
public CaptchaAuthenticationProvider captchaAuthProvider(){
    CaptchaAuthenticationProvider provider = new CaptchaAuthenticationProvider();
    provider.setUserDetailsService(userService());
    return provider;
}

@Bean
public AuthenticationManager authManager(){
    return new ProviderManager(captchaAuthProvider());
}

Tags: spring-boot spring-security Authentication MyBatis JDBC

Posted on Wed, 27 May 2026 16:36:19 +0000 by NickTyson