I'm new to Spring development and I use Spring Security for JWT authentication in my application.
It is already configured and works fine, but the only messy thing is unpacking the Principal in each API request mapping. I only encode the user UUID in a JWT payload, but I need the entire User entity fetched from database in each request mapping.
Currently my code looks like:
#GetMapping("/something")
public SomeResponse someMethod(Authentication authentication) {
CustomUserDetails userDetails = (CustomUserDetails) authentication.getPrincipal();
MyUserEntity user = userService.findByUuid(userDetails.getUuid());
// ...
}
But I want to create some kind of a middleware so I'll be able to call findByUuid before the controller receives the request and then pass the entity to Spring to inject it, so the mapping code will look like:
#GetMapping("/some")
public SomeResponse someMethod(MyUserEntity user) {
// ...
}
I've searched for the same problem and the only idea I found was creating a filter which performs the user lookup by their UUID and setting the request attribute:
#Component
public class UserFilter extends OncePerRequestFilter {
#Override
protected void doFilterInternal(
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
request.setAttribute("user", new User("Jerry"));
filterChain.doFilter(request, response);
}
}
And then injecting the WebRequest into each mapping:
#GetMapping("/current-user")
public String getCurrentUser(WebRequest request) {
var user = (User) request.getAttribute("user", WebRequest.SCOPE_REQUEST);
return user.getUsername();
}
But it still doesn't look like a good approach as it forces me to repeat the same line for each of my 50 API methods.
Is there a way to manipulate the arguments injected to a request mapping by Spring?
Thanks to #M.Deinum, I was able to set up my own HandlerMethodArgumentsResolver component which can provide the required argument:
#Component
#RequiredArgsConstructor
public class AuthenticatedUserArgumentResolver implements HandlerMethodArgumentResolver {
private final UserService userService;
#Override
public boolean supportsParameter(#NonNull MethodParameter parameter) {
return parameter.getParameterType().equals(MyUserEntity.class);
}
#Override
public Object resolveArgument(#NonNull MethodParameter parameter, ModelAndViewContainer mavContainer, #NonNull NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
CustomUserDetails userDetails = (CustomUserDetails) auth.getPrincipal();
return userService.findByUuid(userDetails.getUuid());
}
}
And use it as expected:
#GetMapping("/some")
public SomeResponse someMethod(MyUserEntity user) {
// ...
}
For learning purposes, I have made a custom authentication system where I pass a token from the client to the server through the Authorization header.
In the server side, I'd like to know if it's possible to create in the interceptor, before the request reaches a method in the controller, an User object with the email from the token as a property, and then pass this user object to every request where I require it.
This what I'd like to get, as an example:
#RestController
public class HelloController {
#RequestMapping("/")
public String index(final User user) {
return user.getEmail();
}
}
public class User {
private String email;
}
Where user is an object that I created in the pre-interceptor using the request Authorization header and then I can pass, or not, to any method in the RestController.
Is this possible?
#Recommended solution
I would create a #Bean with #Scope request which would hold the user and then put the appropriate entity into that holder and then take from that holder inside the method.
#Component
#Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class CurrentUser {
private User currentUser;
public User getCurrentUser() {
return currentUser;
}
public void setCurrentUser(User currentUser) {
this.currentUser = currentUser;
}
}
and then
#Component
public class MyInterceptor implements HandlerInterceptor {
private CurrentUser currentUser;
#Autowired
MyInterceptor(CurrentUser currentUser) {
this.currentUser = currentUser;
}
#Override
public boolean preHandle(
HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
this.currentUser.setCurrentUser(new User("whatever"));
return true;
}
}
and in the Controller
#RestController
public class HelloController {
private CurrentUser currentUser;
#Autowired
HelloController(CurrentUser currentUser) {
this.currentUser = currentUser;
}
#RequestMapping("/")
public String index() {
return currentUser.getCurrentUser().getEmail();
}
}
#Alternative solution
In case your object that you would like to have, only contains one field, you can just cheat on that and add that field to the HttpServletRequest parameters and just see the magic happen.
#Component
public class MyInterceptor implements HandlerInterceptor {
#Override
public boolean preHandle(
HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//TRY ONE AT THE TIME: email OR user
//BOTH SHOULD WORK BUT SEPARATELY OF COURSE
request.setAttribute("email", "login#domain.com");
request.setAttribute("user", new User("login#domain.com"));
return true;
}
}
You can use a local thread context object as follows - which will be handling one parameter per request thread (thread safe):
public abstract class LoggedUserContext {
private static ThreadLocal<User> currentLoggedUser = new ThreadLocal<>();
public static void setCurrentLoggedUser(User loggedUser) {
if (currentLoggedUser == null) {
currentLoggedUser = new ThreadLocal<>();
}
currentLoggedUser.set(loggedUser);
}
public static User getCurrentLoggedUser() {
return currentLoggedUser != null ? currentLoggedUser.get() : null;
}
public static void clear() {
if (currentLoggedUser != null) {
currentLoggedUser.remove();
}
}
}
Then in the interceptor prehandle function:
LoggedUserContext.setCurrentLoggedUser(loggedUser);
And in the interceptor postHandler function:
LoggedUserContext.clear();
From any other place:
User loggedUser = LoggedUserContext.getCurrentLoggedUser();
How to access request.setattribute inside spring custom validator class. i need to set these values in jsp side I am trying something like below
#Component
public class ProductSearchValidator implements Validator {
#Override
public boolean supports(Class<?> clazz) {
return Product.class.isAssignableFrom(clazz);
}
#Override
public void validate(Object target, Errors errors) {
Product product = (Product) target;
String name = product.getName();
String cod="Validated";
request.setAttribute("isVal",cod);
}
}
You can access request this way
RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
if (attrs instanceof ServletRequestAttributes) {
HttpServletRequest request = ((ServletRequestAttributes)attrs).getRequest();
}
I am using Spring Boot 1.4.1 which includes spring-web-4.3.3. I have a class annotated with #ControllerAdvice and methods annotated with #ExceptionHandler to handle exceptions thrown by the service code. When handling these exceptions, I would like to log the #RequestBody that was part of the request for PUT and POST operations so I can see the request body that caused the problem which in my case is crucial for diagnosis.
Per Spring Docs the method signature for #ExceptionHandler methods can include various things including the HttpServletRequest. The request body can normally be obtained from here via getInputStream() or getReader(), but if my controller methods parse the request body like "#RequestBody Foo fooBody" as all of mine do, the HttpServletRequest's input stream or reader is already closed by the time my exception handler method is called. Essentially the request body has already been read by Spring, similar to the issue described here. It is a common problem working with servlets that the request body can only be read once.
Unfortunately #RequestBody is not one of the options available for the exception handler method, if it were then I could use that.
I can add an InputStream to the exception handler method, but that ends up being the same thing as the HttpServletRequest's InputStream and so has the same issue.
I also tried getting the current request with ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest() which is another trick for getting the current request, but this ends up being the same HttpServletRequest that Spring passes into the exception handler method and so has the same problem.
I have read about a few solutions like this and this that involve inserting a custom request wrapper in the filter chain that will read the contents of the request and cache them so they can be read more than once. I don't like this solution because I don't want to interrupt the entire filter/request/response chain (and potentially introduce performance or stability problems) just to implement logging, and if I have any large requests such as uploaded documents (which I do), I don't want to cache that in memory. Besides, Spring probably has the #RequestBody cached somewhere already if I could only find it.
Incidentally many solutions recommend using the ContentCachingRequestWrapper Spring class but in my experience this does not work. Aside from not being documented, looking at its source code it looks like it only caches the parameters, but not the request body. Trying to get the request body from this class always results in an empty string.
So I am looking for any other options that I may have missed. thanks for reading.
Accepted answer creates a new POJO to pass things around, but the same behaviour can be achieved without creating additional objects by reusing the http request.
Example code for Controller mapping:
public ResponseEntity savePerson(#RequestBody Person person, WebRequest webRequest) {
webRequest.setAttribute("person", person, RequestAttributes.SCOPE_REQUEST);
And later in the ExceptionHandler class / method you can use:
#ExceptionHandler(Exception.class)
public ResponseEntity exceptionHandling(WebRequest request,Exception thrown) {
Person person = (Person) request.getAttribute("person", RequestAttributes.SCOPE_REQUEST);
You can reference the request body object to a request-scoped bean. And then inject that request-scoped bean in your exception handler to retrieve the request body (or other request-context beans that you wish to reference).
// #Component
// #Scope("request")
#ManagedBean
#RequestScope
public class RequestContext {
// fields, getters, and setters for request-scoped beans
}
#RestController
#RequestMapping("/api/v1/persons")
public class PersonController {
#Inject
private RequestContext requestContext;
#Inject
private PersonService personService;
#PostMapping
public Person savePerson(#RequestBody Person person) throws PersonServiceException {
requestContext.setRequestBody(person);
return personService.save(person);
}
}
#ControllerAdvice
public class ExceptionMapper {
#Inject
private RequestContext requestContext;
#ExceptionHandler(PersonServiceException.class)
protected ResponseEntity<?> onPersonServiceException(PersonServiceException exception) {
Object requestBody = requestContext.getRequestBody();
// ...
return responseEntity;
}
}
You should be able to get the content of the request body by using the RequestBodyAdvice interface. If you implement this on a class annotated with #ControllerAdvice it should be picked up automatically.
To get other request information like the HTTP method and query params I'm using an interceptor. I'm capturing all this request info for error reporting in a ThreadLocal variable which I clear on the afterCompletion hook in that same interceptor.
The class below implements this and can be used in your ExceptionHandler to get all request information:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdvice;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.Map;
#ControllerAdvice
public class RequestInfo extends HandlerInterceptorAdapter implements RequestBodyAdvice {
private static final Logger logger = LoggerFactory.getLogger(RequestInfo.class);
private static final ThreadLocal<RequestInfo> requestInfoThreadLocal = new ThreadLocal<>();
private String method;
private String body;
private String queryString;
private String ip;
private String user;
private String referrer;
private String url;
public static RequestInfo get() {
RequestInfo requestInfo = requestInfoThreadLocal.get();
if (requestInfo == null) {
requestInfo = new RequestInfo();
requestInfoThreadLocal.set(requestInfo);
}
return requestInfo;
}
public Map<String,String> asMap() {
Map<String,String> map = new HashMap<>();
map.put("method", this.method);
map.put("url", this.url);
map.put("queryParams", this.queryString);
map.put("body", this.body);
map.put("ip", this.ip);
map.put("referrer", this.referrer);
map.put("user", this.user);
return map;
}
private void setInfoFromRequest(HttpServletRequest request) {
this.method = request.getMethod();
this.queryString = request.getQueryString();
this.ip = request.getRemoteAddr();
this.referrer = request.getRemoteHost();
this.url = request.getRequestURI();
if (request.getUserPrincipal() != null) {
this.user = request.getUserPrincipal().getName();
}
}
public void setBody(String body) {
this.body = body;
}
private static void setInfoFrom(HttpServletRequest request) {
RequestInfo requestInfo = requestInfoThreadLocal.get();
if (requestInfo == null) {
requestInfo = new RequestInfo();
}
requestInfo.setInfoFromRequest(request);
requestInfoThreadLocal.set(requestInfo);
}
private static void clear() {
requestInfoThreadLocal.remove();
}
private static void setBodyInThreadLocal(String body) {
RequestInfo requestInfo = get();
requestInfo.setBody(body);
setRequestInfo(requestInfo);
}
private static void setRequestInfo(RequestInfo requestInfo) {
requestInfoThreadLocal.set(requestInfo);
}
// Implementation of HandlerInterceptorAdapter to capture the request info (except body) and be able to add it to the report in case of an error
#Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
RequestInfo.setInfoFrom(request);
return true;
}
#Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception exception) {
RequestInfo.clear();
}
// Implementation of RequestBodyAdvice to capture the request body and be able to add it to the report in case of an error
#Override
public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
return true;
}
#Override
public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
return inputMessage;
}
#Override
public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
RequestInfo.setBodyInThreadLocal(body.toString());
return body;
}
#Override
public Object handleEmptyBody(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
return body;
}
}
Just an enhancement to quintencls answer
I got request body and can use it anywhere inside exception handler class.
#ControllerAdvice
public class CustomErrorHandler extends ResponseEntityExceptionHandler implements RequestBodyAdvice {
...
private Object reqBody;
...
#ExceptionHandler(NoSuchElementException.class)
public ResponseEntity<Object> handleNoSuchElementException(final NoSuchElementException ex,
final WebRequest request) {
System.out.println("===================================" + reqBody);
return handleNotFoundException(ex, request);
}
...
#Override
public boolean supports(MethodParameter methodParameter, Type targetType,
Class<? extends HttpMessageConverter<?>> converterType) {
return true;
}
#Override
public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType,
Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
return inputMessage;
}
#Override
public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType,
Class<? extends HttpMessageConverter<?>> converterType) {
// capture request body here to use in our controller advice class
this.reqBody = body;
return body;
}
#Override
public Object handleEmptyBody(Object body, HttpInputMessage inputMessage, MethodParameter parameter,
Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
return body;
}
}
Here is a solution in Kotlin syntax that I used for some fields validation control.
I needed to enhance the default handleMethodArgumentNotValid(...) method from the #RestControllerAdvice, to systematically log a field that was embedded in that same request body object.
override fun handleMethodArgumentNotValid(e: MethodArgumentNotValidException, headers: HttpHeaders, status: HttpStatus, request: WebRequest): ResponseEntity<Any> {
val error = e.bindingResult.fieldErrors.first()
val requestBody = try {
val field = error.javaClass.getDeclaredField("violation").apply { trySetAccessible() }
((field[error] as ConstraintViolationImpl<Any>).rootBean as MyRequestBodyObject)
} catch (ex : Exception) {
//do some failsafe here
}
}
See here: https://stackoverflow.com/a/61813076/1036433 - for a clean way to have access to the HttpServerRequest.
This instance of class AuthorizationRequest is created during HTTP request, params are sent in query string.
#RequestMapping(value = "/authorize", method = {RequestMethod.GET, RequestMethod.POST})
public String authorize(
#Valid AuthorizationRequest authorizationRequest,
BindingResult result
) {
I would like to use this code, this is an example parameter from AuthorizationRequest class:
#NotEmpty
#JsonProperty("client_id")
private String clientId;
but new instance has a filed clientId empty, because in query string there is a value for this parameter under client_id parameter.
Is there some way how to tell Spring which parameter from HTTP request should use for one particular field of created instance? I need to solve problem with different naming clientId andclient_id`.
What you need is a setter to handle each kind of clientId. Keep in mind that if both clientId and client_id is specified that it is unknown which will take precedence.
//These methods will allow clientId or client_id
to be used as arguments setting the same field this.clientId
public void setClient_id(String client_id) {
this.clientId = client_id;
}
public void setClientId(String client_id) {
this.clientId = client_id;
}
I tested this with a post and a get
get - http://localhost:8080/authorize?clientId=2&username=someusername
get - http://localhost:8080/authorize?client_id=2&username=someusername
post - http://localhost:8080/authorize
Content-Type: application/x-www-form-urlencoded
Body: clientId=2&username=someusername
or Body: client_id=2&username=someusername
I was only able to have #JsonProperty("client_id") to be recognized when I annotated AuthorizationRequest with #RequestBody and then used application/json instead of application/x-www-form-urlencoded
I found the solution with own implementation of org.springframework.web.method.supportHandlerMethodArgumentResolver.
Resolver implementation:
public class AuthorizationRequestResolver implements HandlerMethodArgumentResolver {
private static Logger LOG = Logger.getLogger(AuthorizationRequestResolver.class);
#Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.getParameterType().equals(AuthorizationRequest.class);
}
#Override
public Object resolveArgument(MethodParameter parameter,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) throws Exception {
HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
AuthorizationRequest authRequest = mapFromServletRequest(request);
return authRequest;
}
private AuthorizationRequest mapFromServletRequest(HttpServletRequest request) {
AuthorizationRequest authorizationRequest = new AuthorizationRequest();
authorizationRequest.setClientId(request.getParameter("client_id"));
authorizationRequest.setRedirectUri(request.getParameter("request_uri"));
authorizationRequest.setResponseType(request.getParameter("response_type"));
authorizationRequest.setScope(request.getParameter("scope"));
authorizationRequest.setState(request.getParameter("state"));
return authorizationRequest;
}
}
and cofiguration class:
#Configuration
public class WebappConfig extends WebMvcConfigurerAdapter {
#Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(new AuthorizationRequestResolver());
}
}