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.
Security Fundamentals
Before diving into Spring Security specifics, understand the core concepts.
Authentication vs Authorization
| 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)
The Security Filter Chain
Spring Security is built on servlet filters. Every request passes through a chain of security filters.
Request → Filter Chain → Servlet (Controller)
┌─────────────────────────────────────────────────────────┐
│ Security Filter Chain │
├─────────────────────────────────────────────────────────┤
│ 1. SecurityContextPersistenceFilter │
│ └── Loads/saves SecurityContext from session │
│ │
│ 2. UsernamePasswordAuthenticationFilter │
│ └── Processes login form submissions │
│ │
│ 3. BasicAuthenticationFilter │
│ └── Processes HTTP Basic authentication │
│ │
│ 4. BearerTokenAuthenticationFilter │
│ └── Processes JWT/OAuth2 bearer tokens │
│ │
│ 5. ExceptionTranslationFilter │
│ └── Handles security exceptions │
│ │
│ 6. FilterSecurityInterceptor │
│ └── Enforces authorization rules │
└─────────────────────────────────────────────────────────┘
SecurityContext and Principal
// 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
Spring Security supports multiple authentication mechanisms. Here's how to implement the most common ones.
Basic Configuration (Spring Boot 3.x)
@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();
}
}UserDetailsService
Load user data from your database.
@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();
}
}Custom UserDetails
For more user information in the security context.
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();
}
}Password Encoding
Never store plain-text passwords.
@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 recommendedAuthentication Provider
For custom authentication logic.
@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
Control what authenticated users can access.
URL-Based Authorization
@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();
}Method Security
Enable with @EnableMethodSecurity:
@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() { }Role vs Authority
// 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
Stateless authentication using JSON Web Tokens.
JWT Structure
Header.Payload.Signature
eyJhbGciOiJIUzI1NiJ9. // Header (algorithm)
eyJzdWIiOiJ1c2VyMSIsInJvbGVzIjpbIlVTRVIiXX0. // Payload (claims)
abc123signature // Signature
JWT Utility Class
@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());
}
}JWT Authentication Filter
@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);
}
}JWT Security Configuration
@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();
}
}Login Endpoint
@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 & OpenID Connect
Modern authentication for APIs and single sign-on.
OAuth2 Roles
| 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 |
Resource Server Configuration
# 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;
}
}OAuth2 Client (Login with Google/GitHub)
# 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();
}
}Custom OAuth2 User Service
@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());
}
}Common Security Configurations
CORS Configuration
@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;
}CSRF Configuration
@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();
}Security Headers
@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();
}Session Management
@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
Password Storage
// 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);
}Rate Limiting
@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);
}
}Preventing Common Attacks
@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();
}
}Logging Security Events
@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());
}
}Testing Security
@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
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
What's Next?
Spring Security is vast—this guide covers what matters for interviews. In practice, start with the simplest configuration that meets your needs, then add complexity as required.
Remember: security is not a feature, it's a requirement. Every endpoint should be consciously configured as public or protected. Default to authenticated access.
