Security is where enterprise Java gets serious. Spring Security is the de facto standard for securing Spring applications, and interviewers expect backend developers to understand it deeply.
This guide covers authentication, authorization, JWT, OAuth2, and security best practices—the knowledge that comes up in Java backend interviews.
Table of Contents
- Security Fundamentals Questions
- Authentication Questions
- Authorization Questions
- JWT Authentication Questions
- OAuth2 and OpenID Connect Questions
- Security Configuration Questions
- Security Best Practices Questions
Security Fundamentals Questions
Understanding the core concepts is essential before diving into Spring Security specifics.
What is the difference between authentication and authorization?
Authentication and authorization are the two fundamental pillars of application security, and confusing them is a common mistake. Authentication answers the question "Who are you?" by verifying identity through credentials like username/password, tokens, or certificates. Authorization comes after and answers "What can you do?" by checking whether the authenticated user has permission to perform a specific action.
Spring Security processes these in order: AuthenticationManager handles authentication first, then AccessDecisionManager enforces authorization rules. Understanding this flow is crucial for configuring security correctly.
| Concept | Question | Spring Security Component |
|---|---|---|
| Authentication | Who are you? | AuthenticationManager |
| Authorization | What can you do? | AccessDecisionManager |
Authentication verifies identity through credentials:
- Username and password
- JWT tokens
- OAuth2 tokens
- Certificates
Authorization checks permissions after authentication:
- Role-based (ROLE_ADMIN, ROLE_USER)
- Permission-based (READ_PRIVILEGE, WRITE_PRIVILEGE)
- Resource-based (own resources only)
How does the Spring Security filter chain work?
Spring Security is built on servlet filters, which is fundamental to understanding how it processes requests. Every HTTP request passes through a chain of security filters before reaching your controller. Each filter has a specific responsibility—some handle authentication, others handle authorization, and some handle specific attack vectors like CSRF.
The filters execute in a specific order, and understanding this order helps you troubleshoot security issues and know where to add custom filters. The chain is configured through the SecurityFilterChain bean.
flowchart LR
REQ["Request"] --> CHAIN["Filter Chain"] --> SERVLET["Servlet<br/>(Controller)"]flowchart TB
subgraph CHAIN["Security Filter Chain"]
F1["1. SecurityContextPersistenceFilter<br/><i>Loads/saves SecurityContext from session</i>"]
F2["2. UsernamePasswordAuthenticationFilter<br/><i>Processes login form submissions</i>"]
F3["3. BasicAuthenticationFilter<br/><i>Processes HTTP Basic authentication</i>"]
F4["4. BearerTokenAuthenticationFilter<br/><i>Processes JWT/OAuth2 bearer tokens</i>"]
F5["5. ExceptionTranslationFilter<br/><i>Handles security exceptions</i>"]
F6["6. FilterSecurityInterceptor<br/><i>Enforces authorization rules</i>"]
F1 --> F2 --> F3 --> F4 --> F5 --> F6
endHow do you access the current user in Spring Security?
Spring Security stores the authenticated user's information in the SecurityContext, which is held in a ThreadLocal variable via SecurityContextHolder. This means you can access the current user from anywhere in your code during request processing. The Authentication object contains the principal (usually UserDetails), credentials, and granted authorities.
In controllers, you can also use the @AuthenticationPrincipal annotation to inject the current user directly as a method parameter, which is cleaner than accessing SecurityContextHolder manually.
// Get the current authenticated user
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
// Get username
String username = authentication.getName();
// Get authorities (roles/permissions)
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
// Get principal (usually UserDetails)
Object principal = authentication.getPrincipal();
if (principal instanceof UserDetails) {
UserDetails user = (UserDetails) principal;
String username = user.getUsername();
}
// In a controller, inject directly
@GetMapping("/profile")
public ResponseEntity<User> getProfile(@AuthenticationPrincipal UserDetails user) {
return ResponseEntity.ok(userService.findByUsername(user.getUsername()));
}Authentication Questions
Spring Security supports multiple authentication mechanisms. Here's how to implement the most common ones.
How do you configure Spring Security in Spring Boot 3.x?
Spring Boot 3.x introduced significant changes to Spring Security configuration. The old WebSecurityConfigurerAdapter was removed in favor of component-based configuration using SecurityFilterChain beans. This approach is more flexible and aligns with Spring's general move toward functional configuration.
The basic configuration defines which URLs require authentication, how login works, and how logout is handled. Always include a PasswordEncoder bean—BCrypt is the recommended default for password hashing.
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**", "/auth/**").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.defaultSuccessUrl("/dashboard")
.permitAll()
)
.logout(logout -> logout
.logoutSuccessUrl("/login?logout")
.permitAll()
);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}How do you implement UserDetailsService?
UserDetailsService is the core interface for loading user-specific data during authentication. Spring Security calls its loadUserByUsername method when a user attempts to log in, and you're responsible for fetching the user from your database and returning a UserDetails object.
The implementation should throw UsernameNotFoundException if the user doesn't exist. The returned UserDetails must include the encoded password and the user's roles/authorities.
@Service
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
public CustomUserDetailsService(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));
return org.springframework.security.core.userdetails.User.builder()
.username(user.getUsername())
.password(user.getPassword()) // Already encoded
.roles(user.getRoles().toArray(new String[0]))
.build();
}
}How do you create a custom UserDetails implementation?
The default UserDetails interface provides basic user information, but real applications often need additional fields like email, profile picture, or custom permissions. Creating a custom UserDetails implementation lets you include this extra information in the security context, making it available throughout your application.
Your custom class must implement all UserDetails methods and can add any additional fields your application needs. The getAuthorities method should return the user's roles with the ROLE_ prefix.
public class CustomUserDetails implements UserDetails {
private final User user;
public CustomUserDetails(User user) {
this.user = user;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return user.getRoles().stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role.getName()))
.collect(Collectors.toList());
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return !user.isLocked();
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return user.isEnabled();
}
// Custom methods
public Long getId() {
return user.getId();
}
public String getEmail() {
return user.getEmail();
}
}How should you encode passwords in Spring Security?
Never store plain-text passwords—this is a fundamental security requirement. Spring Security provides several password encoders, with BCryptPasswordEncoder being the recommended default. BCrypt is adaptive, meaning you can increase the work factor as hardware improves, making it future-proof.
When registering users, encode the password before saving. When authenticating, Spring Security automatically uses the same encoder to verify the password matches. For applications with legacy password formats, DelegatingPasswordEncoder can handle migration.
@Bean
public PasswordEncoder passwordEncoder() {
// BCrypt is the recommended default
return new BCryptPasswordEncoder();
}
// Usage in registration
public User registerUser(RegisterRequest request) {
User user = new User();
user.setUsername(request.getUsername());
user.setPassword(passwordEncoder.encode(request.getPassword()));
return userRepository.save(user);
}
// Password encoders available
BCryptPasswordEncoder // Recommended, adaptive
Argon2PasswordEncoder // Memory-hard, most secure
SCryptPasswordEncoder // Memory-hard alternative
Pbkdf2PasswordEncoder // NIST recommendedWhen should you create a custom AuthenticationProvider?
AuthenticationProvider is the interface responsible for the actual authentication logic. Spring Security provides default implementations for common scenarios, but you need a custom provider when you have special authentication requirements—like checking additional conditions beyond username and password, integrating with external authentication systems, or implementing custom authentication schemes.
The provider's authenticate method receives the authentication request and returns a fully authenticated token on success, or throws an exception on failure.
@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
private final UserDetailsService userDetailsService;
private final PasswordEncoder passwordEncoder;
@Override
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
String username = authentication.getName();
String password = authentication.getCredentials().toString();
UserDetails user = userDetailsService.loadUserByUsername(username);
if (!passwordEncoder.matches(password, user.getPassword())) {
throw new BadCredentialsException("Invalid password");
}
// Additional checks
if (!user.isEnabled()) {
throw new DisabledException("Account is disabled");
}
return new UsernamePasswordAuthenticationToken(
user, password, user.getAuthorities()
);
}
@Override
public boolean supports(Class<?> authentication) {
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
}
}Authorization Questions
Authorization controls what authenticated users can access.
How do you configure URL-based authorization?
URL-based authorization is configured in the SecurityFilterChain and determines which endpoints require authentication and what roles or authorities are needed. The order of matchers matters—more specific patterns should come before general ones. Spring Security evaluates them in order and uses the first match.
You can use role checks (hasRole), authority checks (hasAuthority), or even SpEL expressions for complex authorization logic. The anyRequest().authenticated() at the end is a catch-all that ensures no endpoint is accidentally left unprotected.
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(auth -> auth
// Public endpoints
.requestMatchers("/", "/public/**", "/auth/**").permitAll()
// Static resources
.requestMatchers("/css/**", "/js/**", "/images/**").permitAll()
// Role-based
.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers("/user/**").hasAnyRole("USER", "ADMIN")
// Authority-based (more granular)
.requestMatchers(HttpMethod.DELETE, "/api/**").hasAuthority("DELETE_PRIVILEGE")
// SpEL expressions
.requestMatchers("/api/users/{id}/**")
.access((authentication, context) -> {
Long userId = Long.parseLong(context.getVariables().get("id"));
// Custom logic
return new AuthorizationDecision(
hasAccess(authentication.get(), userId)
);
})
// Everything else requires authentication
.anyRequest().authenticated()
);
return http.build();
}How do you enable method-level security?
Method-level security lets you protect individual methods rather than URLs, which is useful when authorization logic depends on method parameters or return values. Enable it with @EnableMethodSecurity, then annotate methods with security rules.
There are three annotation styles: @PreAuthorize (Spring's most powerful, uses SpEL), @Secured (Spring's simple role check), and @RolesAllowed (JSR-250 standard). @PreAuthorize is recommended for its flexibility.
@Configuration
@EnableMethodSecurity(
prePostEnabled = true, // @PreAuthorize, @PostAuthorize
securedEnabled = true, // @Secured
jsr250Enabled = true // @RolesAllowed
)
public class MethodSecurityConfig {
}@PreAuthorize - Before method execution:
@Service
public class UserService {
// Simple role check
@PreAuthorize("hasRole('ADMIN')")
public List<User> getAllUsers() {
return userRepository.findAll();
}
// Multiple roles
@PreAuthorize("hasAnyRole('ADMIN', 'MANAGER')")
public void updateUser(User user) {
userRepository.save(user);
}
// SpEL with method parameters
@PreAuthorize("#userId == principal.id or hasRole('ADMIN')")
public User getUser(Long userId) {
return userRepository.findById(userId).orElseThrow();
}
// Complex expressions
@PreAuthorize("hasRole('ADMIN') and #user.department == principal.department")
public void promoteUser(User user) {
// ...
}
// Custom method
@PreAuthorize("@securityService.canAccessResource(#resourceId)")
public Resource getResource(Long resourceId) {
return resourceRepository.findById(resourceId).orElseThrow();
}
}@PostAuthorize - After method execution:
// Check result after method executes
@PostAuthorize("returnObject.owner == principal.username or hasRole('ADMIN')")
public Document getDocument(Long id) {
return documentRepository.findById(id).orElseThrow();
}@Secured and @RolesAllowed:
// Spring's @Secured
@Secured("ROLE_ADMIN")
public void adminOnly() { }
@Secured({"ROLE_ADMIN", "ROLE_MANAGER"})
public void adminOrManager() { }
// JSR-250 @RolesAllowed
@RolesAllowed("ADMIN")
public void adminOnly() { }What is the difference between role and authority?
This distinction confuses many developers. In Spring Security, roles are just authorities with a special ROLE_ prefix. When you use hasRole("ADMIN"), Spring Security actually checks for the authority "ROLE_ADMIN". This is purely a naming convention—roles represent high-level user categories, while authorities can represent any permission.
When creating authorities, add the ROLE_ prefix for roles and use descriptive names for fine-grained permissions. In configuration, hasRole() adds the prefix automatically, while hasAuthority() requires the exact authority name.
// Roles are authorities with ROLE_ prefix
// hasRole("ADMIN") checks for ROLE_ADMIN authority
// When creating authorities:
new SimpleGrantedAuthority("ROLE_ADMIN") // For roles
new SimpleGrantedAuthority("READ_PRIVILEGE") // For permissions
// In configuration:
.hasRole("ADMIN") // Checks ROLE_ADMIN
.hasAuthority("ROLE_ADMIN") // Checks ROLE_ADMIN (explicit)
.hasAuthority("READ_PRIVILEGE") // Checks exact authorityJWT Authentication Questions
Stateless authentication using JSON Web Tokens is essential for modern APIs.
What is the structure of a JWT token?
A JWT consists of three Base64-encoded parts separated by dots: header, payload, and signature. The header specifies the signing algorithm. The payload contains claims—statements about the user like username, roles, and expiration time. The signature ensures the token hasn't been tampered with.
Understanding this structure helps you debug JWT issues and design appropriate claims. The token is self-contained, meaning the server doesn't need to store session state—all necessary information is in the token itself.
Header.Payload.Signature
eyJhbGciOiJIUzI1NiJ9. // Header (algorithm)
eyJzdWIiOiJ1c2VyMSIsInJvbGVzIjpbIlVTRVIiXX0. // Payload (claims)
abc123signature // Signature
How do you create a JWT utility class?
A JWT utility class handles token generation, validation, and claim extraction. It encapsulates the cryptographic operations and expiration logic. The class needs a secret key for signing (for symmetric algorithms like HS256) and should provide methods for generating tokens and extracting claims.
Use a library like jjwt for the heavy lifting. Store the secret and expiration time in configuration properties, not hardcoded. Always validate both the signature and expiration when verifying tokens.
@Component
public class JwtUtil {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration}")
private long expiration;
private SecretKey getSigningKey() {
byte[] keyBytes = Decoders.BASE64.decode(secret);
return Keys.hmacShaKeyFor(keyBytes);
}
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
claims.put("roles", userDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList()));
return Jwts.builder()
.setClaims(claims)
.setSubject(userDetails.getUsername())
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + expiration))
.signWith(getSigningKey(), SignatureAlgorithm.HS256)
.compact();
}
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
public Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}
private Claims extractAllClaims(String token) {
return Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token)
.getBody();
}
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());
}
}How do you implement a JWT authentication filter?
The JWT filter intercepts every request, extracts the token from the Authorization header, validates it, and populates the SecurityContext with the authenticated user. This filter runs before the standard username/password filter and enables stateless authentication.
The filter should gracefully handle missing or invalid tokens—for public endpoints, simply continue the filter chain. For expired tokens, return an appropriate error response.
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
private final UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String authHeader = request.getHeader("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
String token = authHeader.substring(7);
try {
String username = jwtUtil.extractUsername(token);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (jwtUtil.validateToken(token, userDetails)) {
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities()
);
authToken.setDetails(new WebAuthenticationDetailsSource()
.buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
} catch (ExpiredJwtException e) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("Token expired");
return;
} catch (JwtException e) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("Invalid token");
return;
}
filterChain.doFilter(request, response);
}
}How do you configure Spring Security for JWT?
JWT configuration requires three key changes from traditional form-based authentication: disable CSRF protection (tokens provide their own protection), set session management to stateless (no server-side sessions), and add the JWT filter to the filter chain. The filter must be added before UsernamePasswordAuthenticationFilter so it processes the token first.
You also need to expose the AuthenticationManager bean for use in your login endpoint.
@Configuration
@EnableWebSecurity
public class JwtSecurityConfig {
private final JwtAuthenticationFilter jwtAuthFilter;
private final UserDetailsService userDetailsService;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// Disable CSRF for stateless API
.csrf(csrf -> csrf.disable())
// Stateless session
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
// Authorization rules
.authorizeHttpRequests(auth -> auth
.requestMatchers("/auth/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
// Add JWT filter before username/password filter
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
}How do you implement JWT login and refresh endpoints?
The login endpoint authenticates the user with username and password, then generates and returns a JWT token. The refresh endpoint takes an existing valid token and issues a new one with extended expiration. This allows clients to maintain sessions without re-entering credentials.
Use AuthenticationManager for credential verification and JwtUtil for token operations. Return appropriate HTTP status codes for authentication failures.
@RestController
@RequestMapping("/auth")
public class AuthController {
private final AuthenticationManager authenticationManager;
private final JwtUtil jwtUtil;
private final UserDetailsService userDetailsService;
@PostMapping("/login")
public ResponseEntity<AuthResponse> login(@RequestBody LoginRequest request) {
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
request.getUsername(),
request.getPassword()
)
);
UserDetails user = userDetailsService.loadUserByUsername(request.getUsername());
String token = jwtUtil.generateToken(user);
return ResponseEntity.ok(new AuthResponse(token));
}
@PostMapping("/refresh")
public ResponseEntity<AuthResponse> refresh(@RequestHeader("Authorization") String authHeader) {
String oldToken = authHeader.substring(7);
String username = jwtUtil.extractUsername(oldToken);
UserDetails user = userDetailsService.loadUserByUsername(username);
if (jwtUtil.validateToken(oldToken, user)) {
String newToken = jwtUtil.generateToken(user);
return ResponseEntity.ok(new AuthResponse(newToken));
}
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
}OAuth2 and OpenID Connect Questions
Modern authentication for APIs and single sign-on.
What are the OAuth2 roles?
OAuth2 defines four roles that interact during the authorization process. Understanding these roles helps you architect secure integrations and debug authorization flows. The Resource Owner is typically the end user who owns the data. The Client is your application requesting access. The Authorization Server (like Keycloak, Auth0, or Okta) handles authentication and issues tokens. The Resource Server is your API that validates tokens and serves protected data.
In many setups, your Spring Boot application acts as the Resource Server, validating tokens issued by an external Authorization Server.
| Role | Description |
|---|---|
| Resource Owner | The user who owns the data |
| Client | Application requesting access |
| Authorization Server | Issues tokens (Keycloak, Auth0, Okta) |
| Resource Server | API that validates tokens |
How do you configure a Spring Boot OAuth2 resource server?
Spring Boot makes OAuth2 resource server configuration straightforward. Add the starter dependency, configure the issuer URI (the Authorization Server's URL), and Spring Security automatically validates incoming JWT tokens against the Authorization Server's public keys.
For custom claims mapping, create a JwtAuthenticationConverter to extract roles from your token's specific claim name and add the appropriate prefix.
# application.yml
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://auth.example.com/realms/myrealm
# Or specify JWK Set URI directly
# jwk-set-uri: https://auth.example.com/.well-known/jwks.json@Configuration
@EnableWebSecurity
public class OAuth2ResourceServerConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll()
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.jwtAuthenticationConverter(jwtAuthenticationConverter())
)
);
return http.build();
}
@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter authoritiesConverter = new JwtGrantedAuthoritiesConverter();
authoritiesConverter.setAuthoritiesClaimName("roles");
authoritiesConverter.setAuthorityPrefix("ROLE_");
JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
converter.setJwtGrantedAuthoritiesConverter(authoritiesConverter);
return converter;
}
}How do you implement social login with Google or GitHub?
OAuth2 client configuration enables "Login with Google/GitHub" functionality. Spring Security handles the OAuth2 flow automatically—you just need to provide the client credentials from each provider. Users are redirected to the provider for authentication, then returned to your application with their profile information.
Configure multiple providers in application.yml, each with their client ID, secret, and requested scopes.
# application.yml
spring:
security:
oauth2:
client:
registration:
google:
client-id: ${GOOGLE_CLIENT_ID}
client-secret: ${GOOGLE_CLIENT_SECRET}
scope: openid, profile, email
github:
client-id: ${GITHUB_CLIENT_ID}
client-secret: ${GITHUB_CLIENT_SECRET}
scope: read:user, user:email@Configuration
@EnableWebSecurity
public class OAuth2LoginConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/login/**", "/error").permitAll()
.anyRequest().authenticated()
)
.oauth2Login(oauth2 -> oauth2
.loginPage("/login")
.defaultSuccessUrl("/dashboard")
.userInfoEndpoint(userInfo -> userInfo
.userService(customOAuth2UserService())
)
);
return http.build();
}
@Bean
public OAuth2UserService<OAuth2UserRequest, OAuth2User> customOAuth2UserService() {
return new CustomOAuth2UserService();
}
}How do you customize OAuth2 user loading?
By default, Spring Security creates a basic OAuth2User from the provider's response. To integrate with your own user database—creating accounts for new users or loading existing ones—implement a custom OAuth2UserService.
This service receives the OAuth2 user data from the provider, finds or creates the corresponding user in your database, and returns a custom principal that includes your application's user information.
@Service
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
private final UserRepository userRepository;
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2User oauth2User = super.loadUser(userRequest);
String provider = userRequest.getClientRegistration().getRegistrationId();
String providerId = oauth2User.getAttribute("sub"); // or "id" for GitHub
String email = oauth2User.getAttribute("email");
String name = oauth2User.getAttribute("name");
// Find or create user
User user = userRepository.findByProviderAndProviderId(provider, providerId)
.orElseGet(() -> {
User newUser = new User();
newUser.setProvider(provider);
newUser.setProviderId(providerId);
newUser.setEmail(email);
newUser.setName(name);
newUser.setRoles(Set.of("USER"));
return userRepository.save(newUser);
});
return new CustomOAuth2User(user, oauth2User.getAttributes());
}
}Security Configuration Questions
Common configurations that appear in most Spring Security applications.
How do you configure CORS in Spring Security?
Cross-Origin Resource Sharing (CORS) must be configured in Spring Security when your frontend and backend are on different origins. Without proper CORS configuration, browsers block cross-origin requests for security. Spring Security provides a CorsConfigurationSource that you integrate into the security filter chain.
Specify allowed origins explicitly rather than using wildcards in production. Configure allowed methods, headers, and whether credentials are allowed.
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
// ... other config
;
return http.build();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(List.of("https://example.com", "http://localhost:3000"));
configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
configuration.setAllowedHeaders(List.of("*"));
configuration.setAllowCredentials(true);
configuration.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/api/**", configuration);
return source;
}How do you configure CSRF protection?
CSRF protection is enabled by default in Spring Security for stateful applications. It generates a unique token per session that must be included in state-changing requests. Attackers can't forge requests because they can't access the victim's CSRF token.
For stateless JWT-based APIs, disable CSRF—the JWT token itself provides protection against forgery. For traditional web applications, use CookieCsrfTokenRepository to make the token accessible to JavaScript.
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// For traditional web apps - CSRF enabled by default
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.ignoringRequestMatchers("/api/webhooks/**") // Exclude webhooks
)
// For stateless APIs - disable CSRF
// .csrf(csrf -> csrf.disable())
;
return http.build();
}How do you configure security headers?
Security headers protect against various attacks like XSS, clickjacking, and protocol downgrade attacks. Spring Security provides sensible defaults, but you should customize them based on your application's needs.
Content-Security-Policy is crucial for preventing XSS by controlling which resources the browser can load. X-Frame-Options prevents clickjacking by controlling iframe embedding. HSTS forces HTTPS connections.
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.headers(headers -> headers
.contentSecurityPolicy(csp -> csp
.policyDirectives("default-src 'self'; script-src 'self'")
)
.frameOptions(frame -> frame.deny())
.xssProtection(xss -> xss.disable()) // Modern browsers have built-in protection
.contentTypeOptions(Customizer.withDefaults()) // X-Content-Type-Options: nosniff
.httpStrictTransportSecurity(hsts -> hsts
.includeSubDomains(true)
.maxAgeInSeconds(31536000)
)
);
return http.build();
}How do you configure session management?
Session management controls how Spring Security handles HTTP sessions. Options include when to create sessions, how many concurrent sessions a user can have, and how to protect against session fixation attacks.
For stateless APIs, set the policy to STATELESS. For traditional web applications, configure concurrent session limits and fixation protection.
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.sessionManagement(session -> session
// Session creation policy
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
// Concurrent session control
.maximumSessions(1)
.maxSessionsPreventsLogin(false) // Kicks out previous session
// Session fixation protection
.sessionFixation().migrateSession()
// Invalid session handling
.invalidSessionUrl("/login?invalid")
);
return http.build();
}Security Best Practices Questions
Following security best practices is essential for production applications.
What are the best practices for password storage?
Password storage is critical—a breach that exposes passwords can have catastrophic consequences. Always use a strong adaptive hashing algorithm like BCrypt with an appropriate work factor. Never use MD5, SHA-1, or plain SHA-256 for passwords.
For applications migrating from legacy systems, DelegatingPasswordEncoder can handle multiple encoding formats, automatically upgrading old hashes on successful login.
// Always use strong password encoder
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12); // Work factor 12
}
// For upgrading legacy passwords
@Bean
public PasswordEncoder passwordEncoder() {
String encodingId = "bcrypt";
Map<String, PasswordEncoder> encoders = new HashMap<>();
encoders.put("bcrypt", new BCryptPasswordEncoder());
encoders.put("sha256", new StandardPasswordEncoder()); // Legacy
return new DelegatingPasswordEncoder(encodingId, encoders);
}How do you implement rate limiting?
Rate limiting protects against brute force attacks and denial of service. Implement it as a filter that tracks request counts per client (usually by IP or authenticated user) and rejects requests that exceed the limit.
Consider using established libraries like Bucket4j or Resilience4j for production implementations. Apply stricter limits to authentication endpoints where brute force is most common.
@Component
public class RateLimitingFilter extends OncePerRequestFilter {
private final RateLimiter rateLimiter;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String clientIp = request.getRemoteAddr();
if (!rateLimiter.tryAcquire(clientIp)) {
response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
response.getWriter().write("Rate limit exceeded");
return;
}
filterChain.doFilter(request, response);
}
}How does Spring Security prevent common attacks?
Spring Security provides built-in protection against many common web vulnerabilities. Understanding these protections helps you configure them correctly and avoid accidentally disabling important defenses.
XSS protection comes from Content-Security-Policy headers. SQL injection is prevented by using parameterized queries (JPA handles this). CSRF protection is enabled by default. Clickjacking is prevented by X-Frame-Options. Always keep these protections enabled unless you have a specific reason to disable them.
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// XSS Protection - use Content-Security-Policy
.headers(headers -> headers
.contentSecurityPolicy(csp -> csp
.policyDirectives("default-src 'self'; script-src 'self'")
)
)
// SQL Injection - use parameterized queries (JPA does this)
// CSRF - enabled by default for stateful apps
// Clickjacking - frame options
.headers(headers -> headers
.frameOptions(frame -> frame.deny())
)
// Secure cookies
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
);
return http.build();
}
}How do you log security events?
Logging security events is essential for monitoring and forensics. Spring Security publishes authentication events that you can capture with event listeners. Log successful logins for audit trails and failed attempts for detecting attacks.
Include relevant details like username and timestamp, but never log passwords or tokens. Consider sending alerts for suspicious patterns like multiple failed logins.
@Component
public class AuthenticationEventListener {
private static final Logger logger = LoggerFactory.getLogger(AuthenticationEventListener.class);
@EventListener
public void onAuthenticationSuccess(AuthenticationSuccessEvent event) {
String username = event.getAuthentication().getName();
logger.info("Successful login: {}", username);
}
@EventListener
public void onAuthenticationFailure(AbstractAuthenticationFailureEvent event) {
String username = event.getAuthentication().getName();
logger.warn("Failed login attempt: {} - Reason: {}",
username, event.getException().getMessage());
}
}How do you test Spring Security configurations?
Testing security configurations ensures your endpoints are properly protected. Spring Security Test provides annotations like @WithMockUser to simulate authenticated users with specific roles. Test both positive cases (authorized users can access) and negative cases (unauthorized users are blocked).
Use MockMvc for integration testing and verify HTTP status codes match your security requirements.
@SpringBootTest
@AutoConfigureMockMvc
class SecurityTest {
@Autowired
private MockMvc mockMvc;
@Test
void publicEndpoint_shouldBeAccessible() throws Exception {
mockMvc.perform(get("/public/health"))
.andExpect(status().isOk());
}
@Test
void protectedEndpoint_shouldReturn401_whenNotAuthenticated() throws Exception {
mockMvc.perform(get("/api/users"))
.andExpect(status().isUnauthorized());
}
@Test
@WithMockUser(username = "user", roles = "USER")
void protectedEndpoint_shouldBeAccessible_whenAuthenticated() throws Exception {
mockMvc.perform(get("/api/users"))
.andExpect(status().isOk());
}
@Test
@WithMockUser(username = "user", roles = "USER")
void adminEndpoint_shouldReturn403_forRegularUser() throws Exception {
mockMvc.perform(get("/api/admin/users"))
.andExpect(status().isForbidden());
}
@Test
@WithMockUser(username = "admin", roles = "ADMIN")
void adminEndpoint_shouldBeAccessible_forAdmin() throws Exception {
mockMvc.perform(get("/api/admin/users"))
.andExpect(status().isOk());
}
}Quick Reference
What are the key Spring Security concepts?
Authentication vs Authorization:
- Authentication = Who are you?
- Authorization = What can you do?
Filter chain order:
- SecurityContextPersistenceFilter
- Authentication filters
- ExceptionTranslationFilter
- FilterSecurityInterceptor
Method security annotations:
@PreAuthorize- SpEL expressions, most flexible@Secured- Simple role checks@RolesAllowed- JSR-250 standard
JWT setup:
- Create JWT utility (generate, validate)
- Create JWT filter (extract, authenticate)
- Configure stateless session
- Disable CSRF
OAuth2 Resource Server:
- Add starter dependency
- Configure issuer-uri
- Customize JWT converter for roles
Related Articles
- Complete Java Backend Developer Interview Guide - Full Java backend interview guide
- Spring Boot Interview Guide - Spring Boot fundamentals
- Authentication & JWT Interview Guide - JWT concepts in depth
- Web Security & OWASP Interview Guide - Security fundamentals
