Spring Security protects servlet applications through filters that run before the controller. Authentication establishes who is making the request; authorization decides whether that identity may perform the requested action. In current Spring Security, SecurityFilterChain defines the web rules and AuthorizationManager is the central authorization contract.
This lesson assumes basic Spring MVC knowledge. After it, you can read a filter-chain configuration in request order, distinguish 401 from 403 behavior, protect routes with least-privilege rules, and test access for anonymous and authenticated users.
Spring Security is a powerful and highly customizable authentication and access-control framework for Java applications. It is the de-facto standard for securing Spring-based applications. It provides:
package com.example.config;
import org.springframework.context.annotation.*;
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.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// Authorization rules
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/home", "/register", "/css/**", "/js/**").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers("/api/**").hasAnyRole("USER", "ADMIN")
.anyRequest().authenticated()
)
// Form login
.formLogin(form -> form
.loginPage("/login")
.loginProcessingUrl("/login")
.defaultSuccessUrl("/dashboard", true)
.failureUrl("/login?error=true")
.permitAll()
)
// Logout
.logout(logout -> logout
.logoutUrl("/logout")
.logoutSuccessUrl("/login?logout=true")
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID")
.permitAll()
)
// HTTP Basic auth (for REST APIs)
// .httpBasic(Customizer.withDefaults())
// CSRF protection (disable for REST APIs)
// .csrf(csrf -> csrf.disable())
;
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12); // Strength 12
}
}
package com.example.security;
import com.example.entity.User;
import com.example.repository.UserRepository;
import org.springframework.security.core.*;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.*;
import org.springframework.stereotype.Service;
import java.util.*;
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
private final UserRepository userRepository;
public UserDetailsServiceImpl(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found: " + username));
// Convert roles to GrantedAuthority
List<GrantedAuthority> authorities = List.of(
new SimpleGrantedAuthority("ROLE_" + user.getRole().name())
);
return new org.springframework.security.core.userdetails.User(
user.getUsername(),
user.getPassword(), // BCrypt hashed password
authorities
);
}
}
// Registration service - hash password before saving
@Service
public class RegistrationService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
public RegistrationService(UserRepository userRepository, PasswordEncoder passwordEncoder) {
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
}
public User register(String username, String email, String rawPassword) {
User user = new User();
user.setUsername(username);
user.setEmail(email);
user.setPassword(passwordEncoder.encode(rawPassword)); // Hash!
user.setRole(User.Role.USER);
return userRepository.save(user);
}
}
// Enable method security in config:
// @EnableMethodSecurity(prePostEnabled = true)
@Service
public class AdminService {
// Only ADMIN role can call this
@PreAuthorize("hasRole('ADMIN')")
public List<User> getAllUsers() {
return userRepository.findAll();
}
// User can only access their own data
@PreAuthorize("hasRole('ADMIN') or #username == authentication.name")
public User getUserProfile(String username) {
return userRepository.findByUsername(username).orElseThrow();
}
// Check after method returns
@PostAuthorize("returnObject.username == authentication.name or hasRole('ADMIN')")
public User getUser(Long id) {
return userRepository.findById(id).orElseThrow();
}
// Secure with SpEL expression
@PreAuthorize("hasRole('ADMIN') and #user.role != 'ADMIN'")
public void deleteUser(User user) {
userRepository.delete(user);
}
}
A DelegatingFilterProxy connects the servlet container to Spring Security. FilterChainProxy selects the first SecurityFilterChain whose matcher applies, then invokes its security filters in order. Authentication filters must run before authorization because the authorization decision needs the current SecurityContext.
Write specific request matchers before broad ones and finish with an explicit fallback such as anyRequest().authenticated() or denyAll(). A visible fallback prevents a newly added endpoint from becoming public merely because no rule mentioned it.
Browser applications commonly keep authentication in a session. Because browsers automatically attach session cookies, a forged cross-site request can submit an unwanted state change; CSRF protection addresses that risk with a token. Do not disable CSRF simply to make a form or test pass. Configure the client to send the token, or justify a stateless non-browser API design.
For stateless bearer-token APIs, configure session creation deliberately and validate tokens on every request. CORS and CSRF solve different problems: CORS controls which browser origins may read or send cross-origin requests, while CSRF protects authenticated state-changing actions from forged browser requests.
A 401 response means usable authentication is missing or invalid. A 403 means the request reached an authorization decision but access was denied, or a required CSRF token was absent. Enable focused Spring Security logging in development and inspect which chain matched, which authentication was created, and which authorization rule denied the request.
Use spring-security-test with MockMvc to state the caller explicitly. Test anonymous access, a permitted role, a forbidden role, and CSRF behavior for writes. These tests protect the route contract when mappings or rules change.
Try this next
0 of 2 completed
Explore 500+ free tutorials across 20+ languages and frameworks.