How do I pass over the locale from http request to websocket in spring boot?
My locale is already set in LocaleContextHolder, but when I hand over to websocket, it's gone and it's default again.
What's the right way to hand over the locale to websockets?
Ok, I found a solution.
Since LocaleContextHolder is thread based and websockets are running asynchronously, things get lost from the request.
But luckily there is HandshakeInterceptor to hand over certain things to websocket sessions.
My config:
#Configuration
#EnableScheduling
#EnableWebSocketMessageBroker
public class WebSocketBrokerConfig extends AbstractSessionWebSocketMessageBrokerConfigurer<Session> {
// ...
#Override
protected void configureStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws")
.setAllowedOrigins("*")
.addInterceptors(new HttpWebsocketHandshakeInterceptor()) // <-- The interceptor
.withSockJS();
}
// ...
}
The interceptor:
public class HttpWebsocketHandshakeInterceptor implements HandshakeInterceptor {
#Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
Map<String, Object> attributes) throws Exception {
if (request instanceof ServletServerHttpRequest) {
Locale locale = LocaleContextHolder.getLocale();
attributes.put(WSConstants.HEADER_HTTP_LOCALE, locale);
// hand over more stuff, if needed ...
}
return true;
}
#Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
Exception exception) {
}
}
WSConstants.HEADER_HTTP_LOCALE is just a string constant. Call it whatever you like.
Then in your controller:
#Controller
public class WSController {
#MessageMapping("/somewhere/")
public void message(
SimpMessageHeaderAccessor headerAccessor,
Principal principal,
WSMessage frame) {
// update locale to keep it up to date
Map<String, Object> sessionHeaders = headerAccessor.getSessionAttributes();
Locale locale = (Locale) sessionHeaders.get(WSConstants.HEADER_HTTP_LOCALE);
if (locale != null) {
LocaleContextHolder.setLocale(locale);
}
// use your localized stuff as you used to
}
#SubscribeMapping("/somewhereelse/")
public ChannelPayload bubble(
SimpMessageHeaderAccessor headerAccessor,
Principal principal
) {
// update locale to keep it up to date
Map<String, Object> sessionHeaders = headerAccessor.getSessionAttributes();
Locale locale = (Locale) sessionHeaders.get(WSConstants.HEADER_HTTP_LOCALE);
if (locale != null) {
LocaleContextHolder.setLocale(locale);
}
// use your localized stuff as you used to
return null;
}
}
Hope this helps others with the same issues.
Related
I am creating an aspect to register my application using org.springframework.web.bind.annotation.RestController like #Pointcut, this works perfectly when my class responds normally, but when an exception occurs for some reason, the returned httpStatus is always 200, even If my http response returns 500 when an error occurs, I think this is because RestController does not set the http status, but delegates it to the exception handler, how do I fix this and still have traceability on top of the restcontroller?
Follow my rest controller
#Slf4j
#RestController
#RequestMapping("/api/conta")
public class ContaResourceHTTP {
#JetpackMethod("Pagamento de conta")
#PostMapping("/pagamento")
public void realizarPagamento(#RequestBody DTOPagamento dtoPagamento) throws InterruptedException
{
}
#JetpackMethod("Transferência entre bancos")
#PostMapping("/ted")
public void realizarTED(#RequestBody DTOPagamento dtoPagamento) throws java.lang.Exception
{
if(true)
throw new Exception("XXX");
//log.info(dtoPagamento.toString());
}
}
my AOP implementation:
#Aspect
#Component
#EnableAspectJAutoProxy(proxyTargetClass = true)
#Slf4j
public class MetricsAspect {
//#Pointcut("within(#org.springframework.web.bind.annotation.RestController *)")
#Pointcut("execution(* javax.servlet.http.HttpServlet.*(..)) *)")
public void springBeanPointcut() {
}
#Autowired
Tracer tracer;
#Around("springBeanPointcut()")
public void logAround(ProceedingJoinPoint joinPoint) throws Throwable {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes())
.getRequest();
long inicioProcesso = System.currentTimeMillis();
joinPoint.proceed();
long finalProcesso = System.currentTimeMillis();
long duracaoProcesso = finalProcesso - inicioProcesso;
HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes())
.getResponse();
Metrics metricas = new Metrics();
metricas.setDuracaoMs(duracaoProcesso);
metricas.setDataHoraRequisicao(milissegundosToStringDate(inicioProcesso));
metricas.setDataHoraResposta(milissegundosToStringDate(finalProcesso));
metricas.setServidorOrigem(request.getRemoteAddr());
metricas.setPortaOrigem(request.getRemotePort());
metricas.setDominioAcesso(request.getLocalName());
metricas.setPortaAcesso(request.getLocalPort());
metricas.setUrlPath(request.getRequestURI());
metricas.setMetodoHttp(request.getMethod());
metricas.setIdTransacao(tracer.currentSpan().context().traceIdString());
metricas.setIdSpan(tracer.currentSpan().context().spanIdString());
metricas.setStatusHttp(response.getStatus());
log.info(JSONConversor.toJSON(metricas));
}
public String milissegundosToStringDate(long ms) {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
Date dataInicial = new Date(ms);
return dateFormat.format(dataInicial);
}
}
My exception handler:
#ControllerAdvice
#Order(Ordered.HIGHEST_PRECEDENCE)
public class ExceptionControllerAdvice {
#ExceptionHandler({ Throwable.class })
public ResponseEntity<ApiError> handlerValidationException2(Throwable e) {
return new ResponseEntity<>(new ApiError(HttpStatus.INTERNAL_SERVER_ERROR, e, traceRespostaAPI),
HttpStatus.INTERNAL_SERVER_ERROR);
}
}
After a while I was able to solve the problem with a solution that may not be the most elegant for the problem, basically I used two pointcuts, one in the restcontroller to intercept the #JetpackMethod annotation value and add it to the http response header with advice before and another around HttpServlet that really is the one who really gets back with the modified http status.
Here's the code below that solved my problem.
This class intercepts annotation and adds its value to the header.
#Aspect
#Component
public class InterceptRestAnnotationAspect {
#Pointcut("within(#org.springframework.web.bind.annotation.RestController *)")
public void restControllerExecution() {}
#Before("restControllerExecution()")
public void setMetodoHttpHeader(JoinPoint joinPoint) throws Throwable {
HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes())
.getResponse();
String origem = VerificadorOrigem.processarOrigem(joinPoint);
response.setHeader("nomeMetodo", origem);
}
}
This other class logs the servlet metrics I needed and can retrieve the value entered in the header earlier.
#Aspect
#Component
#Slf4j
public class MetricsAspect {
#Pointcut("execution(* javax.servlet.http.HttpServlet.*(..)) *)")
public void servletService() {
}
#Autowired
Tracer tracer;
#Around("servletService()")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes())
.getRequest();
long inicioProcesso = System.currentTimeMillis();
Object result = joinPoint.proceed();
long finalProcesso = System.currentTimeMillis();
long duracaoProcesso = finalProcesso - inicioProcesso;
HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes())
.getResponse();
Metrics metricas = new Metrics();
String funcionalidade = response.getHeader("nomeMetodo") == null ? "Indeterminada"
: response.getHeader("nomeMetodo");
metricas.setNivelLog("INFO");
metricas.setFuncionalidade(funcionalidade);
metricas.setDuracaoMs(duracaoProcesso);
metricas.setDataHoraRequisicao(ManipulaData.milissegundosToStringDate(inicioProcesso));
metricas.setDataHoraResposta(ManipulaData.milissegundosToStringDate(finalProcesso));
metricas.setServidorOrigem(request.getRemoteAddr());
metricas.setPortaOrigem(request.getRemotePort());
metricas.setDominioAcesso(request.getLocalName());
metricas.setPortaAcesso(request.getLocalPort());
metricas.setUrlPath(request.getRequestURI());
metricas.setMetodoHttp(request.getMethod());
metricas.setIdTransacao(tracer.currentSpan().context().traceIdString());
metricas.setIdSpan(tracer.currentSpan().context().spanIdString());
metricas.setStatusHttp(response.getStatus());
log.info(JSONConversor.toJSON(metricas));
return result;
}
}
I don't think the code after joinPoint.proceed(); gets executed in case of Exceptions.
You can have a different advice for execution in case of Exceptions:
#AfterThrowing(pointcut = "springBeanPointcut()", throwing = "e")
public void afterThrowingAdvice(JoinPoint jp, Exception e) {
....
}
In SpringBoot version 2.1.6 unable to intercept access actuator request
Now I have a global interceptor
#Component
public class ServiceFilter implements HandlerInterceptor {
//log4j
static final Logger logger = LogManager.getLogger(ServiceFilter.class);
private final RateLimiter limiter = RateLimiter.create(Runtime.getRuntime().availableProcessors() * 2 + 1);
private final ThreadLocal<ExecuteRecordDto> executeRecord = new ThreadLocal<>();
public ServiceFilter() {
}
#Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
ExecuteRecordDto recordDto = ExecuteRecordDto.bulider(request);
executeRecord.set(recordDto);
if (!limiter.tryAcquire()) {
logger.warn("rate limiter ; json logger : {}",CommonUtil.toJSONString(recordDto));
response.getWriter().print(CommonUtil.toJSONString(ResultStatus.status(407, "rate limiter")));
return false;
}
if (ObjectUtils.isEmpty(request.getHeader("Authorization"))) {
logger.warn("illegal request, json logger : {} ",CommonUtil.toJSONString(recordDto));
response.getWriter().print(CommonUtil.toJSONString(ResultStatus.status(403, "Permission denied")));
return false;
}
switch (TokenHandle.checkToken(request.getHeader("Authorization"))) {
//正常放行token
case 0:
response.getWriter().print(CommonUtil.toJSONString(ResultStatus.status(407, "rate limiter")));
return true;
//token 过期
case 1:
response.getWriter().println(CommonUtil.toJSONString(ResultStatus.status(408, "Token expire")));
break;
//非法token
case 2:
logger.warn("illegal token, json logger : {} ",CommonUtil.toJSONString(recordDto));
response.getWriter().print(CommonUtil.toJSONString(ResultStatus.status(409, "Illegal token ")));
break;
default:
throw new RuntimeException("server runtime exception");
}
return true;
}
#Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
ExecuteRecordDto recordDto = executeRecord.get();
logger.info("json logger : {}",CommonUtil.toJSONString(recordDto));
executeRecord.remove();
}
}
And make it work
#Configuration
public class ConfigFilter implements WebMvcConfigurer {
private final ServiceFilter filter;
#Autowired
public ConfigFilter(ServiceFilter filter){
this.filter = filter;
}
#Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(filter).addPathPatterns("/**");
}
}
I requested my own api, get the effect I want
How can SpringBoot intercept a visit to actuator
Actuator is using a different HandlerMapping (see: org.springframework.boot.actuate.endpoint.web.servlet.WebMvcEndpointHandlerMapping).
This Handlermapping will be chosen over your configured RequestHandlerMapping because of the order (-100 vs 0). You can see this in the DispatcherServlet precisely the method HandlerExecutionChain getHandler(HttpServletRequest request).
In our projects we configure the access to the actuator endpoints with spring security so i'm not aware if there are any recommended ways to do it but:
The handler are chose by order so this a thing to consider, you can also try to manipulate the actuator WebMvcEndpointHandlerMapping.
Like i said i'm not sure about the right solution, but i hope it points you in the right direction to find a proper solution.
regards, WiPu
I am writing an application with spring messaging and stomp and rabbitmq. My application already sends messages from the browser to rabbitmq and back. But i dont want the predefined rabbitmq queue names based on the session id. I want to change the session id on connect. This is what i tried:
#Component
public class MyListener {
private Logger logger = LoggerFactory.getLogger(getClass().getSimpleName());
#EventListener
public void x(SessionConnectEvent event) {
Map<String, Object> headers = event.getMessage().getHeaders();
String id = headers.get("simpSessionId").toString();
logger.info("My current session id is " + id);
headers.put("sessionId", "fred");
}
}
Error is: the map is immutable
You need to update the sessionId before the handshake is done between client <-> server, that is when the headers attributes are defined.
On the other hand, the listener SessionConnectEvent is executed only after the handshake is done.
public class HttpHandshakeInterceptor implements HandshakeInterceptor {
#Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
Map attributes) throws Exception {
if (request instanceof ServletServerHttpRequest) {
ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) request;
HttpSession session = servletRequest.getServletRequest().getSession();
attributes.put("sessionId", "mySessiond");
}
return true;
}
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
Exception ex) {
}
}
Also don't forget to register the interceptor on the specific endpoint
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/greeting").addInterceptors(new HttpHandshakeInterceptor());
}
Changing the session ID was not the correct way. I used ServletFilter for Cookie and Security Checks and #SendTo for the correct use of rabbitmq queues.
You can change the session id by creating a Principal for each handshake and then you can
target each connected session with the provided username :
class CustomHandshake extends DefaultHandshakeHandler {
#Override
public Principal determineUser(ServerHttpRequest request, WebSocketHandler wsHandler, Map<String, Object> attributes) {
Principal principal = request.getPrincipal();
if (principal == null) {
principal = new AnonymousPrincipal();
String uniqueName = UUID.randomUUID().toString();
((AnonymousPrincipal) principal).setName(uniqueName);
}
return principal;
}
}
Do not forget to register the handler as below :
.setHandshakeHandler(new CustomHandshake())
hope this is helpful
Working with Spring Boot 1.2.1.RELEASE and Spring Websockets. Having a deployment runtime issue where when running embedded Jetty 9, I cannot fake a user (java.security.Principal) successfully when app deployed anywhere else but localhost.
I have consulted
http://docs.spring.io/spring/docs/current/spring-framework-reference/html/websocket.html#websocket-stomp-authentication
http://docs.spring.io/spring/docs/current/spring-framework-reference/html/websocket.html#websocket-server-runtime-configuration
The config below (I believe) already "upgrades" a request
#Configuration
#EnableWebSocketMessageBroker
#EnableScheduling
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
#Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
// see http://docs.spring.io/spring/docs/current/spring-framework-reference/html/websocket.html#websocket-stomp-handle-broker-relay
// requires an external broker like AMQP or RabbitMQ
//registry.enableStompBrokerRelay("/queue/", "/topic/");
// XXX This might wind up being the impl we actually deploy; but be aware it has certain constraints
// see http://docs.spring.io/spring/docs/current/spring-framework-reference/html/websocket.html#websocket-stomp-message-flow
registry.enableSimpleBroker("/queue/", "/topic/");
registry.setApplicationDestinationPrefixes("/app");
}
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/cards").setHandshakeHandler(new UserHandler()).withSockJS();
}
// cheat; ensure that we have a Principal w/o relying on authentication
class UserHandler extends DefaultHandshakeHandler {
#Override
protected Principal determineUser(ServerHttpRequest request, WebSocketHandler wsHandler,
Map<String, Object> attributes) {
return new TestPrincipal("bogus");
}
}
And here's the principal...
public class TestPrincipal implements Principal {
private final String name;
public TestPrincipal(String name) {
this.name = name;
}
#Override
public String getName() {
return this.name;
}
}
But this is the exception I'm receiving...
Logger=org.springframework.messaging.simp.annotation.support.SimpAnnotationMethodMessageHandler Type=ERROR Message=Unhandled exception
org.springframework.messaging.simp.annotation.support.MissingSessionUserException: No "user" header in message
at org.springframework.messaging.simp.annotation.support.PrincipalMethodArgumentResolver.resolveArgument(PrincipalMethodArgumentResolver.java:42) ~[spring-messaging-4.1.4.RELEASE.jar!/:4.1.4.RELEASE]
at org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:77) ~[spring-messaging-4.1.4.RELEASE.jar!/:4.1.4.RELEASE]
at org.springframework.messaging.handler.invocation.InvocableHandlerMethod.getMethodArgumentValues(InvocableHandlerMethod.java:127) ~[spring-messaging-4.1.4.RELEASE.jar!/:4.1.4.RELEASE]
at org.springframework.messaging.handler.invocation.InvocableHandlerMethod.invoke(InvocableHandlerMethod.java:100) ~[spring-messaging-4.1.4.RELEASE.jar!/:4.1.4.RELEASE]
at org.springframework.messaging.handler.invocation.AbstractMethodMessageHandler.handleMatch(AbstractMethodMessageHandler.java:451) [spring-messaging-4.1.4.RELEASE.jar!/:4.1.4.RELEASE]
at org.springframework.messaging.simp.annotation.support.SimpAnnotationMethodMessageHandler.handleMatch(SimpAnnotationMethodMessageHandler.java:443) [spring-messaging-4.1.4.RELEASE.jar!/:4.1.4.RELEASE]
at org.springframework.messaging.simp.annotation.support.SimpAnnotationMethodMessageHandler.handleMatch(SimpAnnotationMethodMessageHandler.java:82) [spring-messaging-4.1.4.RELEASE.jar!/:4.1.4.RELEASE]
at org.springframework.messaging.handler.invocation.AbstractMethodMessageHandler.handleMessageInternal(AbstractMethodMessageHandler.java:412) [spring-messaging-4.1.4.RELEASE.jar!/:4.1.4.RELEASE]
at org.springframework.messaging.handler.invocation.AbstractMethodMessageHandler.handleMessage(AbstractMethodMessageHandler.java:350) [spring-messaging-4.1.4.RELEASE.jar!/:4.1.4.RELEASE]
at org.springframework.messaging.support.ExecutorSubscribableChannel$SendTask.run(ExecutorSubscribableChannel.java:135) [spring-messaging-4.1.4.RELEASE.jar!/:4.1.4.RELEASE]
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) [na:1.8.0_05]
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) [na:1.8.0_05]
at java.lang.Thread.run(Thread.java:745) [na:1.8.0_05]
What else should I consider here?
UPDATE
I can reproduce this reliably in a localhost deployment now.
Interestingly, using STS 3.6.3 in debug mode, when I set a breakpoint at line 91 of JettyRequestUpgradeStrategy
public JettyRequestUpgradeStrategy(WebSocketServerFactory factory) {
Assert.notNull(factory, "WebSocketServerFactory must not be null");
this.factory = factory;
this.factory.setCreator(new WebSocketCreator() {
#Override
public Object createWebSocket(ServletUpgradeRequest request, ServletUpgradeResponse response) {
// Cast to avoid infinite recursion
return createWebSocket((UpgradeRequest) request, (UpgradeResponse) response);
}
Then continue on to another breakpoint set at line 41 of PrincipalMethodArgumentResolver
#Override
public Object resolveArgument(MethodParameter parameter, Message<?> message) throws Exception {
Principal user = SimpMessageHeaderAccessor.getUser(message.getHeaders());
if (user == null) {
throw new MissingSessionUserException(message);
}
return user;
}
the user is null. Is there a race condition? E.g., is there some time limit within which the socket must acquire the user from request?
I'm in the process of setting up Spring Security. My CookieAuthenticationFilter should make sure to keep users out unless they have a cookie with an UUID we accept. Although CookieAuthenticationFilter sets an empty context if the UUID is not accepted I still have access to all URLs.
Any idea what's missing?
This is my security configuration:
#Configuration
#EnableWebMvcSecurity
public class LIRSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.addFilter(cookieAuthenticationFilter())
.authorizeRequests()
.antMatchers("/**").hasAnyAuthority("ALL");
}
#Bean
public CookieAuthenticationFilter cookieAuthenticationFilter() {
return new CookieAuthenticationFilter(cookieService());
}
private CookieService cookieService() {
return new CookieService.Impl();
}
#Bean(name = "springSecurityFilterChain")
public FilterChainProxy getFilterChainProxy() {
SecurityFilterChain chain = new SecurityFilterChain() {
#Override
public boolean matches(HttpServletRequest request) {
// All goes through here
return true;
}
#Override
public List<Filter> getFilters() {
List<Filter> filters = new ArrayList<Filter>();
filters.add(cookieAuthenticationFilter());
return filters;
}
};
return new FilterChainProxy(chain);
}
}
This is the CookieAuthenticationFilter implementation:
public class CookieAuthenticationFilter extends GenericFilterBean {
#Resource
protected AuthenticationService authenticationService;
private CookieService cookieService;
public CookieAuthenticationFilter(CookieService cookieService) {
super();
this.cookieService = cookieService;
}
#Override
public void doFilter(ServletRequest req, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
UUID uuid = cookieService.extractUUID(request.getCookies());
UserInfo userInfo = authenticationService.findBySessionKey(uuid);
SecurityContext securityContext = null;
if (userInfo != null) {
securityContext = new CookieSecurityContext(userInfo);
SecurityContextHolder.setContext(securityContext);
} else {
securityContext = SecurityContextHolder.createEmptyContext();
}
try {
SecurityContextHolder.setContext(securityContext);
chain.doFilter(request, response);
}
finally {
// Free the thread of the context
SecurityContextHolder.clearContext();
}
}
}
The issue here is that you don't want to use GenericFilterBean as it's not actually part of the Spring Security framework, just regular Spring so it's not aware of how to send security-related messages back to the browser or deny access, etc. If you do want to use the GenericFilterBean you'll need to handle the redirect or the 401 response yourself. Alternatively, look into the AbstractPreAuthenticatedProcessingFilter that is part of the Spring Security framework. There is some documentation here: http://docs.spring.io/spring-security/site/docs/3.0.x/reference/preauth.html