Accessing the Current User

Read the signed-in user with @AuthenticationPrincipal, carry your own user id in the principal, and keep the security context on other threads.

Syntax@AuthenticationPrincipal AppUserPrincipal user @AuthenticationPrincipal(expression = "id()") Long userId Authentication authentication // controller parameter SecurityContextHolder.getContext().getAuthentication()

After authentication, the current user lives in the SecurityContextHolder as an Authentication object. Its getName() is the username, getAuthorities() the roles and permissions, and getPrincipal() the user object itself: a UserDetails for username and password login, a Jwt for a resource server.

In controllers: method parameters

Spring MVC resolves several parameter types for you, so you rarely touch the holder directly:

@GetMapping("/api/me")
MeResponse me(@AuthenticationPrincipal AppUserPrincipal user) { ... }

@GetMapping("/api/orders")
List<Order> mine(Authentication authentication) { ... }

@AuthenticationPrincipal (from org.springframework.security.core.annotation) gives you the principal cast to your own type. Its expression attribute can pick out one property, such as @AuthenticationPrincipal(expression = "id()") for a record principal (call the accessor as a method). @CurrentSecurityContext does the same for the whole security context. If the request is anonymous, the parameter is null, which is another reason to protect the URL as well.

Carry an id, not just a name

The built-in User class only knows a username. Most code needs the database id, a tenant or a display name, and looking the user up again on every request is wasteful. Return your own principal type from UserDetailsService, a small record implementing UserDetails, and read it with @AuthenticationPrincipal. Do not put the JPA entity itself in the principal: it is long-lived, may be serialised into the session, and lazy associations fail outside a transaction.

In services: pass it in

SecurityContextHolder.getContext().getAuthentication() works anywhere on the request thread, but a service that reads it hides a dependency and is awkward to test. Prefer reading the user in the controller and passing the id to the service. Keep the static call for cross-cutting code such as auditing.

Other threads

The holder is thread-local. Work handed to @Async methods, CompletableFuture.supplyAsync or your own executor runs with an empty context, so the user is null and method security denies the call. Declare a TaskDecorator bean that wraps each task in DelegatingSecurityContextRunnable: Boot applies it to its own applicationTaskExecutor, so @Async methods get the user and pool sizing and the virtual-thread switch keep working. CompletableFuture.supplyAsync without an executor uses the common pool, which no decorator touches, so pass it the injected applicationTaskExecutor. Declaring your own executor bean instead (for example a DelegatingSecurityContextAsyncTaskExecutor) makes Boot's auto-configured executor back off entirely. Often clearer still: pass the user id as an argument. Scheduled jobs and message listeners have no user at all; design those code paths to run without one.

Never trust a user id sent by the client in the body or path to mean "the caller". Take identity from the security context; take only the target resource from the request.

Example

Example · java
import java.util.Collection;
import java.util.List;
import org.springframework.jdbc.core.simple.JdbcClient;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

// Our own principal: everything controllers need, nothing lazy.
record AppUserPrincipal(long id, String email, String displayName, String passwordHash, String role)
        implements UserDetails {

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return List.of(new SimpleGrantedAuthority("ROLE_" + role));
    }

    @Override
    public String getPassword() {
        return passwordHash;
    }

    @Override
    public String getUsername() {
        return email;
    }
}

@Service
class AppUserDetailsService implements UserDetailsService {
    private final JdbcClient jdbc;

    AppUserDetailsService(JdbcClient jdbc) {
        this.jdbc = jdbc;
    }

    @Override
    public UserDetails loadUserByUsername(String email) {
        return jdbc.sql("""
                select id, email, display_name, password_hash, role
                from app_user where email = :email""")
                .param("email", email)
                .query(AppUserPrincipal.class)
                .optional()
                .orElseThrow(() -> new UsernameNotFoundException("not found"));
    }
}

record MeResponse(long id, String email, String displayName) {
}

@RestController
public class MeController {

    @GetMapping("/api/me")
    MeResponse me(@AuthenticationPrincipal AppUserPrincipal user) {
        // The password hash stays inside the principal and never reaches JSON.
        return new MeResponse(user.id(), user.email(), user.displayName());
    }
}

When to use it

  • Return the signed-in user's profile from GET /api/me without trusting any id sent by the client.
  • Stamp created-by and updated-by columns in an audit trail from any layer of the application.
  • Generate a report on a background thread that still knows which user requested it.

More examples

A @CurrentUser shortcut

<code>@AuthenticationPrincipal</code> works as a meta-annotation, so a project-specific <code>@CurrentUserId</code> hides the expression string and makes controller signatures read naturally. The service receives a plain <code>long</code>, so it can be unit tested without any security setup. The path has no id in it at all: there is nothing for a caller to tamper with.

Example · java
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.List;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@AuthenticationPrincipal(expression = "id()")
@interface CurrentUserId {
}

record OrderSummary(long id, String status) {
}

interface OrderQueries {
    List<OrderSummary> forCustomer(long customerId);
}

@RestController
public class MyOrdersController {
    private final OrderQueries orders;

    MyOrdersController(OrderQueries orders) {
        this.orders = orders;
    }

    @GetMapping("/api/me/orders")
    List<OrderSummary> myOrders(@CurrentUserId Long userId) {
        // The id comes from the security context, never from the request.
        return orders.forCustomer(userId);
    }
}

Reading the context outside controllers

Auditing is the classic case for the static holder: it applies everywhere and threading a user parameter through every method would be noise. The checks matter. The authentication is <code>null</code> on threads with no context, such as scheduled jobs, and an anonymous user is an <code>AnonymousAuthenticationToken</code> that reports itself as authenticated. Falling back to <code>system</code> keeps the audit row meaningful in both cases.

Example · java
import java.time.Instant;
import java.util.Optional;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;

record AuditEntry(String actor, String action, Instant at) {
}

@Component
public class AuditTrail {

    public AuditEntry record(String action) {
        return new AuditEntry(currentActor().orElse("system"), action, Instant.now());
    }

    private Optional<String> currentActor() {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        if (auth == null || !auth.isAuthenticated() || auth instanceof AnonymousAuthenticationToken) {
            return Optional.empty();
        }
        return Optional.of(auth.getName());
    }
}

Keeping the user on async threads

Without the decorator, <code>getAuthentication()</code> returns <code>null</code> on the worker thread and the method throws a <code>NullPointerException</code>, or is denied by method security. <code>DelegatingSecurityContextRunnable</code> captures the context when the task is submitted and puts the worker thread's previous, normally empty, context back when the task ends, so it does not leak onto the next task that reuses the thread. Using a <code>TaskDecorator</code> rather than your own executor bean matters: any <code>Executor</code> bean you declare makes Boot's <code>applicationTaskExecutor</code> back off, taking <code>spring.task.execution.*</code> and <code>spring.threads.virtual.enabled</code> with it. Passing the user id as a method argument avoids the problem entirely and is often the simpler design.

Example · java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.TaskDecorator;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.security.concurrent.DelegatingSecurityContextRunnable;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;

@Configuration
@EnableAsync
class AsyncSecurityConfig {

    // Boot passes TaskDecorator beans to the builders behind its applicationTaskExecutor,
    // so the auto-configured executor (and spring.task.execution.* settings) stays in place.
    @Bean
    TaskDecorator securityContextTaskDecorator() {
        // decorate() runs on the submitting thread, so the caller's context is captured here.
        return DelegatingSecurityContextRunnable::new;
    }
}

@Service
public class ReportService {

    @Async
    public void buildReport(long reportId) {
        String requestedBy = SecurityContextHolder.getContext().getAuthentication().getName();
        // ... generate the report and record who asked for it
    }
}

Discussion

  • Be the first to comment on this lesson.