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
Related
I'm working with RedisHttpSession and my basic goal is to save the staff object in the session object on successful login, retrieve it wherever I need and destroy the session on logout.
On successful login, this is what I'm doing:
Staff staff = staffService.getEmailInstance(body.getEmailId());
request.getSession(true).setAttribute("staff", staff);
And Logout is simply this:
request.getSession().invalidate();
In a different controller, I am calling this utility method that checks if the staff is logged in: util.isStaffLoggedIn(request, response, StaffRole.EDITOR); If the staff is logged in, the API proceeds, else the user is redirected to the login page.
#Service
public class Util {
public boolean isStaffLoggedIn(HttpServletRequest request, HttpServletResponse response, StaffRole staffRole)
throws PaperTrueInvalidCredentialsException, PaperTrueJavaException {
Staff staff = (Staff) request.getSession().getAttribute("staff");
if (!isObjectNull(staff) && staff.getStaffRole().equals(staffRole)) {
return true;
}
invalidateSessionAndRedirect(request, response);
return false;
}
public void invalidateSessionAndRedirect(HttpServletRequest request, HttpServletResponse response)
throws PaperTrueJavaException, PaperTrueInvalidCredentialsException {
request.getSession().invalidate();
try {
response.sendRedirect(ProjectConfigurations.configMap.get("staff_logout_path"));
} catch (IOException e) {
throw new PaperTrueJavaException(e.getMessage());
}
throw new PaperTrueInvalidCredentialsException("Staff not loggedIn");
}
}
Now while the app is running, the get-jobs API is called immidiately after successful login. Most of the times the request.getSession().getAttribute("staff") method works fine and returns the 'staff' object but, once in a while, it returns null. This doesn't happen often, but it does. I printed the session Id to see if they are different after logout, and they were. After each logout I had a new session Id. I even checked if the staff object I retrieved from the database was null, but it wasn't.
The staff object was successfully saved in the sessions but I wasn't able to retrieve it in othe APIs. This is how my session config looks:
#EnableRedisHttpSession(maxInactiveIntervalInSeconds = 10800)
public class SessionConfig {
HashMap<String, String> configMap = ProjectConfigurations.configMap;
#Bean
public LettuceConnectionFactory connectionFactory() {
int redisPort = Integer.parseInt(configMap.get("redis_port"));
RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration(
configMap.get("redis_host"), redisPort);
redisStandaloneConfiguration.setPassword(configMap.get("redis_password"));
return new LettuceConnectionFactory(redisStandaloneConfiguration);
}
#Bean
public CookieSerializer cookieSerializer() {
DefaultCookieSerializer serializer = new DefaultCookieSerializer();
serializer.setCookieName("PTSESSIONID");
serializer.setSameSite("none");
serializer.setUseSecureCookie(!configMap.get("staff_logout_path").contains("localhost"));
return serializer;
}
}
Please let me know if I missed out anything. Thanks in advance.
Update 1
I'm not invalidating the session anymore and I've replaced request.getSession(true).setAttribute("staff", staff); to request.getSession().setAttribute("staff", staff);
I'm setting the 'staff' in StaffController and getting it in EditorController. Here's how I'm setting it:
#RestController
#RequestMapping(path = { "/staff" }, produces = "application/json")
public class StaffApiController {
private final HttpServletRequest request;
private final HttpSession httpSession;
#Autowired
private StaffService staffService;
#Autowired
StaffApiController(HttpServletRequest request, HttpServletResponse response, HttpSession session) {
this.request = request;
this.httpSession = session;
}
#PostMapping("/login")
public ResponseEntity<StaffLoginResponse> login(#Valid #RequestBody StaffLoginBody body) {
StaffLoginResponse staffLoginResponse = new StaffLoginResponse();
try {
if (!staffService.isValidLogin(body.getEmailId(), body.getPassword())) {
throw new PaperTrueInvalidCredentialsException("Invalid Credentials");
}
Staff staff = staffService.getEmailInstance(body.getEmailId());
httpSession.setAttribute("staff", staff);
staffLoginResponse.setEmail(staff.getEmail()).setRole(staff.getStaffRole().getValue())
.setStaffID(staff.getId()).setStatus(new Status("Staff Login Successful"));
} catch (PaperTrueException e) {
httpSession.removeAttribute("staff");
staffLoginResponse.setStatus(new Status(e.getCode(), e.getMessage()));
}
return ResponseEntity.ok(staffLoginResponse);
}
#PostMapping("/logout")
public ResponseEntity<Status> logout() {
httpSession.removeAttribute("staff");
return ResponseEntity.ok(new Status("Staff Logged Out Successfully"));
}
}
If you are using Spring Security, you can create a custom "/login" endpoint that authenticates the user by setting the SecurityContext.
You can use the default logout behaviour provided by Spring Security.
If you do not need to supply the credentials in the body, you can use the default login behaviour provided by Spring Security and omit this Controller altogether.
This is intended as a starting point.
It does not offer comprehensive security, for example it may be vulnerable session fixation attacks.
#RestController
public class LoginController {
private AuthenticationManager authenticationManager;
public LoginController(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
#PostMapping("/login")
public void login(#RequestBody StaffLoginBody body, HttpServletRequest request) {
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(body.getUsername(), body.getPassword());
Authentication auth = authenticationManager.authenticate(token);
SecurityContextHolder.getContext().setAuthentication(auth);
HttpSession session = request.getSession();
session.setAttribute("staff", "staff_value");
}
#GetMapping("/jobs")
public String getStaffJobs(HttpServletRequest request) {
return request.getSession().getAttribute("staff").toString();
}
}
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// expose AuthenticationManager bean to be used in Controller
#Override
#Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests(authorize -> authorize
.antMatchers("/login").permitAll()
.anyRequest().authenticated()
)
// use built in logout
.logout(logout -> logout
.deleteCookies("PTSESSIONID")
);
}
}
You will need to add the Spring Security dependency to use this code org.springframework.boot:spring-boot-starter-security.
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.
I've been struggling a lot to properly implement Stomp (websocket) Authentication and Authorization with Spring-Security. For posterity i'll answer my own question to provide a guide.
The Problem
Spring WebSocket documentation (for Authentication) looks unclear ATM (IMHO). And i couldn't understand how to properly handle Authentication and Authorization.
What i want
Authenticate users with login/password.
Prevent anonymous users to CONNECT though WebSocket.
Add authorization layer (user, admin, ...).
Having Principal available in controllers.
What i don't want
Authenticate on HTTP negotiation endpoints (since most of JavaScript libraries don't sends authentication headers along with the HTTP negotiation call).
As stated above the documentation looks unclear (IMHO), until Spring provide some clear documentation, here is a boilerplate to save you from spending two days trying to understand what the security chain is doing.
A really nice attempt was made by Rob-Leggett but, he was forking some Springs class and I don't feel comfortable doing so.
Things to know before you start:
Security chain and Security config for http and WebSocket are completely independent.
Spring AuthenticationProvider take not part at all in Websocket authentication.
The authentication won't happen on HTTP negotiation endpoint in our case, because none of the JavaScripts STOMP (websocket) libraries I know sends the necessary authentication headers along with the HTTP request.
Once set on CONNECT request, the user (simpUser) will be stored in the websocket session and no more authentication will be required on further messages.
Maven deps
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-messaging</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-messaging</artifactId>
</dependency>
WebSocket configuration
The below config register a simple message broker (a simple endpoint that we will later protect).
#Configuration
#EnableWebSocketMessageBroker
public class WebSocketConfig extends WebSocketMessageBrokerConfigurer {
#Override
public void configureMessageBroker(final MessageBrokerRegistry config) {
// These are endpoints the client can subscribes to.
config.enableSimpleBroker("/queue/topic");
// Message received with one of those below destinationPrefixes will be automatically router to controllers #MessageMapping
config.setApplicationDestinationPrefixes("/app");
}
#Override
public void registerStompEndpoints(final StompEndpointRegistry registry) {
// Handshake endpoint
registry.addEndpoint("stomp"); // If you want to you can chain setAllowedOrigins("*")
}
}
Spring security config
Since the Stomp protocol rely on a first HTTP Request, we'll need to authorize HTTP call to our stomp handshake endpoint.
#Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(final HttpSecurity http) throws Exception {
// This is not for websocket authorization, and this should most likely not be altered.
http
.httpBasic().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests().antMatchers("/stomp").permitAll()
.anyRequest().denyAll();
}
}
Then we'll create a service responsible for authenticating users.
#Component
public class WebSocketAuthenticatorService {
// This method MUST return a UsernamePasswordAuthenticationToken instance, the spring security chain is testing it with 'instanceof' later on. So don't use a subclass of it or any other class
public UsernamePasswordAuthenticationToken getAuthenticatedOrFail(final String username, final String password) throws AuthenticationException {
if (username == null || username.trim().isEmpty()) {
throw new AuthenticationCredentialsNotFoundException("Username was null or empty.");
}
if (password == null || password.trim().isEmpty()) {
throw new AuthenticationCredentialsNotFoundException("Password was null or empty.");
}
// Add your own logic for retrieving user in fetchUserFromDb()
if (fetchUserFromDb(username, password) == null) {
throw new BadCredentialsException("Bad credentials for user " + username);
}
// null credentials, we do not pass the password along
return new UsernamePasswordAuthenticationToken(
username,
null,
Collections.singleton((GrantedAuthority) () -> "USER") // MUST provide at least one role
);
}
}
Note that: UsernamePasswordAuthenticationToken MUST have at least one GrantedAuthority, if you use another constructor, Spring will auto-set isAuthenticated = false.
Almost there, now we need to create an Interceptor that will set the `simpUser` header or throw `AuthenticationException` on CONNECT messages.
#Component
public class AuthChannelInterceptorAdapter extends ChannelInterceptor {
private static final String USERNAME_HEADER = "login";
private static final String PASSWORD_HEADER = "passcode";
private final WebSocketAuthenticatorService webSocketAuthenticatorService;
#Inject
public AuthChannelInterceptorAdapter(final WebSocketAuthenticatorService webSocketAuthenticatorService) {
this.webSocketAuthenticatorService = webSocketAuthenticatorService;
}
#Override
public Message<?> preSend(final Message<?> message, final MessageChannel channel) throws AuthenticationException {
final StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
if (StompCommand.CONNECT == accessor.getCommand()) {
final String username = accessor.getFirstNativeHeader(USERNAME_HEADER);
final String password = accessor.getFirstNativeHeader(PASSWORD_HEADER);
final UsernamePasswordAuthenticationToken user = webSocketAuthenticatorService.getAuthenticatedOrFail(username, password);
accessor.setUser(user);
}
return message;
}
}
Note that: preSend() MUST return a UsernamePasswordAuthenticationToken, another element in the spring security chain test this.
Note that: If your UsernamePasswordAuthenticationToken was built without passing GrantedAuthority, the authentication will fail, because the constructor without granted authorities auto set authenticated = false THIS IS AN IMPORTANT DETAIL which is not documented in spring-security.
Finally create two more class to handle respectively Authorization and Authentication.
#Configuration
#Order(Ordered.HIGHEST_PRECEDENCE + 99)
public class WebSocketAuthenticationSecurityConfig extends WebSocketMessageBrokerConfigurer {
#Inject
private AuthChannelInterceptorAdapter authChannelInterceptorAdapter;
#Override
public void registerStompEndpoints(final StompEndpointRegistry registry) {
// Endpoints are already registered on WebSocketConfig, no need to add more.
}
#Override
public void configureClientInboundChannel(final ChannelRegistration registration) {
registration.setInterceptors(authChannelInterceptorAdapter);
}
}
Note that: The #Order is CRUCIAL don't forget it, it allows our interceptor to be registered first in the security chain.
#Configuration
public class WebSocketAuthorizationSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {
#Override
protected void configureInbound(final MessageSecurityMetadataSourceRegistry messages) {
// You can customize your authorization mapping here.
messages.anyMessage().authenticated();
}
// TODO: For test purpose (and simplicity) i disabled CSRF, but you should re-enable this and provide a CRSF endpoint.
#Override
protected boolean sameOriginDisabled() {
return true;
}
}
for java client side use this tested example:
StompHeaders connectHeaders = new StompHeaders();
connectHeaders.add("login", "test1");
connectHeaders.add("passcode", "test");
stompClient.connect(WS_HOST_PORT, new WebSocketHttpHeaders(), connectHeaders, new MySessionHandler());
Going with spring authentication is a pain. You can do it in a simple way. Create a web Filter and read the Authorization token by yourself, then perform the authentication.
#Component
public class CustomAuthenticationFilter implements Filter {
#Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException {
if (servletRequest instanceof HttpServletRequest) {
HttpServletRequest request = (HttpServletRequest) servletRequest;
String authorization = request.getHeader("Authorization");
if (/*Your condition here*/) {
// logged
filterChain.doFilter(servletRequest, servletResponse);
} else {
HttpServletResponse response = (HttpServletResponse) servletResponse;
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
response.getWriter().write("{\"message\": "\Bad login\"}");
}
}
}
#Override
public void init(FilterConfig filterConfig) throws ServletException {
}
#Override
public void destroy() {
}
}
Then in your configuration define the filter using the spring mechanism:
#Configuration
public class SomeConfig {
#Bean
public FilterRegistrationBean<CustomAuthenticationFilter> securityFilter(
CustomAuthenticationFilter customAuthenticationFilter){
FilterRegistrationBean<CustomAuthenticationFilter> registrationBean
= new FilterRegistrationBean<>();
registrationBean.setFilter(customAuthenticationFilter);
registrationBean.addUrlPatterns("/*");
return registrationBean;
}
}
I have an application with Spring Session and Spring Websockets. I use websocket handshake interceptor to set properties to websocket session
public class WebsocketHandshakeInterceptor implements HandshakeInterceptor {
#Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response,
WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
if (request instanceof ServletServerHttpRequest) {
ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) request;
HttpSession session = servletRequest.getServletRequest().getSession(true);
if (session != null) {
attributes.put(ApplicationConstants.HTTP_SESSION_ID_KEY, session.getId());
attributes.put(ApplicationConstants.HTTP_SESSION_KEY, session);
}
}
return true;
}
#Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response,
WebSocketHandler wsHandler, Exception ex) {
}
}
Then I catch SessionConnectEvent. Event contais needed information (http session and session id). But the session is null in sessionRepository
#Component
public class WebSocketListener implements ApplicationListener<SessionConnectEvent> {
#Autowired
private MapSessionRepository sessionManager;
#Override
public void onApplicationEvent(SessionConnectEvent event) {
String httpSessionId
= event.getWebSocketSession().getAttributes().get(ApplicationConstants.HTTP_SESSION_ID_KEY).toString();
sessionManager.getSession(httpSessionId); //returns null
}
}
It returns null with the first call, if I try to reconnect, it returns valid session. Can you show me my error? Thanks.
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?