What is Spring Security?
Imagine your application is a building. Spring Security is the comprehensive security system - the guards at the entrance, the key cards for different floors, the cameras, and the access logs. It controls who gets in (authentication) and what they can do inside (authorization).
Spring Security is the de facto standard for securing Spring-based applications. It handles authentication (verifying identity), authorization (checking permissions), protection against common attacks, and secure session management.
Why Security Matters
In 2024, the average cost of a data breach was $4.45 million. Security isn't optional - it's essential. Every application that stores user data, processes payments, or handles sensitive information needs robust security.
Comprehensive Protection
Spring Security protects against CSRF, session fixation, clickjacking, SQL injection, and other common attacks automatically.
Flexible Authentication
Support for username/password, JWT tokens, OAuth2, LDAP, database authentication, and more - all configurable.
Fine-Grained Authorization
Control access at method level, URL level, or even field level. Different users see different data.
Industry Standard
Trusted by banks, healthcare systems, and government applications worldwide.
Core Security Concepts
Authentication vs Authorization
These terms are often confused. Here's the difference:
Authentication: "Who are you?" - Proving your identity - Like showing your passport at airport security - Example: Login with username and password Authorization: "What can you do?" - Checking your permissions - Like checking if your ticket is for business class or economy - Example: Only admins can delete users // Real-world example User logs in with email/password → Authentication System checks if user has ADMIN role → Authorization
Principal, Credentials, and Authorities
Principal: The user (their identity) - Example: username, email, user ID Credentials: Proof of identity - Example: password, token, fingerprint Authorities: Permissions/roles - Example: ROLE_ADMIN, ROLE_USER, permission:delete
Setting Up Spring Security
Step 1: Add Dependency
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
// Just adding this dependency automatically secures ALL endpoints!
// Default behavior:
// - All URLs require authentication
// - Form login is enabled
// - HTTP Basic authentication is enabled
// - Default user: "user"
// - Password: printed in console logs
Step 2: Basic Configuration
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll() // Allow everyone
.requestMatchers("/admin/**").hasRole("ADMIN") // Only admins
.anyRequest().authenticated() // All other URLs need login
)
.formLogin(form -> form
.loginPage("/login") // Custom login page
.defaultSuccessUrl("/dashboard") // After successful login
.permitAll()
)
.logout(logout -> logout
.logoutSuccessUrl("/") // After logout
.permitAll()
);
return http.build();
}
}
Step 3: User Details Service
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
// Load user from database
User user = userRepository.findByEmail(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
// Convert to Spring Security's UserDetails
return org.springframework.security.core.userdetails.User
.withUsername(user.getEmail())
.password(user.getPassword()) // Must be encrypted!
.roles(user.getRoles()) // ADMIN, USER, etc.
.accountExpired(!user.isActive())
.build();
}
}
Password Encoding: Never Store Plain Text
Storing passwords in plain text is like leaving your house key under the doormat - everyone knows to look there. Always hash passwords using a strong algorithm.
@Configuration
public class SecurityConfig {
// Use BCrypt - industry standard password hashing
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
@Service
public class UserService {
@Autowired
private PasswordEncoder passwordEncoder;
public User registerUser(String email, String password) {
User user = new User();
user.setEmail(email);
// Hash password before saving
user.setPassword(passwordEncoder.encode(password));
return userRepository.save(user);
}
}
// When user logs in:
// 1. Spring Security loads user from database (hashed password)
// 2. Takes provided password, hashes it
// 3. Compares hashes
// 4. If match → authenticated
// Example:
// User registers with password: "myPassword123"
// Stored in DB: "$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy"
// User logs in with: "myPassword123"
// BCrypt hashes it → gets same hash → authenticated!
Password Encoding Best Practices
- BCrypt - Default choice, adaptive (gets stronger over time)
- Argon2 - More modern, resistant to GPU attacks
- NEVER - Plain text, MD5, SHA1 (all insecure)
JWT (JSON Web Tokens): Stateless Authentication
JWT is like a digital passport. Instead of the server remembering who you are (session), you carry proof of identity with every request.
Why JWT?
- Stateless - Server doesn't store sessions (scales better)
- Mobile-Friendly - Perfect for apps and SPAs
- Microservices - Token works across multiple services
- Cross-Domain - Works with different domains (CORS-friendly)
JWT Structure
// JWT Token Format: xxxxx.yyyyy.zzzzz
Header.Payload.Signature
// Header (Base64 encoded)
{
"alg": "HS256", // Algorithm
"typ": "JWT"
}
// Payload (Base64 encoded) - Your data
{
"sub": "user@example.com", // Subject (user identifier)
"name": "John Doe",
"roles": ["USER", "ADMIN"],
"iat": 1516239022, // Issued at
"exp": 1516242622 // Expiration
}
// Signature (encrypted)
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret_key
)
// Complete JWT:
// eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyQGV4YW1wbGUuY29tIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Implementing JWT
// Add dependency
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
// JWT Utility Class
@Component
public class JwtUtil {
@Value("${jwt.secret}")
private String secret;
private static final long EXPIRATION_TIME = 864_000_000; // 10 days
// Generate token
public String generateToken(String username, List<String> roles) {
return Jwts.builder()
.setSubject(username)
.claim("roles", roles)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
.signWith(SignatureAlgorithm.HS256, secret)
.compact();
}
// Extract username from token
public String extractUsername(String token) {
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody()
.getSubject();
}
// Validate token
public boolean validateToken(String token, UserDetails userDetails) {
String username = extractUsername(token);
return username.equals(userDetails.getUsername())
&& !isTokenExpired(token);
}
private boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
}
// Login Endpoint - Generate JWT
@RestController
@RequestMapping("/api/auth")
public class AuthController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtUtil jwtUtil;
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest request) {
// Authenticate user
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
request.getEmail(),
request.getPassword()
)
);
// Load user details
UserDetails userDetails = userDetailsService
.loadUserByUsername(request.getEmail());
// Generate JWT
String token = jwtUtil.generateToken(
userDetails.getUsername(),
userDetails.getAuthorities()
);
return ResponseEntity.ok(new AuthResponse(token));
}
}
// JWT Filter - Validate token on each request
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) {
String token = extractTokenFromHeader(request);
if (token != null && jwtUtil.validateToken(token)) {
String username = jwtUtil.extractUsername(token);
UserDetails userDetails = userDetailsService
.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities()
);
SecurityContextHolder.getContext()
.setAuthentication(authentication);
}
chain.doFilter(request, response);
}
}
// Client usage:
// 1. Login → receive JWT token
// 2. Store token (localStorage, sessionStorage)
// 3. Send token with each request:
// Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
When to Use JWT?
- REST APIs - Perfect for stateless APIs
- Mobile Apps - Apps can store and send tokens
- Microservices - Share token across services
- Single Page Apps (SPAs) - React, Angular, Vue apps
Avoid JWT for: Traditional web apps with server-side rendering (use sessions), short-lived sessions (JWT can't be revoked easily), storing large amounts of data (tokens can get big).
OAuth2: "Login with Google/Facebook"
OAuth2 is like having a VIP pass that works at multiple venues. Instead of creating accounts everywhere, you use your Google/Facebook/GitHub account to log in.
Why OAuth2?
- User Convenience - No need to remember another password
- Security - Leverage Google/Facebook's security teams
- Trust - Users trust big providers more
- Social Data - Access user's profile, email, etc.
OAuth2 Flow
1. User clicks "Login with Google" 2. Redirect to Google login page 3. User logs in to Google 4. Google asks: "Allow MyApp to access your email and profile?" 5. User clicks "Allow" 6. Google redirects back to your app with authorization code 7. Your app exchanges code for access token 8. Your app uses token to fetch user's profile 9. Your app creates/logs in user // This is all handled by Spring Security OAuth2!
Implementing OAuth2
// Add dependency
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
// application.yml - Configure providers
spring:
security:
oauth2:
client:
registration:
google:
client-id: YOUR_GOOGLE_CLIENT_ID
client-secret: YOUR_GOOGLE_CLIENT_SECRET
scope:
- email
- profile
github:
client-id: YOUR_GITHUB_CLIENT_ID
client-secret: YOUR_GITHUB_CLIENT_SECRET
scope:
- user:email
- read:user
// Security Configuration
@Configuration
@EnableWebSecurity
public class OAuth2SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/login/**").permitAll()
.anyRequest().authenticated()
)
.oauth2Login(oauth2 -> oauth2
.loginPage("/login")
.defaultSuccessUrl("/dashboard")
.failureUrl("/login?error=true")
.userInfoEndpoint(userInfo -> userInfo
.userService(customOAuth2UserService)
)
);
return http.build();
}
}
// Custom OAuth2 User Service
@Service
public class CustomOAuth2UserService
extends DefaultOAuth2UserService {
@Autowired
private UserRepository userRepository;
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) {
OAuth2User oauth2User = super.loadUser(userRequest);
// Extract user information
String email = oauth2User.getAttribute("email");
String name = oauth2User.getAttribute("name");
String provider = userRequest.getClientRegistration().getRegistrationId();
// Create or update user in database
User user = userRepository.findByEmail(email)
.orElseGet(() -> {
User newUser = new User();
newUser.setEmail(email);
newUser.setName(name);
newUser.setProvider(provider);
return userRepository.save(newUser);
});
return oauth2User;
}
}
// HTML Login Page
<!-- Login with Google -->
<a href="/oauth2/authorization/google">
<img src="google-logo.png"> Login with Google
</a>
<!-- Login with GitHub -->
<a href="/oauth2/authorization/github">
<img src="github-logo.png"> Login with GitHub
</a>
// That's it! Spring Security handles the entire OAuth2 flow
When to Use OAuth2?
- Consumer Apps - Apps for general public (e.g., social media, productivity apps)
- Faster Onboarding - Reduce signup friction
- Third-party Access - When you need access to user's Google Drive, Facebook posts, etc.
Don't use OAuth2 for: Internal enterprise apps (use LDAP/Active Directory), Banking/Finance (need direct control), High-security apps (don't want third-party dependency).
Role-Based Access Control (RBAC)
RBAC is like assigning different levels of access in a company - interns, employees, managers, and executives all have different permissions.
Method-Level Security
// Enable method security
@Configuration
@EnableMethodSecurity
public class MethodSecurityConfig {
}
// Protect methods with annotations
@Service
public class UserService {
// Only ADMINs can call this
@PreAuthorize("hasRole('ADMIN')")
public void deleteUser(Long userId) {
userRepository.deleteById(userId);
}
// Only if user owns the resource
@PreAuthorize("@userSecurity.isOwner(#userId)")
public User updateUser(Long userId, User updates) {
User user = userRepository.findById(userId).orElseThrow();
// Update logic
return userRepository.save(user);
}
// Multiple roles
@PreAuthorize("hasAnyRole('ADMIN', 'MANAGER')")
public List<User> getAllUsers() {
return userRepository.findAll();
}
// Custom expression
@PreAuthorize("hasRole('ADMIN') or #user.id == authentication.principal.id")
public User viewProfile(User user) {
return user;
}
// Check after method execution
@PostAuthorize("returnObject.email == authentication.principal.username")
public User getUserById(Long id) {
return userRepository.findById(id).orElseThrow();
}
}
// Custom Security Checker
@Component
public class UserSecurity {
public boolean isOwner(Long userId) {
Authentication auth = SecurityContextHolder
.getContext().getAuthentication();
String currentUsername = auth.getName();
User user = userRepository.findById(userId).orElseThrow();
return user.getEmail().equals(currentUsername);
}
}
URL-Based Security
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
// Public endpoints
.requestMatchers("/", "/home", "/about").permitAll()
.requestMatchers("/api/public/**").permitAll()
// Specific roles
.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers("/manager/**").hasAnyRole("ADMIN", "MANAGER")
// HTTP method based
.requestMatchers(HttpMethod.DELETE, "/api/users/**").hasRole("ADMIN")
.requestMatchers(HttpMethod.POST, "/api/products/**").hasRole("MANAGER")
.requestMatchers(HttpMethod.GET, "/api/**").hasRole("USER")
// Authenticated users
.anyRequest().authenticated()
);
return http.build();
}
Security Best Practices
1. Always Use HTTPS in Production
# application-prod.properties server.ssl.enabled=true server.ssl.key-store=classpath:keystore.p12 server.ssl.key-store-password=yourPassword server.ssl.key-store-type=PKCS12 # Force HTTPS server.require-ssl=true
2. Enable CSRF Protection for Forms
// CSRF is enabled by default for forms
// Disable only for stateless REST APIs with JWT
http.csrf().disable(); // Only for JWT APIs
// For form-based apps, include CSRF token
<form action="/transfer" method="post">
<input type="hidden" name="${_csrf.parameterName}"
value="${_csrf.token}"/>
<!-- form fields -->
</form>
3. Implement Rate Limiting
// Prevent brute force attacks
@Component
public class LoginAttemptService {
private final LoadingCache<String, Integer> attemptsCache;
public LoginAttemptService() {
attemptsCache = CacheBuilder.newBuilder()
.expireAfterWrite(1, TimeUnit.DAYS)
.build(new CacheLoader<String, Integer>() {
public Integer load(String key) {
return 0;
}
});
}
public void loginFailed(String key) {
int attempts = attemptsCache.getUnchecked(key);
attempts++;
attemptsCache.put(key, attempts);
}
public boolean isBlocked(String key) {
return attemptsCache.getUnchecked(key) >= 3;
}
}
4. Use Security Headers
http
.headers(headers -> headers
.contentSecurityPolicy("default-src 'self'")
.xssProtection()
.frameOptions().deny()
.httpStrictTransportSecurity()
);
5. Never Log Sensitive Data
// BAD
log.info("User logged in: " + username + " with password: " + password);
// GOOD
log.info("User logged in: " + username);
// Mask sensitive data in logs
log.info("Processing payment for card: ****" + cardNumber.substring(12));
6. Validate Input
@PostMapping("/users")
public User createUser(@Valid @RequestBody UserDTO userDTO) {
// @Valid triggers validation
return userService.create(userDTO);
}
public class UserDTO {
@NotBlank(message = "Email is required")
@Email(message = "Invalid email format")
private String email;
@Size(min = 8, message = "Password must be at least 8 characters")
@Pattern(regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d).*$",
message = "Password must contain uppercase, lowercase, and number")
private String password;
}