Lessons available in both languages
Java Backend · Interview Prep

Spring Security interview questions & answers

204+ real Spring Security interview questions with model answers, plus free lessons to learn the concepts. Prepare in English & Hinglish, then practise with an AI mock interview.

18 topics · 204+ questions

How Hirenix teaches

One chapter. 90 minutes.
Interview-ready.

Every concept starts with a real-world problem — the kind that actually shows up in production code. Nothing to cram; it just clicks. Every question comes with a model answer: exactly what to say in the room, and why. Then an AI mock interview on the same chapter.

  • 📖Concept in 5 minutesNo jargon — straight to the point
  • 🛠️Real-world problemThe kind production code throws at you
  • 💬Model answerExactly what to say in the room
  • 🧠FlashcardsRevise in 10 minutes
  • 🤖AI mock interviewIt asks follow-ups too
  • 📊Weak topicsSee exactly where you're stuck
Start this chapter — free🌐 English🇮🇳 Hinglish
A student learning an interview concept on Hirenix at home
Video playlistbuilt around a syllabus18h+
Hirenix chapterbuilt around interviews90 min

The difference isn’t the content — it’s the filter. Only what’s actually used in production and actually asked in interviews. Textbook topics the industry never touches don’t make the cut.

Lessons available in both languages

What you’ll learn

  • Why Spring Security: what you get before you write any config
  • The security filter chain and its order
  • Authentication vs authorization: 401 vs 403Free account
  • UserDetailsService and authentication providersFree account
  • Password storage: BCrypt and the encoder prefixFree account
  • Authorizing requests: matchers, rules, and why order mattersFree account
  • Roles vs authorities and the ROLE_ prefixFree account
  • Method security: PreAuthorize and the annotation that does nothingFree account
  • SecurityContextHolder and getting the current userFree account
  • CSRF protection: on by default, and when to switch it offFree account
  • Sessions vs stateless APIsFree account
  • JWT and the resource server: valid, expired, tamperedFree account
  • CORS and the security headersFree account
  • Custom filters, entry points and access-denied handlingFree account
  • RecapFree account
  • Project: Secure the Job Board API
  • Project: JWT Login FlowFree account
  • Project: Role-Based Admin AreaFree account

Why Spring Security: what you get before you write any config

The fastest way to understand what Spring Security is for is to add nothing and watch what happens.

The demo below is a complete application: one @SpringBootApplication class, one @GetMapping("/hello") that returns a string. There is no security configuration in it - no config class, no annotations, no filter. The only thing that changed is that Spring Security's jars are on the classpath.

The output is the whole lesson. With no credentials the endpoint answers 401. With the right username and password it answers 200. With the right username and a wrong password it answers 401 again. None of that behaviour was written by the developer.

What actually turned on

Three separate things happened at startup, and each has a name an interviewer will expect:

  1. A bean called springSecurityFilterChain appeared. That is the entry point for every secured request - the next topic takes it apart.
  2. An in-memory user was created. Normally Boot generates a random password and prints it in the log; the demo pins the username and password with spring.security.user.* so the test is repeatable.
  3. HTTP Basic authentication was enabled, which is why a browser or an HTTP client can send credentials at all.

The bean count in the output makes the size of that visible: 206 beans in an application whose source is two methods. Spring Boot registers those beans because Security's classes are on the classpath - the same @ConditionalOnClass mechanism the Spring Boot chapter measured. Boot's own numbers back this up: on a plain web app that count was 156, and adding the Security jars alone took it to 286.

Authentication and authorization are two different questions. Authentication asks who are you and produces an Authentication object. Authorization asks are you allowed to do this and produces a yes or no. Almost every confusing result in this chapter comes from answering the wrong one - the 401-vs-403 topic is entirely about that.

Why use the framework instead of writing a filter

Writing your own filter that checks a header looks like a fifty-line job, and for the happy path it is. The part that is not fifty lines is everything around it: hashing and upgrading passwords, not leaking whether a username exists, CSRF for cookie-based flows, session fixation, the default security headers, and returning the right status code in the right situation. Each of those is a whole topic in this chapter, and each one is something Spring Security already does.

When to use it: any Spring application that has users, roles, tokens, or an admin area - which is nearly every backend a job description will mention. It is also the answer interviewers expect: not "I wrote a filter", but "I configured a SecurityFilterChain".

Trade-off: the cost is that the defaults are invisible. Nothing in your source says the endpoint is protected, and nothing says CSRF is on. That is exactly why this chapter measures instead of describing - and why the next topic starts by printing the filter chain rather than talking about it.

The modern shape of configuration is a SecurityFilterChain bean built from HttpSecurity. If you read an older tutorial that extends WebSecurityConfigurerAdapter, that class is gone in Spring Security 6 - saying so is itself a common interview checkpoint.

// ---- smoke/Smoke.java ----
package smoke;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@SpringBootApplication
@RestController
public class Smoke {

  @GetMapping("/hello")
  public String hello() { return "hello"; }

  // NOTE: there is no security configuration anywhere in this file.

  public static void main(String[] args) throws Exception {
    System.setProperty("server.port", "0");
    // Boot generates a random password and logs it. Pinned here so the test repeats.
    System.setProperty("spring.security.user.name", "waquar");
    System.setProperty("spring.security.user.password", "s3cret");

    try (ConfigurableApplicationContext ctx = SpringApplication.run(Smoke.class, args)) {
      int port = Integer.parseInt(ctx.getEnvironment().getProperty("local.server.port", "0"));

      // status(port, path, basicCredentials) does one real HTTP call
      System.out.println("  /hello  no creds        -> " + status(port, "/hello", null));
      System.out.println("  /hello  waquar:s3cret   -> " + status(port, "/hello", "waquar:s3cret"));
      System.out.println("  /hello  waquar:wrong    -> " + status(port, "/hello", "waquar:wrong"));

      System.out.println("  springSecurityFilterChain bean present = "
          + ctx.containsBean("springSecurityFilterChain"));
      System.out.println("  total beans = " + ctx.getBeanDefinitionCount());
    }
  }
}

The security filter chain and its order

Spring Security is not a layer inside your controller. It is a chain of servlet filters that runs before your controller, and almost every surprising result in this chapter is explained by that one sentence.

The path a request takes has three named parts, and interviewers ask for them in order:

  1. DelegatingFilterProxy - a plain servlet filter registered with the servlet container. The container knows nothing about Spring beans, so this proxy is the bridge: it looks up a Spring bean and delegates to it.
  2. FilterChainProxy - that bean. It holds a list of SecurityFilterChain objects and picks the first one whose matcher matches the request.
  3. SecurityFilterChain - one chain: a matcher plus an ordered list of filters that actually do the work.

The demo prints all of it rather than describing it. It declares two chains - one matching /api/**, one matching everything else - and then walks FilterChainProxy.getFilterChains() and prints every filter's class name.

Read the two chains side by side

Both chains have 11 filters, but they are not the same 11, and the difference is the whole point:

  • The /api/** chain has a BasicAuthenticationFilter and no CsrfFilter - because that chain declared httpBasic() and csrf().disable().
  • The catch-all chain has a CsrfFilter and no BasicAuthenticationFilter - because it declared neither.

So the filter list is not a fixed thing you memorise. It is generated from your configuration, and this chapter has now measured three different counts in three configurations: 12 filters on a chain with HTTP Basic and CSRF left on, 11 on each of the two chains above, and 11 again once a custom filter was added and CSRF disabled. Quoting any one of those as "the number of filters in Spring Security" is wrong - not because the number is wrong, but because the question has no fixed answer. "That config produced 11, and here is how I printed them" is an answer that cannot be faked.

The order is the part that bites

Across both chains the order is stable and worth learning:

DisableEncodeUrl -> WebAsyncManagerIntegration -> SecurityContextHolder -> HeaderWriter -> (Csrf) -> Logout -> (BasicAuthentication) -> RequestCacheAware -> SecurityContextHolderAwareRequest -> AnonymousAuthentication -> ExceptionTranslation -> Authorization

Three consequences follow directly from that list:

  • CsrfFilter runs before BasicAuthenticationFilter. A request can be rejected for CSRF before its credentials are ever checked, so valid credentials do not rescue a tokenless POST. Which status then reaches the client is a separate question - the CSRF topic measures a 401 that is really a protected /error page answering CsrfFilter's 403, not a consequence of this order.
  • AuthorizationFilter is last. Authorization can only run once identity exists, which is why role checks are the final gate rather than the first one.
  • ExceptionTranslationFilter sits just before it. It is the thing that turns an AuthenticationException into 401 and an AccessDeniedException into 403 for everything after it. Filters that run earlier write their own status - CsrfFilter answers a missing token with 403 without ever reaching it.

When to use multiple chains: when one application serves both a stateless JSON API and a browser UI. Give the API chain a securityMatcher("/api/**") with CSRF disabled and token authentication, and let a second chain handle the browser side with CSRF on and form login. @Order decides which is consulted first, and the first match wins - a catch-all chain placed first will swallow every request and the specific chain will never run.

Trade-off: filters are cheap but they are not free, and more importantly they are invisible in your source. Because security runs before the dispatcher servlet, a security failure never reaches your controller - which means @ControllerAdvice and ProblemDetail do not shape a 401 or a 403. Customising those responses needs an AuthenticationEntryPoint or an AccessDeniedHandler, which is a later topic in this chapter.

// ---- t2/T2.java ----
package t2;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.FilterChainProxy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.bind.annotation.*;
import jakarta.servlet.Filter;

@SpringBootApplication
@RestController
public class T2 {
  @GetMapping("/api/data") public String api() { return "data"; }
  @GetMapping("/page")     public String page() { return "page"; }

  @Bean @Order(1)                       // first chain: only /api/**
  SecurityFilterChain apiChain(HttpSecurity http) throws Exception {
    return http.securityMatcher("/api/**")
        .csrf(c -> c.disable())
        .authorizeHttpRequests(a -> a.anyRequest().authenticated())
        .httpBasic(b -> {})
        .build();
  }

  @Bean @Order(2)                       // second chain: everything else
  SecurityFilterChain webChain(HttpSecurity http) throws Exception {
    return http.authorizeHttpRequests(a -> a.anyRequest().permitAll()).build();
  }

  public static void main(String[] args) {
    System.setProperty("server.port", "0");
    try (ConfigurableApplicationContext ctx = SpringApplication.run(T2.class, args)) {
      FilterChainProxy proxy = ctx.getBean("springSecurityFilterChain", FilterChainProxy.class);
      System.out.println("total chains = " + proxy.getFilterChains().size());
      int i = 0;
      for (var chain : proxy.getFilterChains()) {
        System.out.println("chain[" + i + "] filters=" + chain.getFilters().size());
        for (Filter f : chain.getFilters()) System.out.println("    " + f.getClass().getSimpleName());
        i++;
      }
    }
  }
}

Project: Secure the Job Board API

This is the chapter's entry project, and it is deliberately not a new application. The Spring Data JPA chapter built a job board - companies, jobs, search with paging, close a job - and it had no authentication at all. Anyone who could reach the API could post a job or close someone else's. This project puts the lock on.

That is also the order an interview asks in: you built the API, you wired the database, how did you secure it?

The one honest difference from the previous chapter

The JPA chapter ran with no web layer; this chapter has a web layer and no database. So the entities here are a record in a Map rather than JPA entities. Nothing about the security is affected by that - the filter chain does not know or care what is behind the controller - but do not read this file as a replacement for the persistence work. The two halves are deliberately taught in the chapters that can actually run them.

The rules, and why each one is shaped that way

.requestMatchers(HttpMethod.GET,    "/api/jobs").permitAll()
.requestMatchers(HttpMethod.POST,   "/api/jobs").hasRole("RECRUITER")
.requestMatchers(HttpMethod.DELETE, "/api/jobs/**").hasRole("ADMIN")
.anyRequest().authenticated()

The matchers are method-aware. GET /api/jobs and POST /api/jobs are the same path with two completely different permissions, which is normal for a real API - reading a job feed is public, publishing to it is not. A path-only matcher cannot express that.

The order is specific to general. Each rule is narrower than the one after it, and anyRequest() is last. Written the other way round the application would not start - the authorizing-requests topic measures that.

The last line is a whitelist. anyRequest().authenticated() means /api/jobs/mine, which no rule mentions, is closed to anonymous callers by default. When someone adds /api/jobs/{id}/applicants next month and forgets the security config, it will be closed too. That is the entire argument for ending with authenticated().

csrf.disable() plus STATELESS is correct here and only because of the credential: HTTP Basic arrives in a header the browser never attaches by itself. Move the credential into a cookie and both decisions become wrong.

What the run proves

GET    /api/jobs        anonymous -> 200  []
POST   /api/jobs        anonymous -> 401
POST   /api/jobs        asha USER -> 403
POST   /api/jobs        rec  REC  -> 200  {"id":1,...,"postedBy":"rec"...}
GET    /api/jobs/mine   rec  REC  -> 200  [ {"id":1,...} ]
GET    /api/jobs/mine   asha USER -> 200  []
DELETE /api/jobs/1      rec  REC  -> 403
DELETE /api/jobs/1      boss ADM  -> 200  deleted

Read rows two and three together - they are the whole 401-versus-403 lesson in the shape it actually appears. The same POST, refused twice for different reasons: nobody gets 401, the wrong somebody gets 403.

And rows five and six: the same endpoint, the same status, different data. /api/jobs/mine is not protected by a rule at all - it is protected by the controller using Authentication.getName() to filter. That distinction matters: authorization rules decide whether you may call something; only your code can decide which rows you are allowed to see. A rule can never express "your own jobs".

Build it yourself

  1. Three users with BCrypt-encoded passwords - a candidate, a recruiter, an admin. Never {noop} outside a demo.
  2. The four rules above. Verify each one from two sides: the role that should pass and a role that should not.
  3. GET /api/jobs/mine filtered by Authentication.getName().
  4. Then break it on purpose and watch: change hasRole("RECRUITER") to hasAuthority("RECRUITER") and see every post fail with 403, because the stored authority is ROLE_RECRUITER.

When to use this shape: any API with public reads and privileged writes, which is most of them. Coarse rules on the chain, row-level decisions in code.

When NOT to: do not try to express ownership in authorizeHttpRequests. "Only the recruiter who posted this job may close it" is not a URL rule - it is either a @PreAuthorize with the argument, or a check in the service. Twisting matchers into ownership rules produces configuration nobody can read and holes nobody can see.

Trade-off: these rules are readable precisely because they are coarse, and coarse rules cannot protect a service method reached from a scheduled job or a second controller. That is the gap the next two projects and the method-security topic cover.

// ---- p16/P16.java ----
// Ch9 ka Job - bina JPA ke, kyunki is chapter ke env me koi DB nahi hai.
record Job(Long id, String title, String city, String postedBy, String status) {}

@GetMapping("/api/jobs")                                   // public feed
public Collection<Job> feed() { return JOBS.values(); }

@PostMapping("/api/jobs")                                  // recruiter only
public Job post(@RequestParam String title, @RequestParam String city, Authentication a) {
  long id = SEQ.incrementAndGet();
  Job j = new Job(id, title, city, a.getName(), "OPEN");   // owner = the caller
  JOBS.put(id, j);
  return j;
}

@GetMapping("/api/jobs/mine")                              // any logged-in user
public List<Job> mine(Authentication a) {
  return JOBS.values().stream().filter(j -> j.postedBy().equals(a.getName())).toList();
}

@DeleteMapping("/api/jobs/{id}")                           // admin only
public String delete(@PathVariable Long id) {
  return JOBS.remove(id) == null ? "not-found" : "deleted";
}

@Bean
SecurityFilterChain chain(HttpSecurity h) throws Exception {
  return h
      .csrf(c -> c.disable())                              // credential is a header, not a cookie
      .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
      .authorizeHttpRequests(a -> a
          .requestMatchers(HttpMethod.GET,    "/api/jobs").permitAll()
          .requestMatchers(HttpMethod.POST,   "/api/jobs").hasRole("RECRUITER")
          .requestMatchers(HttpMethod.DELETE, "/api/jobs/**").hasRole("ADMIN")
          .anyRequest().authenticated())                   // whitelist default
      .httpBasic(b -> {})
      .build();
}

@Bean PasswordEncoder enc() { return new BCryptPasswordEncoder(); }

@Bean UserDetailsService users(PasswordEncoder e) {
  return new InMemoryUserDetailsManager(
      User.withUsername("asha").password(e.encode("pw1")).roles("USER").build(),
      User.withUsername("rec").password(e.encode("pw2")).roles("RECRUITER").build(),
      User.withUsername("boss").password(e.encode("pw3")).roles("ADMIN").build());
}

Spring Securityinterview questions & answers

10 sample questions below — 204+ in the full bank inside.

After csrf(c -> c.disable()), what happens to the same POST that previously failed?

It behaves like any other request: with no credentials it returns 401, and with valid credentials it returns 200 - measured exactly that way. The CSRF check is simply gone from the chain, so the only thing deciding the outcome is authentication and then authorization. That contrast is the cleanest demonstration that the earlier 401 came from CSRF and not from the credentials, since the credentials never changed between the two runs. It also shows that disabling CSRF does not weaken authentication - it removes one specific defence against forged cross-site writes, nothing else.

In simple terms: Changing exactly one thing and re-measuring is what turns 'I think it was CSRF' into knowing, and it also bounds what disabling actually costs.

When do you use hasRole and when hasAuthority?

hasRole("X") is shorthand for hasAuthority("ROLE_X"): use it when your identities really are role-based and you control how they are loaded, so the ROLE_ prefix is guaranteed. hasAuthority("X") is an exact string match with no magic: use it whenever the prefix is not ROLE_ - JWT scopes (SCOPE_read), OAuth2 scopes, or fine-grained permissions like invoice:refund. The deciding question is never 'is this a role conceptually' but 'what string is actually in getAuthorities()', and printing that list settles it in one line.

In simple terms: Framing the choice around the stored string rather than the concept is what prevents both prefix bugs, and it is a one-sentence rule people can keep.

When do you need to configure CORS, and when is it pointless?

Configure it when a browser front end is served from a different origin than the API - a different domain, a different port, or http versus https. A single-page app on localhost:3000 calling an API on localhost:8080 is already a different origin, which is why almost every local development setup meets this. It is pointless for server-to-server calls, mobile apps or anything that is not a browser, because there is no browser enforcing the rule - adding a permissive policy to 'fix' a non-browser problem only widens what browsers are allowed to do, without fixing anything.

In simple terms: The port-counts-as-origin detail catches people out locally, and the 'pointless for non-browsers' half prevents a dangerous cargo-cult fix.

Is CSRF protection enabled by default, and how would you confirm it on a running application?

Yes - a chain that never mentions CSRF has it on. The measured demo's SecurityFilterChain calls neither csrf(...) nor disable(), and a POST without a token was rejected. Confirm it on a running application by printing the chain from FilterChainProxy and looking for CsrfFilter in the list: present means on, absent means something disabled it. That beats reading the configuration, because the configuration only records what you changed - the absence of any csrf line means the default, and the default is enabled.

In simple terms: The 'absence means enabled' inversion is what makes CSRF surprising, and printing the filter list resolves it without argument.

What does an Authentication object hold, and which part do you actually use in application code?

It holds the principal (who), the credentials (often cleared after authentication), the authorities (what they may do) and an isAuthenticated flag. In application code you almost always want getName() for the username and getAuthorities() for the permission check, because both are defined for every mechanism. getPrincipal() is typed differently per mechanism - a UserDetails for form login and Basic, a Jwt on a resource server chain, the plain string anonymousUser when anonymous - so casting it is the line that breaks when a second authentication mechanism is added.

In simple terms: The typed-principal trap is real and common, and the safe alternative (getName) is a one-word change most people have never been told about.

What happens if a custom filter forgets to call chain.doFilter(req, res)?

The request stops dead in the middle of the chain. The controller never runs, the remaining filters never run, and the client typically gets an empty 200 with no explanation - a response that looks like the endpoint returned nothing rather than like an error. It is the classic filter bug precisely because nothing throws. The rule is: a filter that merely inspects or enriches must always pass the request on, and a filter that wants to reject should do so explicitly by writing a status and then returning, so the intent is visible rather than implied by an omission.

In simple terms: An empty 200 is a uniquely confusing symptom because it does not look like a failure at all, and the cause is a missing line rather than a wrong one.

One application, one endpoint family, three callers. Predict the status codes and say why each is what it is.

With /public/** permitAll, /admin/** hasRole ADMIN and everything else authenticated: /public/ping anonymous is 200 because the rule allows it; /user/me anonymous is 401 because no identity was established; /user/me as a ROLE_USER is 200; /admin/panel as that same ROLE_USER is 403 because the identity is known and lacks the role; and /admin/panel as ROLE_ADMIN is 200. Rows two and four are the lesson: the same endpoint family refused twice, once for not knowing who you are and once for knowing exactly who you are.

In simple terms: Being able to predict all five without running them is the cleanest proof that the two questions are separate in the candidate's head.

Name the common authorization rules and say what each does, including one that surprises people.

permitAll() always allows; authenticated() requires any identity; hasRole("ADMIN") requires the ROLE_ADMIN authority; hasAuthority("SCOPE_read") requires that exact authority string; hasAnyRole(...) and hasAnyAuthority(...) accept a list; and denyAll() refuses everyone. The one that surprises people is denyAll() - it refuses an administrator too, which the measured run confirms with a ROLE_ADMIN caller getting 403. That makes it useful for an endpoint that exists but must never be reachable over HTTP, rather than as a stricter version of hasRole.

In simple terms: denyAll is routinely misread as 'deny normal users', and the measured 403 for an admin is the fastest correction.

What is CSRF, and which browser behaviour makes it possible?

Cross-Site Request Forgery: an attacker's page causes your browser to send a state-changing request to a site you are logged into, and the browser attaches your cookies automatically regardless of which site caused the request. So a form on evil.example can POST to bank.example/transfer and the bank sees a perfectly authenticated request. The attacker never reads the response and does not need to - the damage is the write. That one browser behaviour, automatic cookie attachment, is the whole basis of the attack and therefore the whole basis of when the defence is needed.

In simple terms: Anchoring the attack to automatic cookie attachment is what makes the later 'when can I disable it' question answerable from first principles.

What are the three parts of a JWT, and is the payload secret?

header.payload.signature, three base64url segments joined by dots. The header names the signing algorithm, the payload carries claims such as sub, exp and scope, and the signature is computed over the first two with a key. The payload is encoded, not encrypted - anyone holding the token can decode and read every claim with no key at all, which the demo demonstrates by printing the decoded payload. Two rules follow: never put anything secret in a JWT, and never trust an unverified token, because decoding is not verifying. What the signature buys is tamper detection, not confidentiality.

In simple terms: The encoded-versus-encrypted distinction is the single most consequential JWT misconception, and it governs what may go in a token at all.

194+ more Spring Security questions inside

Create a free account to read the full question bank, learn every topic, and practise with an AI mock interview.

Unlock all questions — free

Ready to practise Spring Security?

Unlock every topic free, then face an AI interviewer that asks follow-ups and grades your answers.