Skip to content

Authorization Interview Questions & Answers

15 questions Updated 2026-06-26 Share:

Authorization in Spring Security — URL authorization rules, request matchers, method security with @PreAuthorize/@PostAuthorize, @Secured, SpEL access expressions, role hierarchies, the AccessDeniedException 403, and securing service vs web layers.

Read the in-depth guideAuthorization in Spring Security: URL and Method Rules(opens in new tab)
15 of 15

Inside authorizeHttpRequests you chain matchers to access rules. Spring evaluates them top-down and applies the first match, so order specific rules before the catch-all.

http.authorizeHttpRequests(auth -> auth
    .requestMatchers("/", "/public/**").permitAll()
    .requestMatchers("/admin/**").hasRole("ADMIN")
    .requestMatchers(HttpMethod.POST, "/api/**").hasAuthority("API_WRITE")
    .anyRequest().authenticated());        // catch-all LAST

Common rules are permitAll(), authenticated(), hasRole(), hasAnyRole(), hasAuthority(), and denyAll(). A missing anyRequest() can leave endpoints unguarded, so always end with a catch-all.

Rule of thumb: Order URL rules specific-to-general, end with anyRequest(), and remember only the first matching rule applies.

A request matcher selects which requests a rule applies to — by path pattern, HTTP method, or both. requestMatchers(...) accepts Ant-style patterns and an optional HttpMethod.

auth.requestMatchers("/api/orders/**").hasRole("USER")              // any method, path subtree
    .requestMatchers(HttpMethod.DELETE, "/api/orders/*").hasRole("ADMIN")  // method-specific
    .requestMatchers("/files/{name:.+}").permitAll();              // path variable

* matches one path segment, ** matches across segments. Method-specific matchers let you allow reads but restrict writes on the same path. Modern Spring Security resolves the matcher type (MVC/servlet) automatically.

Rule of thumb: Use requestMatchers with Ant patterns and optional methods to scope rules — * is one segment, ** is many, and method matchers separate read from write access.

Add @EnableMethodSecurity to a configuration class. This activates the annotation-based checks — @PreAuthorize, @PostAuthorize, @PreFilter, @PostFilter — which Spring enforces via an AOP proxy around the bean.

@Configuration
@EnableMethodSecurity        // prePostEnabled = true by default; secured/jsr250 optional
class MethodSecurityConfig { }

@Service
class ReportService {
    @PreAuthorize("hasRole('ANALYST')")
    Report generate(Long id) { ... }
}

Because it's proxy-based, the same caveats as @Transactional apply: the method must be public and called from another bean (self-invocation bypasses the check). @EnableMethodSecurity replaces the older @EnableGlobalMethodSecurity.

Rule of thumb: Turn on @EnableMethodSecurity, then annotate public service methods — it's proxy-based, so self-invocation skips the check.

@PreAuthorize evaluates a SpEL expression before the method runs — it can reference method arguments. @PostAuthorize evaluates after the method returns and can reference the return value via returnObject, vetoing the result if the check fails.

@PreAuthorize("hasRole('ADMIN') or #userId == authentication.name")
User get(String userId) { ... }

@PostAuthorize("returnObject.owner == authentication.name")
Document load(Long id) { ... }    // runs, then denies if not the owner

Use @PreAuthorize for the common case (cheaper — it blocks before doing work). Use @PostAuthorize only when the decision depends on the loaded object, accepting that the method body already ran.

Rule of thumb: @PreAuthorize guards on inputs before execution; @PostAuthorize guards on the returned object after — prefer pre unless the check needs the result.

All three gate method access but differ in power. @Secured (Spring) and @RolesAllowed (JSR-250) take plain role names only. @PreAuthorize takes a full SpEL expression, so it can combine roles, inspect arguments, and call beans.

@Secured("ROLE_ADMIN")                       // role list only, needs ROLE_ prefix
@RolesAllowed("ADMIN")                        // JSR-250 equivalent
@PreAuthorize("hasRole('ADMIN') and #id > 0") // expressions: roles + args + logic

@Secured and @RolesAllowed are off by default (enable via @EnableMethodSecurity(securedEnabled = true, jsr250Enabled = true)); @PreAuthorize is on by default and is the most flexible. Most teams standardize on @PreAuthorize.

Rule of thumb: Use @PreAuthorize for anything beyond a bare role check; @Secured/@RolesAllowed are simpler but role-only and must be explicitly enabled.

@PreAuthorize expressions use built-ins like hasRole, hasAuthority, hasAnyRole, isAuthenticated(), permitAll, plus the authentication and principal objects and method arguments via #paramName.

@PreAuthorize("hasRole('ADMIN') or #account.owner == principal.username")
void update(Account account) { ... }

@PreAuthorize("@accessChecker.canEdit(#id, authentication)")   // call a bean
void edit(Long id) { ... }

Referencing a @bean method (@accessChecker.canEdit(...)) lets you move non-trivial decisions into testable Java instead of cramming logic into a string. Keep expressions short; push complexity into a bean.

Rule of thumb: SpEL gives roles, the principal, and method args in one expression — and for real logic, delegate to a @bean method rather than writing a long expression.

They filter collections instead of allowing/denying the whole call. @PreFilter removes disallowed elements from a collection argument before the method runs; @PostFilter removes elements from the returned collection, using filterObject for the current element.

@PostFilter("filterObject.owner == authentication.name")
List<Document> myDocuments() { return repo.findAll(); }   // keeps only the caller's docs

@PreFilter("filterObject.amount < 10000")
void process(List<Payment> payments) { ... }              // drops large payments before processing

@PostFilter is convenient but loads everything then discards — for large datasets, filter in the query instead. Reserve these for small in-memory collections.

Rule of thumb: @PreFilter/@PostFilter prune collections element-by-element; great for small lists, but for big result sets filter in the database query, not after.

When an authenticated user lacks permission, Spring throws AccessDeniedException, the ExceptionTranslationFilter catches it, and the response becomes 403 Forbidden (vs 401 for unauthenticated). You customize it with an AccessDeniedHandler or an exception handler.

http.exceptionHandling(e -> e
    .accessDeniedHandler((req, res, ex) ->
        res.sendError(403, "Not allowed"))
    .authenticationEntryPoint((req, res, ex) ->
        res.sendError(401, "Authenticate first")));

401 means "we don't know who you are — log in"; 403 means "we know you, but you may not." For method-security denials in MVC you can also map AccessDeniedException in a @ControllerAdvice.

Rule of thumb: Authenticated-but-denied → 403 (AccessDeniedHandler); unauthenticated → 401 (AuthenticationEntryPoint) — don't conflate the two.

A RoleHierarchy lets a higher role imply lower ones, so an ADMIN automatically satisfies hasRole('USER') without granting both explicitly.

@Bean
RoleHierarchy roleHierarchy() {
    return RoleHierarchyImpl.fromHierarchy(
        "ROLE_ADMIN > ROLE_MANAGER \n ROLE_MANAGER > ROLE_USER");
}

Wire it into method security (via a MethodSecurityExpressionHandler) or web authorization. Without a hierarchy you'd write hasAnyRole('ADMIN','MANAGER','USER') everywhere; the hierarchy keeps rules simple and consistent.

Rule of thumb: Define a RoleHierarchy so senior roles inherit junior permissions — it removes repetitive hasAnyRole lists across the app.

Both — they're complementary layers of defense. URL rules are a coarse, centralized gate at the edge; method security puts fine-grained checks next to the business logic, so an endpoint that forgets a URL rule is still protected.

// URL layer: broad gate
auth.requestMatchers("/admin/**").hasRole("ADMIN");
// Method layer: precise, logic-adjacent
@PreAuthorize("hasRole('ADMIN')") void deleteUser(Long id) { ... }

Method security also protects code reached by non-HTTP paths (scheduled jobs, messaging) that URL rules never see. Defense in depth means a single misconfiguration doesn't expose the operation.

Rule of thumb: Use URL rules for edge gatekeeping and method security for logic-adjacent, transport-independent checks — layer them, don't choose one.

Service methods represent the actual business operations and are reachable from many entry points — REST controllers, GraphQL, message listeners, scheduled tasks. Securing there guarantees the rule holds regardless of how the method is invoked.

@Service
class AccountService {
    @PreAuthorize("hasRole('ADMIN')")
    void closeAccount(Long id) { ... }   // enforced from controller, job, or listener alike
}

Securing only the controller leaves the operation exposed if another caller invokes the service directly. The controller still benefits from coarse URL rules, but the authoritative check belongs on the service.

Rule of thumb: Put the authoritative authorization check on the service method — it's the single chokepoint every caller passes through.

For "can this user edit this specific record" decisions, use hasPermission(...) backed by a custom PermissionEvaluator, or delegate to a bean method in the expression. This moves instance-level logic out of the annotation.

@PreAuthorize("hasPermission(#id, 'Document', 'EDIT')")
void edit(Long id) { ... }

@Component
class DocumentPermissionEvaluator implements PermissionEvaluator {
    public boolean hasPermission(Authentication a, Serializable id, String type, Object perm) {
        return docRepo.isOwner((Long) id, a.getName());   // your domain rule
    }
    // ...
}

Spring's full ACL module exists for rich per-object permissions but is heavyweight; a custom PermissionEvaluator or a @bean check covers most needs.

Rule of thumb: For per-record access, use hasPermission with a custom PermissionEvaluator (or a @bean method) instead of stuffing ownership logic into URL rules.

Since Spring Security 6 the AuthorizationManager is the unified authorization API behind both URL and method security, replacing the old AccessDecisionManager/voter model. It returns an AuthorizationDecision (granted or not) for a request or invocation.

auth.requestMatchers("/reports/**").access(
    (authentication, context) ->
        new AuthorizationDecision(authentication.get().getName().startsWith("svc-")));

.access(AuthorizationManager) lets you plug fully custom logic into URL rules, and all the hasRole/hasAuthority helpers are just built-in AuthorizationManagers underneath. It's simpler and more testable than the legacy voters.

Rule of thumb: AuthorizationManager is the one authorization abstraction in Spring Security 6 — use .access(...) to drop custom decision logic into URL rules.

Mark the open paths permitAll() and finish with anyRequest().authenticated(). permitAll still runs the filter chain but demands no credentials, while the catch-all locks down everything else.

http.authorizeHttpRequests(auth -> auth
    .requestMatchers("/", "/login", "/css/**", "/actuator/health").permitAll()
    .anyRequest().authenticated());

Put permitAll rules before the catch-all (first-match wins). Static assets, the login page, and health checks are typical public paths; everything unlisted falls through to authenticated().

Rule of thumb: permitAll the few public paths first, then anyRequest().authenticated() to secure the rest by default.

Use spring-security-test: @WithMockUser (or @WithMockUser(roles="ADMIN")) sets up an authenticated principal, and MockMvc's SecurityMockMvcRequestPostProcessors (with(user(...)), with(csrf())) drive secured endpoints.

@Test
@WithMockUser(roles = "ADMIN")
void adminCanDelete() throws Exception {
    mockMvc.perform(delete("/api/users/1").with(csrf()))
           .andExpect(status().isNoContent());
}

@Test
@WithMockUser(roles = "USER")
void userIsForbidden() throws Exception {
    mockMvc.perform(delete("/api/users/1").with(csrf()))
           .andExpect(status().isForbidden());      // 403
}

Test both the allowed and denied paths — asserting a 403 for the wrong role is as important as asserting success for the right one.

Rule of thumb: Drive authorization tests with @WithMockUser and MockMvc; always assert the denied (403) case, not just the happy path.

More ways to practice

The self-quiz is live. Join our channel for updates, new content & tech tips.

Join our WhatsApp Channel