Spring Boot
Spring Security Basics
Spring Security is the standard security framework for Spring Boot applications. It provides authentication (verifying who a user is), authorisation (verifying what they can do), protection against common web attacks (CSRF, session fixation, clickjacking), and integrations with OAuth2, JWT, LDAP, and more. Adding spring-boot-starter-security to the classpath immediately secures every endpoint — understanding its architecture is essential before customising it.
What Spring Security Does Out of the Box
Adding spring-boot-starter-security immediately activates a default security configuration. Every endpoint is protected. A login form is provided. A random password is printed to the console. HTTP Basic authentication is enabled. CSRF protection is active. Understanding what the default configuration does — and what it does not do — is the starting point for any customisation.
XML
<!-- pom.xml: -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
# ── What the default auto-configuration provides: ────────────────────
# 1. Every URL requires authentication (HTTP 401 if unauthenticated)
# 2. A default login form at /login
# 3. A default logout endpoint at /logout
# 4. HTTP Basic authentication enabled
# 5. A single in-memory user: username "user",
# password printed to console on startup:
# "Using generated security password: 3a7e9b2f-..."
# 6. CSRF protection enabled for all state-changing requests
# 7. Session fixation protection
# 8. Security headers: X-Content-Type-Options, X-Frame-Options,
# X-XSS-Protection, Cache-Control
# ── Console output when auto-configuration is active: ─────────────────
# Using generated security password: 8f1e2a4c-9b3d-4e7f-a1c2-5d8e9f0b1c2d
# This will be ignored in any bean of type UserDetailsService.
# ── Override the default username/password in application.yml: ────────
spring:
security:
user:
name: admin
password: secret
roles: ADMIN
# Only for development — never hardcode credentials in production.Spring Security Architecture
Spring Security is built on a chain of servlet filters. Every HTTP request passes through the SecurityFilterChain before reaching any controller. The chain contains specialised filters for authentication, authorisation, session management, CSRF protection, and header injection.
Shell
# ── Request processing flow: ─────────────────────────────────────────
#
# HTTP Request
# │
# ▼
# DelegatingFilterProxy (bridges Servlet filters to Spring beans)
# │
# ▼
# FilterChainProxy (holds all SecurityFilterChains)
# │
# ▼
# SecurityFilterChain (ordered list of filters for this request)
# │
# ├── DisableEncodeUrlFilter
# ├── WebAsyncManagerIntegrationFilter
# ├── SecurityContextHolderFilter ← loads SecurityContext from session
# ├── HeaderWriterFilter ← adds security headers
# ├── CsrfFilter ← validates CSRF tokens
# ├── LogoutFilter ← handles /logout
# ├── UsernamePasswordAuthenticationFilter ← handles form login POST
# ├── BasicAuthenticationFilter ← handles HTTP Basic
# ├── RequestCacheAwareFilter
# ├── SecurityContextHolderAwareFilter
# ├── AnonymousAuthenticationFilter ← sets anonymous auth if none found
# ├── SessionManagementFilter
# ├── ExceptionTranslationFilter ← translates exceptions to HTTP 401/403
# └── AuthorizationFilter ← enforces access rules
# │
# ▼
# DispatcherServlet → @Controller
# ── SecurityContext — holds the authenticated user: ───────────────────
# SecurityContextHolder stores a SecurityContext per thread.
# SecurityContext holds an Authentication object.
# Authentication holds the principal (UserDetails) and granted authorities.
// Access the current user anywhere:
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
String username = auth.getName();
Collection<? extends GrantedAuthority> authorities = auth.getAuthorities();
UserDetails user = (UserDetails) auth.getPrincipal();SecurityFilterChain Configuration
The SecurityFilterChain bean replaces the deprecated WebSecurityConfigurerAdapter. Define it in a @Configuration class to customise which URLs require authentication, which HTTP methods are allowed, and how authentication works.
Java
@Configuration
@EnableWebSecurity
public class SecurityConfig {
// ── Basic REST API security configuration: ────────────────────────
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// ── URL authorisation rules (evaluated top to bottom): ────
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.requestMatchers("/api/users/**").hasAnyRole("USER", "ADMIN")
.requestMatchers(HttpMethod.GET, "/api/products/**").permitAll()
.requestMatchers(HttpMethod.POST, "/api/products/**").hasRole("ADMIN")
.requestMatchers("/actuator/health").permitAll()
.requestMatchers("/actuator/**").hasRole("ADMIN")
.anyRequest().authenticated() // everything else requires auth
)
// ── HTTP Basic — for REST API clients: ───────────────────
.httpBasic(Customizer.withDefaults())
// ── Form login — for browser-based apps: ─────────────────
// .formLogin(form -> form
// .loginPage("/login")
// .permitAll()
// )
// ── Session management — stateless for REST APIs: ─────────
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
// ── CSRF — disable for stateless REST APIs: ───────────────
// CSRF is only needed for cookie/session-based auth.
// Stateless APIs using JWT or HTTP Basic don't need it.
.csrf(csrf -> csrf.disable());
return http.build();
}
// ── MVC matchers for static resources (exclude from security): ────
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return web -> web.ignoring()
.requestMatchers("/static/**", "/css/**", "/js/**", "/images/**");
}
}UserDetailsService and UserDetails
UserDetailsService is the interface Spring Security calls to load a user by username during authentication. Implement it to load users from a database, LDAP, or any other source. UserDetails is the interface that represents a loaded user — it carries the username, password, enabled flag, and granted authorities.
Java
// ── UserDetails implementation: ──────────────────────────────────────
@Entity
@Table(name = "users")
public class User implements UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String username;
@Column(nullable = false)
private String password; // must be encoded (BCrypt etc.)
@Column(nullable = false)
private boolean enabled = true;
@ElementCollection(fetch = FetchType.EAGER)
@CollectionTable(name = "user_roles",
joinColumns = @JoinColumn(name = "user_id"))
@Column(name = "role")
private Set<String> roles = new HashSet<>();
// ── UserDetails interface: ────────────────────────────────────────
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return roles.stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role))
.toList();
}
@Override public String getPassword() { return password; }
@Override public String getUsername() { return username; }
@Override public boolean isEnabled() { return enabled; }
// These default to true — override if account expiry is needed:
@Override public boolean isAccountNonExpired() { return true; }
@Override public boolean isAccountNonLocked() { return true; }
@Override public boolean isCredentialsNonExpired() { return true; }
}
// ── UserDetailsService implementation: ───────────────────────────────
@Service
@RequiredArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService {
private final UserRepository userRepository;
@Override
@Transactional(readOnly = true)
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
return userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException(
"User not found: " + username));
}
}
// ── Register the UserDetailsService in the security config: ──────────
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final UserDetailsServiceImpl userDetailsService;
private final PasswordEncoder passwordEncoder;
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
@Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(userDetailsService);
provider.setPasswordEncoder(passwordEncoder);
return provider;
}
}Password Encoding
Passwords must never be stored as plain text. Spring Security requires a PasswordEncoder bean to hash passwords on registration and verify them on login. BCryptPasswordEncoder is the standard choice — it incorporates a salt automatically and is deliberately slow to resist brute-force attacks.
Java
// ── Declare the PasswordEncoder bean: ────────────────────────────────
@Configuration
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12);
// 12 = work factor (cost). Default is 10.
// Higher = slower hashing = more brute-force resistant.
// 12 takes ~300ms per hash on modern hardware — acceptable for login.
// Do NOT increase above 14 without benchmarking under your load.
}
}
// ── Hash a password on user registration: ────────────────────────────
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
@Transactional
public User register(RegisterRequest request) {
if (userRepository.existsByUsername(request.username())) {
throw new UsernameAlreadyExistsException(request.username());
}
User user = new User();
user.setUsername(request.username());
user.setPassword(passwordEncoder.encode(request.password()));
// encode() generates a new random salt each time —
// two calls with the same password produce different hashes.
user.setRoles(Set.of("USER"));
return userRepository.save(user);
}
}
// ── Verify a password manually (e.g. change password flow): ──────────
public void changePassword(Long userId, ChangePasswordRequest request) {
User user = userRepository.findById(userId).orElseThrow();
if (!passwordEncoder.matches(request.currentPassword(), user.getPassword())) {
throw new BadCredentialsException("Current password is incorrect");
}
user.setPassword(passwordEncoder.encode(request.newPassword()));
userRepository.save(user);
}
// ── DelegatingPasswordEncoder — supports multiple encodings: ──────────
// Useful when migrating from an old hash algorithm to BCrypt:
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
// Stores hashes as: {bcrypt}$2a$10$...
// Can verify: {bcrypt}, {sha256}, {md5}, {noop} (plain text — dev only)
}Method-Level Security
@EnableMethodSecurity activates annotations on service methods that enforce authorisation independently of URL rules. This is the correct place for fine-grained access control — checking whether the authenticated user owns the resource or has the right role.
Java
// ── Enable method security: ──────────────────────────────────────────
@Configuration
@EnableMethodSecurity(
prePostEnabled = true, // @PreAuthorize, @PostAuthorize (default: true)
securedEnabled = true, // @Secured
jsr250Enabled = true // @RolesAllowed (JSR-250)
)
public class SecurityConfig { }
// ── @PreAuthorize — checked before the method executes: ───────────────
@Service
public class UserService {
// Only ADMIN can list all users:
@PreAuthorize("hasRole('ADMIN')")
public List<UserResponse> findAll() { ... }
// User can view their own profile; ADMIN can view any:
@PreAuthorize("hasRole('ADMIN') or #id == authentication.principal.id")
public UserResponse findById(Long id) { ... }
// Check that the email does not belong to someone else:
@PreAuthorize("hasRole('ADMIN') or #request.email == authentication.name")
public UserResponse update(Long id, UpdateUserRequest request) { ... }
// Requires authenticated user (any role):
@PreAuthorize("isAuthenticated()")
public UserResponse getProfile() { ... }
}
// ── @PostAuthorize — checked after the method returns: ────────────────
@PostAuthorize("returnObject.username == authentication.name or hasRole('ADMIN')")
public UserResponse findById(Long id) {
return UserResponse.from(userRepository.findById(id).orElseThrow());
// The returned object is checked — method executes first, then access verified.
// Use sparingly — the method body runs even if access is later denied.
}
// ── @Secured — simpler role-based check: ─────────────────────────────
@Secured({"ROLE_ADMIN", "ROLE_MODERATOR"})
public void deletePost(Long id) { ... }
// ── @RolesAllowed (JSR-250): ──────────────────────────────────────────
@RolesAllowed("ADMIN")
public void purgeInactiveUsers() { ... }
// ── Accessing the current user in a method: ───────────────────────────
@GetMapping("/me")
public UserResponse getMe(
@AuthenticationPrincipal UserDetails currentUser) {
// @AuthenticationPrincipal injects the authenticated principal directly
return userService.findByUsername(currentUser.getUsername());
}In-Memory and JDBC User Stores
For development, tests, and simple applications, Spring Security provides in-memory and JDBC user stores without requiring a custom UserDetailsService implementation.
Java
// ── In-memory user store (dev and testing only): ─────────────────────
@Bean
public UserDetailsService inMemoryUserDetailsService(
PasswordEncoder passwordEncoder) {
UserDetails admin = User.builder()
.username("admin")
.password(passwordEncoder.encode("admin123"))
.roles("ADMIN", "USER")
.build();
UserDetails user = User.builder()
.username("user")
.password(passwordEncoder.encode("user123"))
.roles("USER")
.build();
return new InMemoryUserDetailsManager(admin, user);
}
// ── JDBC user store (built-in schema): ───────────────────────────────
// Spring Security expects these tables by default:
// CREATE TABLE users (username VARCHAR(50) PRIMARY KEY,
// password VARCHAR(500) NOT NULL,
// enabled BOOLEAN NOT NULL);
// CREATE TABLE authorities (username VARCHAR(50) NOT NULL,
// authority VARCHAR(50) NOT NULL,
// FOREIGN KEY (username) REFERENCES users(username));
@Bean
public UserDetailsService jdbcUserDetailsService(DataSource dataSource) {
JdbcUserDetailsManager manager = new JdbcUserDetailsManager(dataSource);
// Uses Spring Security's default schema queries.
// Override queries for custom table structure:
manager.setUsersByUsernameQuery(
"SELECT username, password, enabled FROM app_users WHERE username = ?");
manager.setAuthoritiesByUsernameQuery(
"SELECT username, authority FROM app_roles WHERE username = ?");
return manager;
}
// ── LDAP user store: ──────────────────────────────────────────────────
@Configuration
public class SecurityConfig {
@Bean
public EmbeddedLdapServerContextSourceFactoryBean contextSourceFactoryBean() {
EmbeddedLdapServerContextSourceFactoryBean factory =
EmbeddedLdapServerContextSourceFactoryBean.fromEmbeddedLdapServer();
factory.setPort(0);
return factory;
}
// Full LDAP integration: spring-boot-starter-data-ldap + LdapAuthoritiesPopulator
}Common Security Configurations
Complete, ready-to-use security configurations for the two most common Spring Boot application types — a stateless REST API and a server-side rendered web application.
Java
// ── REST API security (stateless, JWT or HTTP Basic): ────────────────
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class RestApiSecurityConfig {
private final UserDetailsServiceImpl userDetailsService;
@Bean
public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/actuator/health").permitAll()
.anyRequest().authenticated())
.httpBasic(Customizer.withDefaults())
.authenticationProvider(authenticationProvider());
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12);
}
@Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(userDetailsService);
provider.setPasswordEncoder(passwordEncoder());
return provider;
}
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
}
// ── Server-side web app security (form login, sessions, CSRF): ────────
@Configuration
@EnableWebSecurity
public class WebAppSecurityConfig {
@Bean
public SecurityFilterChain webFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/home", "/css/**", "/js/**").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated())
.formLogin(form -> form
.loginPage("/login")
.loginProcessingUrl("/login")
.defaultSuccessUrl("/dashboard", true)
.failureUrl("/login?error=true")
.permitAll())
.logout(logout -> logout
.logoutUrl("/logout")
.logoutSuccessUrl("/login?logout=true")
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID")
.permitAll())
.rememberMe(remember -> remember
.key("uniqueAndSecretKey")
.tokenValiditySeconds(86400))
.sessionManagement(session -> session
.sessionFixation().migrateSession()
.maximumSessions(1)
.maxSessionsPreventsLogin(false));
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12);
}
}