Get Current User ID in Spring Boot 3 → @AuthenticationPrincipal Guide | www.codegigs.app
You need the current user’s ID. Not just their username—the actual primary key from your database.
So you call SecurityContextHolder, get the principal, cast it, handle the null checks, and pray you didn’t miss an edge case. I did this for years. It’s ugly, hard to test, and brittle.
I’ve reviewed hundreds of codebases at www.codegigs.app, and this is the #1 spot where Spring Security code gets messy. There is a cleaner way. It involves one annotation and zero static method calls. Here is how you fix your controller logic forever.
Why is SecurityContextHolder So Painful?
We’ve all written this code. It’s the standard Stack Overflow answer from 2014.
It looks innocent enough. But try writing a unit test for it. You can’t just inject a mock; you have to mock a static thread-local context. If you forget to clear that context after the test? You just polluted every other test in your suite. I lost two days debugging a “flaky” build pipeline that turned out to be exactly this.
Here is the legacy way (don’t do this):
// The "Old School" Static Way
// Hard to test, tightly coupled
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null && auth.getPrincipal() instanceof CustomUserDetails) {
CustomUserDetails user = (CustomUserDetails) auth.getPrincipal();
Long userId = user.getId();
// finally do something...
}
It works, but it’s verbose. And frankly, your controller shouldn’t care about the mechanics of how the security context is stored. It just wants the user.
The Fix: @AuthenticationPrincipal
Spring Boot 3.2 and Security 6 make this trivial. Instead of reaching out to a static context, let Spring inject the user directly into your method arguments.
This blew my mind when I finally switched. It decouples your controller from the Security framework entirely. For unit tests, you just pass a standard object. No static mocking required.
The Basic Implementation
If you haven’t customized your UserDetails yet, start here.
// UserController.java
// Spring Boot 3.2.1
package app.codegigs.api;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class UserController {
@GetMapping("/me")
public String getCurrentUser(@AuthenticationPrincipal UserDetails user) {
// Spring handles the lookup and casting automatically
return "You are logged in as: " + user.getUsername();
}
}
Line 14 is the magic. Spring inspects the SecurityContext, grabs the Authentication object, extracts the principal, and injects it. If the user isn’t logged in, this might be null (or anonymous), but we’ll handle that in a second.
How to Get the User ID (Without a DB Call)
Here’s the real problem: UserDetails doesn’t have a getId() method. It only has getUsername().
Most developers solve this by taking the username and hitting the database again to find the ID. That’s a wasted query on every single request. At scale, that burns performance.
The production-grade pattern we teach at www.codegigs.app is to extend UserDetails. Store the ID in the session/token when they login, so it’s ready for you later.
1. Create a Custom UserDetails
// CustomUserDetails.java
package app.codegigs.security;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
public class CustomUserDetails implements UserDetails {
private final Long id;
private final String username;
private final String password;
private final Collection<? extends GrantedAuthority> authorities;
public CustomUserDetails(Long id, String username, String password,
Collection<? extends GrantedAuthority> authorities) {
this.id = id;
this.username = username;
this.password = password;
this.authorities = authorities;
}
public Long getId() {
return id;
}
// Standard UserDetails boilerplate below...
@Override
public String getUsername() { return username; }
@Override
public String getPassword() { return password; }
@Override
public Collection<? extends GrantedAuthority> getAuthorities() { return authorities; }
@Override
public boolean isAccountNonExpired() { return true; }
@Override
public boolean isAccountNonLocked() { return true; }
@Override
public boolean isCredentialsNonExpired() { return true; }
@Override
public boolean isEnabled() { return true; }
}
2. Inject the Custom Type
Now update your controller. You don’t need to cast anything. Spring does it for you.
// OrderController.java
// Accessing the ID without a database lookup
@GetMapping("/orders")
public List<Order> getMyOrders(@AuthenticationPrincipal CustomUserDetails user) {
// Look mom, no casting!
Long userId = user.getId();
return orderService.findOrdersByUserId(userId);
}
Line 6 is clean. It’s type-safe. And if you’re writing a test?
// Unit Test
controller.getMyOrders(new CustomUserDetails(1L, "dave", "pass", roles));
No Mockito.mockStatic. No nightmares.
Common Mistakes (I’ve Made All of Them)
1. The ClassCastException Surprise
If you have endpoints that allow anonymous access (like permitAll()), the principal is a String (“anonymousUser”), not your CustomUserDetails object.
If you use @AuthenticationPrincipal CustomUserDetails user on a public endpoint, Spring might throw a ClassCastException or inject null depending on your config. Someone on Stack Overflow hit this hard a few years back.
The Fix: Only use the specific type on secured endpoints. For public ones, use Object principal and check types manually, or rely on PreAuthorize.
2. Pollution in Service Layers
Don’t pass Authentication or UserDetails into your Service layer. That couples your business logic to Spring Security. Pass the Long userId or String username instead. Your service shouldn’t care if the ID came from a JWT, a session, or a CLI command.
What About SpEL?
Sometimes you don’t even need the controller code. If you just need to check if a user owns a resource, use SpEL directly in the security annotation. It’s cleaner.
@PreAuthorize("authentication.principal.id == #order.ownerId")
public void deleteOrder(Order order) {
repository.delete(order);
}
This blew my mind when I started using it. You can access your custom getters (like .id) directly in the expression.
This Is Just Step One
Getting the user ID is great, but what happens when that user needs to access a resource owned by someone else? Or when you need to handle JWT rotation?
We go deep into advanced authorization patterns—including ACLs and custom permission evaluators—in the Spring Security Masterclass at www.codegigs.app.
If you’re still confused about the difference between GrantedAuthority and Role, check out this breakdown next.
Build Secure Spring Apps the Right Way
Stop guessing with security configurations. Join 50,000+ developers mastering the Spring ecosystem.