package com.example.security;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface CurrentUser {
}
package com.example.security;
import org.springframework.core.MethodParameter;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
@Component
public class CurrentUserArgumentResolver implements HandlerMethodArgumentResolver {
private static final String ANONYMOUS = "anonymousUser";
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(CurrentUser.class)
&& AuthUser.class.isAssignableFrom(parameter.getParameterType());
}
@Override
public Object resolveArgument(MethodParameter parameter,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || !authentication.isAuthenticated()) {
return null;
}
Object principal = authentication.getPrincipal();
if (principal == null || ANONYMOUS.equals(principal)) {
return null;
}
return (AuthUser) principal;
}
}
package com.example.config;
import com.example.security.CurrentUserArgumentResolver;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.List;
@Configuration
public class WebConfig implements WebMvcConfigurer {
private final CurrentUserArgumentResolver currentUserArgumentResolver;
public WebConfig(CurrentUserArgumentResolver currentUserArgumentResolver) {
this.currentUserArgumentResolver = currentUserArgumentResolver;
}
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(currentUserArgumentResolver);
}
}
package com.example.web;
import com.example.security.AuthUser;
import com.example.security.CurrentUser;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/account")
public class AccountController {
private final ProfileService profileService;
public AccountController(ProfileService profileService) {
this.profileService = profileService;
}
@GetMapping("/me")
public ResponseEntity<ProfileView> me(@CurrentUser AuthUser user) {
if (user == null) {
return ResponseEntity.status(401).build();
}
return ResponseEntity.ok(profileService.viewFor(user.getId()));
}
@PutMapping("/me")
public ResponseEntity<ProfileView> updateProfile(@CurrentUser AuthUser user,
@RequestBody ProfileUpdate update) {
ProfileView updated = profileService.update(user.getId(), update);
return ResponseEntity.ok(updated);
}
}
This snippet shows the idiomatic Spring MVC pattern for injecting the current authenticated user directly into a controller method as a typed argument, instead of reaching into SecurityContextHolder from inside every handler. The mechanism is a HandlerMethodArgumentResolver, the same SPI Spring uses to resolve @RequestParam, @PathVariable, and @RequestBody — custom resolvers plug into that pipeline so controllers stay clean and testable.
The @CurrentUser annotation is a small marker with @Target(ElementType.PARAMETER) so it can only decorate method arguments. It carries no behavior itself; it exists so the resolver can identify which parameters it is responsible for. Keeping it a distinct annotation (rather than reusing a type check) means intent is explicit at the call site and the resolver never accidentally hijacks an unrelated AuthUser parameter.
In CurrentUserArgumentResolver, supportsParameter returns true only when the parameter is annotated with @CurrentUser and its type is AuthUser, which is how Spring decides to route resolution here. resolveArgument then pulls the Authentication from the SecurityContext. It deliberately guards against the anonymous case: if there is no authentication, it is not authenticated, or the principal is the literal string "anonymousUser", the method returns null rather than a bogus user. The happy path casts the principal to the application's AuthUser and returns it. Casting the principal assumes the security filter chain populated the context with an AuthUser — a fair assumption when a JWT filter builds that principal during authentication.
WebConfig wires the resolver in through WebMvcConfigurer.addArgumentResolvers; without this registration the annotation is inert. Because the resolver is a Spring bean, it can hold collaborators like repositories if richer lookups are needed.
Finally, AccountController demonstrates the payoff: me and updateProfile simply declare @CurrentUser AuthUser user and receive a fully resolved principal. A subtle pitfall is that returning null for anonymous requests means endpoints must still be protected by Spring Security; the resolver is a convenience, not an authorization boundary. This approach shines when many controllers need the current user and the team wants to avoid repetitive boilerplate and brittle manual casts.
Related snips
payload = {
sub: user.id,
iss: 'https://auth.example.com',
aud: 'codesnips-api',
exp: 15.minutes.from_now.to_i,
iat: Time.now.to_i,
JWT issuance and verification without common footguns
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
createdAt: String!
GraphQL API with Spring Boot
class SignupForm
include ActiveModel::Model
include ActiveModel::Attributes
attribute :account_name, :string
attribute :email, :string
Shallow Controller, Deep Params: Form Object Pattern
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["form"]
static values = { delay: { type: Number, default: 250 } }
Debounced live search with Stimulus + Turbo Streams
package com.example.starter.config;
import com.example.starter.properties.CustomProperties;
import com.example.starter.service.CustomService;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
Custom Spring Boot starters
#!/usr/bin/env bash
set -euo pipefail
export VAULT_ADDR="https://vault.internal:8200"
export VAULT_TOKEN="${VAULT_TOKEN:?missing VAULT_TOKEN}"
Secrets management with environment isolation and Vault
Share this code
Here's the card — post it anywhere.