Introduction to Spring Security
Spring Security is a powerful and highly customizable authentication and access-control framework. It is the de facto standard for securing Spring-based applications and offers comprehensive security services for Java EE-based enterprise software applications.
Spring Security addresses two primary concerns:
- Authentication - Verifying that a user is who they claim to be
- Authorization - Deciding if a user is allowed to perform a requested action
Beyond these core features, Spring Security provides protection against common security vulnerabilities such as CSRF attacks, session fixation, clickjacking, and more.
Why Spring Security?
Spring Security offers several advantages over implementing security mechanisms manually:
- Comprehensive security framework with proven protection against common attacks
- Highly customizable to fit various application requirements
- Seamless integration with Spring ecosystem
- Active community support and regular updates
- Support for various authentication protocols and providers
Core Concepts
Authentication
Authentication is the process of verifying that users are who they claim to be. Spring Security supports various authentication mechanisms:
- Username/password authentication
- OAuth 2.0 / OpenID Connect
- LDAP authentication
- JWT (JSON Web Token) based authentication
- Custom authentication methods
The central interface for authentication is AuthenticationManager
, which has a single method:
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
}
The most common implementation is ProviderManager
, which delegates to a chain of AuthenticationProvider
instances.
Filter Chain
Spring Security uses a chain of servlet filters to provide various security services. Each filter has a specific responsibility and can be enabled, disabled, or customized as needed.
Some key filters include:
SecurityContextPersistenceFilter
- Stores the SecurityContext between requestsUsernamePasswordAuthenticationFilter
- Processes form login authenticationBasicAuthenticationFilter
- Processes HTTP Basic authenticationRememberMeAuthenticationFilter
- Handles remember-me authenticationFilterSecurityInterceptor
- Makes access control decisions
The filter chain processes each request sequentially:
Security Context
The SecurityContext
holds authentication information for the current thread of execution. It can be accessed throughout your application:
SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
String username = authentication.getName();
Object principal = authentication.getPrincipal();
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
By default, the SecurityContextHolder
uses a ThreadLocal
to store the context, making it available to all methods executed in the same thread.
Getting Started with Spring Security
Adding Dependencies
To use Spring Security in your Spring Boot application, add the following dependency to your pom.xml
(Maven):
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
Or in your build.gradle
(Gradle):
implementation 'org.springframework.boot:spring-boot-starter-security'
Auto-Configuration
Spring Boot's auto-configuration will automatically set up basic security with form-based login when the Spring Security dependency is added.
Basic Configuration
Create a security configuration class:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authz) -> authz
.requestMatchers("/", "/home").permitAll()
.anyRequest().authenticated()
)
.formLogin((form) -> form
.loginPage("/login")
.permitAll()
)
.logout((logout) -> logout.permitAll());
return http.build();
}
@Bean
public UserDetailsService userDetailsService() {
UserDetails user = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("USER")
.build();
return new InMemoryUserDetailsManager(user);
}
}
Note:
Using withDefaultPasswordEncoder()
is not recommended for production! It's only shown here for simplicity. For production, use a proper password encoder like BCryptPasswordEncoder
.
Your First Secure Application
Let's build a simple secure application with a protected page and a login form.
Controller
@Controller
public class MainController {
@GetMapping("/")
public String home() {
return "home";
}
@GetMapping("/hello")
public String hello() {
return "hello";
}
@GetMapping("/login")
public String login() {
return "login";
}
}
Login Template (Thymeleaf)
<!-- login.html -->
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org">
<head>
<title>Login</title>
</head>
<body>
<h1>Login</h1>
<div th:if="${param.error}">
Invalid username and password.
</div>
<div th:if="${param.logout}">
You have been logged out.
</div>
<form th:action="@{/login}" method="post">
<div>
<label>Username: <input type="text" name="username"/></label>
</div>
<div>
<label>Password: <input type="password" name="password"/></label>
</div>
<div>
<input type="submit" value="Sign In"/>
</div>
</form>
</body>
</html>
With this setup, the home page is publicly accessible, but the /hello
page requires authentication.
Authentication Mechanisms
Form Login
Form login is the most common authentication mechanism for web applications.
http
.formLogin(form -> form
.loginPage("/login")
.loginProcessingUrl("/perform_login")
.defaultSuccessUrl("/home", true)
.failureUrl("/login?error=true")
.usernameParameter("username")
.passwordParameter("password")
);
Key configuration options:
loginPage
- Custom login page URLloginProcessingUrl
- URL to submit the username and passworddefaultSuccessUrl
- Where to redirect after successful loginfailureUrl
- Where to redirect after failed loginusernameParameter
andpasswordParameter
- Custom parameter names
HTTP Basic Authentication
HTTP Basic is useful for API authentication or simple applications.
http
.httpBasic(basic -> basic
.realmName("My Application")
);
HTTP Basic sends credentials with each request, typically in an Authorization header:
Authorization: Basic dXNlcjpwYXNzd29yZA==
Security Note:
HTTP Basic sends credentials encoded (not encrypted). Always use HTTPS when using HTTP Basic authentication.
OAuth 2.0 and OpenID Connect
Spring Security provides comprehensive support for OAuth 2.0 and OpenID Connect.
OAuth 2.0 Client
Add the dependency:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
Configure OAuth providers in application.properties
:
spring.security.oauth2.client.registration.github.client-id=your-github-client-id
spring.security.oauth2.client.registration.github.client-secret=your-github-client-secret
spring.security.oauth2.client.registration.google.client-id=your-google-client-id
spring.security.oauth2.client.registration.google.client-secret=your-google-client-secret
Configure the security:
http
.oauth2Login(oauth2 -> oauth2
.loginPage("/login")
.defaultSuccessUrl("/home")
.failureUrl("/login?error=true")
);
JWT Authentication
JSON Web Tokens (JWT) are commonly used for stateless authentication in RESTful APIs.
Add the dependency:
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
Create JWT utility class:
@Component
public class JwtTokenUtil {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration}")
private Long expiration;
public String generateToken(UserDetails userDetails) {
Map claims = new HashMap<>();
return createToken(claims, userDetails.getUsername());
}
private String createToken(Map claims, String subject) {
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + expiration * 1000))
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
public Boolean validateToken(String token, UserDetails userDetails) {
final String username = getUsernameFromToken(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
public String getUsernameFromToken(String token) {
return getClaimFromToken(token, Claims::getSubject);
}
private T getClaimFromToken(String token, Function claimsResolver) {
final Claims claims = getAllClaimsFromToken(token);
return claimsResolver.apply(claims);
}
private Claims getAllClaimsFromToken(String token) {
return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
}
private Boolean isTokenExpired(String token) {
final Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
}
public Date getExpirationDateFromToken(String token) {
return getClaimFromToken(token, Claims::getExpiration);
}
}
Create JWT filter:
public class JwtRequestFilter extends OncePerRequestFilter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
final String requestTokenHeader = request.getHeader("Authorization");
String username = null;
String jwtToken = null;
// JWT Token is in the form "Bearer token"
if (requestTokenHeader != null && requestTokenHeader.startsWith("Bearer ")) {
jwtToken = requestTokenHeader.substring(7);
try {
username = jwtTokenUtil.getUsernameFromToken(jwtToken);
} catch (IllegalArgumentException e) {
System.out.println("Unable to get JWT Token");
} catch (ExpiredJwtException e) {
System.out.println("JWT Token has expired");
}
} else {
logger.warn("JWT Token does not begin with Bearer String");
}
// Once we get the token validate it
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
// if token is valid configure Spring Security to manually set authentication
if (jwtTokenUtil.validateToken(jwtToken, userDetails)) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// After setting the Authentication in the context, we specify
// that the current user is authenticated
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
chain.doFilter(request, response);
}
}
Configure JWT in Spring Security:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
@Autowired
private UserDetailsService jwtUserDetailsService;
@Autowired
private JwtRequestFilter jwtRequestFilter;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(authz -> authz
.requestMatchers("/authenticate").permitAll()
.anyRequest().authenticated()
)
.exceptionHandling(ex -> ex.authenticationEntryPoint(jwtAuthenticationEntryPoint))
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
// Add JWT filter
http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {
return authConfig.getAuthenticationManager();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
Create an authentication controller:
@RestController
public class AuthenticationController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Autowired
private UserDetailsService userDetailsService;
@PostMapping("/authenticate")
public ResponseEntity> createAuthenticationToken(@RequestBody AuthenticationRequest authenticationRequest) throws Exception {
try {
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
authenticationRequest.getUsername(),
authenticationRequest.getPassword()
)
);
} catch (BadCredentialsException e) {
throw new Exception("Incorrect username or password", e);
}
final UserDetails userDetails = userDetailsService
.loadUserByUsername(authenticationRequest.getUsername());
final String token = jwtTokenUtil.generateToken(userDetails);
return ResponseEntity.ok(new AuthenticationResponse(token));
}
}
JWT Benefits
JWT offers several advantages for API authentication:
- Stateless - no need to store session information on the server
- Portable - the same token can be used across multiple backends
- Self-contained - contains all necessary user information
- Efficient - reduces database lookups for user information
Advanced Topics
CSRF Protection
Cross-Site Request Forgery (CSRF) is an attack that forces users to execute unwanted actions on a web application they're currently authenticated to.
Spring Security enables CSRF protection by default for HTML form submissions. To configure it:
http
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
);
To disable CSRF protection (for example, for RESTful APIs using token-based authentication):
http
.csrf(csrf -> csrf.disable());
With Thymeleaf, CSRF tokens are automatically included in forms:
<form th:action="@{/process}" method="post">
<!-- CSRF token automatically included by Thymeleaf -->
<input type="text" name="username" />
<button type="submit">Submit</button>
</form>
For other template engines or JavaScript applications, you need to manually include the CSRF token:
<form action="/process" method="post">
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}" />
<input type="text" name="username" />
<button type="submit">Submit</button>
</form>
For AJAX requests, you can include the token in a header:
const token = document.querySelector('meta[name="_csrf"]').content;
const header = document.querySelector('meta[name="_csrf_header"]').content;
fetch('/api/data', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
[header]: token
},
body: JSON.stringify({ key: 'value' })
});
CORS Configuration
Cross-Origin Resource Sharing (CORS) is a mechanism that allows restricted resources on a web page to be requested from another domain outside the domain from which the first resource was served.
Configure CORS in Spring Security:
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("https://example.com"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE"));
configuration.setAllowedHeaders(Arrays.asList("Authorization", "Content-Type"));
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
// other configuration
;
return http.build();
}
For a simpler configuration with Spring Boot, you can use @CrossOrigin
annotation:
@RestController
@CrossOrigin(origins = "https://example.com")
public class UserController {
@GetMapping("/users")
public List getUsers() {
// ...
}
}
Password Encoding
Storing passwords securely is crucial for application security. Spring Security provides several password encoders:
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
Available password encoders:
BCryptPasswordEncoder
- Uses the bcrypt strong hashing functionPbkdf2PasswordEncoder
- Uses PBKDF2 algorithmSCryptPasswordEncoder
- Uses scrypt algorithmArgon2PasswordEncoder
- Uses Argon2 algorithmDelegatingPasswordEncoder
- Delegates to other encoders based on a prefix
Using the delegating password encoder (recommended):
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
This encoder prefixes passwords with the encoder ID, allowing you to upgrade your encoding strategy while still supporting existing passwords:
// Passwords in the database might look like:
// {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
// {pbkdf2}e4e76f1a012340a3...
// {scrypt}$e0801$8bWJaSu2...
// To encode a new password:
String encoded = passwordEncoder.encode("myPassword");
// Result: {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
// To match a password:
boolean matches = passwordEncoder.matches("myPassword", encoded);
// Result: true
Remember Me Authentication
Remember Me functionality allows users to stay logged in between browser sessions.
http
.rememberMe(remember -> remember
.key("uniqueAndSecretKey")
.tokenValiditySeconds(86400) // 1 day
);
For enhanced security, use persistent token storage:
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource);
// tokenRepository.setCreateTableOnStartup(true); // Uncomment for first run
return tokenRepository;
}
http
.rememberMe(remember -> remember
.tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(86400) // 1 day
);
Include the remember-me option in the login form:
<form th:action="@{/login}" method="post">
<input type="text" name="username" />
<input type="password" name="password" />
<label>
<input type="checkbox" name="remember-me" /> Remember me
</label>
<button type="submit">Login</button>
</form>
Session Management
Configure session management to control how Spring Security uses sessions:
http
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.invalidSessionUrl("/invalid-session")
.maximumSessions(1)
.maxSessionsPreventsLogin(true)
.expiredUrl("/session-expired")
);
Session creation policies:
ALWAYS
- Always create a sessionNEVER
- Never create a session, but use existing onesIF_REQUIRED
- Only create a session if needed (default)STATELESS
- Don't create or use sessions (for REST APIs)
Concurrent session control:
maximumSessions
- Maximum concurrent sessions per usermaxSessionsPreventsLogin
- If true, prevents new logins when max sessions reachedexpiredUrl
- Where to redirect when a session expires
Session fixation protection:
http
.sessionManagement(session -> session
.sessionFixation().migrateSession()
);
Session fixation protection strategies:
migrateSession
- Create a new session and copy attributes (default)newSession
- Create a new session without copying attributesnone
- Do nothing (not recommended)
Best Practices
Security Checklist
- Use HTTPS for all authenticated pages
- Use strong password encoding (BCrypt, Argon2, PBKDF2, or SCrypt)
- Implement proper CSRF protection
- Configure CORS correctly for APIs
- Set secure and HttpOnly flags on cookies
- Implement proper session management
- Use Content Security Policy headers
- Validate and sanitize all user input
- Implement proper error handling (don't expose sensitive information)
- Use principle of least privilege for authorization
Security Headers
Configure security headers to protect against common attacks:
http
.headers(headers -> headers
.contentSecurityPolicy(csp -> csp.policyDirectives("default-src 'self'"))
.frameOptions(frame -> frame.deny())
.xssProtection(xss -> xss.block(true))
.contentTypeOptions(content -> content.disable())
);
Authentication Best Practices
- Implement rate limiting for login attempts
- Use multi-factor authentication for sensitive applications
- Create custom authentication success/failure handlers
- Log authentication events
- Implement account lockout policies
@Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(userDetailsService);
provider.setPasswordEncoder(passwordEncoder());
provider.setHideUserNotFoundExceptions(false); // For debugging only
return provider;
}
http
.formLogin(form -> form
.successHandler(new CustomAuthenticationSuccessHandler())
.failureHandler(new CustomAuthenticationFailureHandler())
);
Authorization Best Practices
- Use method security for fine-grained access control
- Implement custom permission evaluators for complex scenarios
- Don't hardcode roles and authorities
- Follow the principle of least privilege
OAuth 2.0 Best Practices
- Use authorization code flow with PKCE for web applications
- Validate redirect URIs
- Use short-lived access tokens and refresh tokens
- Implement proper scope management
- Validate JWT claims (issuer, audience, expiration)
Additional Resources
Official Documentation
- Spring Security Reference
- Spring Security Servlet Applications
- Spring Security Reactive Applications
- Spring Security Features
Books
- "Spring Security in Action" by Laurentiu Spilca
- "Spring Boot in Action" by Craig Walls
- "Hands-On Spring Security 5 for Reactive Applications" by Tomcy John
Tutorials and Courses
- Spring Security Guides
- Baeldung Spring Security Series
- Spring Security on Pluralsight
- Spring Academy courses