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:
- Create a custom authentication provider extending DaoAuthenticationProvider
- Implement CAPTCHA validation logic
- Retrieve stored CAPTCHA from session
- Extract user input from request parameters
- Compare values and proceed with authentication if valid
- Throw exception for invalid CAPTCHA
- 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());
}