Error during WebSocket handshake: Unexpected response code: 200 In Jhipster application - spring-boot

I am trying to connect to websocket in my existing Jhipster application which is build using Java spring and Angular 8. At the time of creation of the project I didn't select the websocket option and now there is a need to implement the websockets in the project. I took the help from "https://www.jhipster.tech/using-websockets/" and tried to implement the same solution but I am getting the handshake failure message.
I am using STOMP and sockJS for in websockets.
Please let me know what could be the reasons for handshake failure.
Here is the websocket configuration.java file.
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.server.support.DefaultHandshakeHandler;
import hr.nic.vc.security.AuthoritiesConstants;
import java.security.Principal;
import org.springframework.http.server.*;
import java.util.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.server.*;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.config.annotation.*;
import org.springframework.web.socket.server.HandshakeInterceptor;
import org.springframework.web.socket.server.support.DefaultHandshakeHandler;
#Configuration
#EnableWebSocketMessageBroker
public class WebsocketConfiguration implements WebSocketMessageBrokerConfigurer {
#Override
public void configureMessageBroker(MessageBrokerRegistry config)
{
config.enableSimpleBroker("/topic");
//config.setApplicationDestinationPrefixes("/app");
}
#Override
public void registerStompEndpoints(StompEndpointRegistry
registry) {
registry.addEndpoint("/websocket/blog").setHandshakeHandler(defaultHandshakeHandler())
.setAllowedOrigins("*").withSockJS();
}
#Bean
public HandshakeInterceptor httpSessionHandshakeInterceptor() {
return new 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;
attributes.put(IP_ADDRESS, servletRequest.getRemoteAddress());
}
return true;
}
#Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {
}
};
}
private DefaultHandshakeHandler defaultHandshakeHandler() {
return new DefaultHandshakeHandler() {
#Override
protected Principal determineUser(ServerHttpRequest request, WebSocketHandler wsHandler, Map<String, Object> attributes) {
Principal principal = request.getPrincipal();
if (principal == null) {
Collection<SimpleGrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority(AuthoritiesConstants.ANONYMOUS));
principal = new AnonymousAuthenticationToken("WebsocketConfiguration", "anonymous", authorities);
}
return principal;
}
};
}
}
And this is the websocket security file.
public class WebsocketSecurityConfiguration extends AbstractSecurityWebSocketMessageBrokerConfigurer {
#Override
protected void configureInbound(MessageSecurityMetadataSourceRegistry messages)
{
messages.nullDestMatcher().authenticated().simpDestMatchers("/topic/blog")
.authenticated()
// matches any destination that starts with /topic/
// (i.e. cannot send messages directly to /topic/)
// (i.e. can’t subscribe to /topic/messages/* to get messages which is sent to
// /topic/messages-user)
.simpDestMatchers("/topic/**").authenticated();
// message types other than MESSAGE and SUBSCRIBE
//.simpTypeMatchers(SimpMessageType.MESSAGE, SimpMessageType.SUBSCRIBE).denyAll()
// catch all
//.anyMessage().denyAll();
}
/**
* Disables CSRF for Websockets.
*/
#Override
protected boolean sameOriginDisabled() {
return true;
}
}
Here is the connect method which is called after authentication for connecting the websocket.
connect():void {
console.log("connect called"); //eslint-disable-line
// build absolute path, websocket doesn't fail during deploying with a context path
let url = '/websocket/blog';
url = this.location.prepareExternalUrl(url);
const authToken = this.authServerProvider.getToken();
if (authToken) {
url += '?access_token=' + authToken;
}
const socket = new SockJS(url);
this.stompClient = Stomp.over(socket);
const headers = {};
this.stompClient.connect(headers, () => {
console.log("this is inside stompclient connect connect"); //eslint-disable-line
this.stompClient.subscribe('/topic/blog', data => {
console.log("This inside subscription"); //eslint-disable-line
this.listenerObserver.next(JSON.parse(data.body));
});
});
}
}
Authorization token is passed with the correct value. Hence there is no error in authorization.
Please let me know if anything else is required.
I am stuck at this point for quite a long time now.
Error:
WebSocket connection to 'ws://localhost:8080/websocket/blog/587/mklduqvp/websocket?access_token=eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJoYXJkZWVwc2h1a2xhMDhAbmljLmluIiwiYXV0aCI6IlJPTEVfRU1QTE9ZRUUiLCJleHAiOjE2MDUyNDUwNjd9.DJ2HITaVAiaphd2yg3yPAiLrLI4n8MjszjBasC3zOHrC-73mFdltDPEYHihY16VzPv0rh6EYLj84zCBv37TDNA' failed: Error during WebSocket handshake: Unexpected response code: 200

In your DefaultHandshakeHandler, can you try to annotate #Bean and set to public.
#Bean
public DefaultHandshakeHandler defaultHandshakeHandler() {

Related

How to get BearerTokenAuthentication from SecurityContext in a GlobalFilter in Spring Cloud Gateway

Spring Cloud Gateway as a OAuth2ResourceServer with following Authorisation Config:
#Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
.authorizeExchange(exchanges ->
exchanges
.anyExchange().authenticated()
)
.oauth2ResourceServer(OAuth2ResourceServerSpec::jwt)
return http.build();
}
I have a global filter in place which is responsible for performing some functions at each valid authenticated request, something like this:
#Service
public class CustomGlobal implements GlobalFilter {
#Autowired
BearerTokenAuthentication authentication;
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// access request headers, and perform some logic
// extract details from the JWT token, and perform some logic
log.info(authentication.getTokenAttributes.get("sub"));
// ^ in the above line there's a NullPointerException, since instance
// BearerTokenAuthentication is not set, or not visible at a GlobalFilter class
return chain.filter(exchange);
}
}
I am still in a learning phase. Any possible leads would be appreciated.
I did that this way(Note you should change WebFilter to GlobalFilter).
Add into your pom
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-jose</artifactId>
<version>5.4.6</version>
</dependency>
Then Filter should be like
package filter;
import lombok.extern.log4j.Log4j2;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
import org.springframework.security.oauth2.jwt.*;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
#Log4j2
public class CustomGlobal implements WebFilter {
public static final String HEADER_PREFIX = "Bearer ";
private final ReactiveJwtDecoder jwtDecoder;
public ReactiveJwtDecoder createDecoder(String issuer, String jwkUrl) {
var jwtDecoder = NimbusReactiveJwtDecoder.withJwkSetUri(jwkUrl).build();
jwtDecoder.setJwtValidator(new DelegatingOAuth2TokenValidator<>(
new JwtIssuerValidator(issuer),
new JwtTimestampValidator()));
return jwtDecoder;
}
protected CustomGlobal(String issuer, String jwkUrl) {
this.jwtDecoder = createDecoder(issuer, jwkUrl);
}
#Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
return Mono
.defer(() -> {
var token = resolveToken(exchange.getRequest());
if (!StringUtils.hasText(token)) {
throw new BadJwtException("Authorisation token is invalid");
}
return jwtDecoder.decode(token);
})
.flatMap(tokenJwt -> {
log.info(tokenJwt.getClaimAsString("sub"));
return chain.filter(exchange);
})
.onErrorResume(err -> handleError(exchange));
}
private Mono<Void> handleError(ServerWebExchange exchange) {
exchange.getResponse().setRawStatusCode(HttpStatus.UNAUTHORIZED.value());
exchange.getResponse().getHeaders().add("Content-Type", "application/json");
return exchange.getResponse().setComplete();
}
private String resolveToken(ServerHttpRequest request) {
String bearerToken = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(HEADER_PREFIX)) {
return bearerToken.substring(7).trim();
}
return "";
}
}
Next step would be to create configuration
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
#Configuration
public class CustomGlobalConfig {
#Value("${jwt.iss}")
private String issuer;
#Value("${jwt.jwk-uri}")
private String jwkUrl;
#Bean
CustomGlobal createFilterBean() {
return new CustomGlobal(this.issuer, this.jwkUrl);
}
}

Sticky session Ribbon rule in Zuul always has null request

I am attempting to implement a sticky session load balancer rule in a Zuul proxy service. I am using the code from this example: https://github.com/alejandro-du/vaadin-microservices-demo/blob/master/proxy-server/src/main/java/com/example/StickySessionRule.java
I seem to have everything configured correctly, and the rule is triggering in my debugger, but the call to RequestContext.getCurrentContext().getResponse() always returns null, so the cookie is never found, so the rule never takes effect.
The rest of the Zuul config is working 100%. My traffic is proxied and routed and I can use the app fine, only the sticky session rule is not working.
Is there another step I am missing to get the request wired in to this rule correctly?
My route config:
zuul.routes.appname.path=/appname/**
zuul.routes.appname.sensitiveHeaders=
zuul.routes.appname.stripPrefix=false
zuul.routes.appname.retryable=true
zuul.add-host-header=true
zuul.routes.appname.service-id=APP_NAME
hystrix.command.APP_NAME.execution.isolation.strategy=THREAD
hystrix.command.APP_NAME.execution.isolation.thread.timeoutInMilliseconds=125000
APP_NAME.ribbon.ServerListRefreshInterval=10000
APP_NAME.ribbon.retryableStatusCodes=500
APP_NAME.ribbon.MaxAutoRetries=5
APP_NAME.ribbon.MaxAutoRetriesNextServer=1
APP_NAME.ribbon.OkToRetryOnAllOperations=true
APP_NAME.ribbon.ReadTimeout=5000
APP_NAME.ribbon.ConnectTimeout=5000
APP_NAME.ribbon.EnablePrimeConnections=true
APP_NAME.ribbon.NFLoadBalancerRuleClassName=my.package.name.StickySessionRule
The app:
#EnableZuulProxy
#SpringBootApplication
public class ApplicationGateway {
public static void main(String[] args) {
SpringApplication.run(ApplicationGateway.class, args);
}
#Bean
public LocationRewriteFilter locationRewriteFilter() {
return new LocationRewriteFilter();
}
}
EDIT: As requested, the code:
import com.netflix.loadbalancer.Server;
import com.netflix.loadbalancer.ZoneAvoidanceRule;
import com.netflix.zuul.context.RequestContext;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
/**
* #author Alejandro Duarte.
*/
public class StickySessionRule extends ZoneAvoidanceRule {
public static final String COOKIE_NAME_SUFFIX = "-" + StickySessionRule.class.getSimpleName();
#Override
public Server choose(Object key) {
Optional<Cookie> cookie = getCookie(key);
if (cookie.isPresent()) {
Cookie hash = cookie.get();
List<Server> servers = getLoadBalancer().getReachableServers();
Optional<Server> server = servers.stream()
.filter(s -> s.isAlive() && s.isReadyToServe())
.filter(s -> hash.getValue().equals("" + s.hashCode()))
.findFirst();
if (server.isPresent()) {
return server.get();
}
}
return useNewServer(key);
}
private Server useNewServer(Object key) {
Server server = super.choose(key);
HttpServletResponse response = RequestContext.getCurrentContext().getResponse();
if (response != null) {
String cookieName = getCookieName(server);
Cookie newCookie = new Cookie(cookieName, "" + server.hashCode());
newCookie.setPath("/");
response.addCookie(newCookie);
}
return server;
}
private Optional<Cookie> getCookie(Object key) {
HttpServletRequest request = RequestContext.getCurrentContext().getRequest();
if (request != null) {
Server server = super.choose(key);
String cookieName = getCookieName(server);
Cookie[] cookies = request.getCookies();
if (cookies != null) {
return Arrays.stream(cookies)
.filter(c -> c.getName().equals(cookieName))
.findFirst();
}
}
return Optional.empty();
}
private String getCookieName(Server server) {
return server.getMetaInfo().getAppName() + COOKIE_NAME_SUFFIX;
}
}
I think you are missing a PreFilter, like this:
import com.netflix.zuul.context.RequestContext;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
public class PreFilter extends com.netflix.zuul.ZuulFilter {
#Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
RequestContext.getCurrentContext().set(FilterConstants.LOAD_BALANCER_KEY, ctx.getRequest());
return null;
}
#Override
public boolean shouldFilter() {
return true;
}
#Override
public int filterOrder() {
return FilterConstants.SEND_RESPONSE_FILTER_ORDER;
}
#Override
public String filterType() {
return "pre";
}
}
Mark as Bean
#Bean
public PreFilter preFilter() {
return new PreFilter();
}
And use it in your rule
#Override
public Server choose(Object key) {
javax.servlet.http.HttpServletRequest request = (javax.servlet.http.HttpServletRequest) key;
RequestContext not working cause "hystrix.command.APP_NAME.execution.isolation.strategy=THREAD"

How can configure activemq ex. queue time to live , or keep the message in queue until period of time or action?

I am building notification system using spring boot and websocket, I used ActiveMQ to keep Queues for offlines users, it's working perfect.
I need to edit some configuration like queue time to live, keep message in queue until user read it, I don't know how can configure it?
The below is its implementation:
#Configuration
#EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
/*config.enableSimpleBroker("/topic");
config.setApplicationDestinationPrefixes("/app");*/
config
.setApplicationDestinationPrefixes("/app")
.setUserDestinationPrefix("/user")
.enableStompBrokerRelay("/topic","/queue","/user")
.setRelayHost("localhost")
.setRelayPort(61613)
.setClientLogin("guest")
.setClientPasscode("guest");
}
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/websocket").withSockJS();
}
}
And:
#Service
public class NotificationWebSocketService {
#Autowired
private SimpMessagingTemplate messagingTemplate;
public void initiateNotification(WebSocketNotification notificationData) throws InterruptedException {
messagingTemplate.convertAndSendToUser(notificationData.getUserID(), "/reply", notificationData.getMessage());
}
}
After invoke NotificationWebSocketService it will create queue "/user/Johon/reply" in activemq contains message when user subscribe in this queue message will received.
How can configure queue time to live , keep message in queue until user read it?
Unit-test to illustrate how to set up expiration of message in user queue.
Required tomcat-embedded, spring-messaging, and active-mq
import org.apache.catalina.Context;
import org.apache.catalina.Wrapper;
import org.apache.catalina.connector.Connector;
import org.apache.catalina.startup.Tomcat;
import org.apache.coyote.http11.Http11NioProtocol;
import org.apache.tomcat.util.descriptor.web.ApplicationListener;
import org.apache.tomcat.websocket.server.WsContextListener;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.messaging.simp.config.ChannelRegistration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.messaging.simp.stomp.*;
import org.springframework.messaging.support.ChannelInterceptorAdapter;
import org.springframework.web.SpringServletContainerInitializer;
import org.springframework.web.WebApplicationInitializer;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;
import org.springframework.web.socket.WebSocketHttpHeaders;
import org.springframework.web.socket.client.standard.StandardWebSocketClient;
import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.messaging.WebSocketStompClient;
import org.springframework.web.socket.sockjs.client.SockJsClient;
import org.springframework.web.socket.sockjs.client.WebSocketTransport;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Type;
import java.util.*;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import static java.util.concurrent.TimeUnit.SECONDS;
public class Test48402361 {
private static final Logger logger = LoggerFactory.getLogger(Test48402361.class);
private static TomcatWebSocketTestServer server = new TomcatWebSocketTestServer(33333);
#BeforeClass
public static void beforeClass() throws Exception {
server.deployConfig(Config.class);
server.start();
}
#AfterClass
public static void afterClass() throws Exception {
server.stop();
}
#Test
public void testUser() throws Exception {
WebSocketStompClient stompClient = new WebSocketStompClient(new SockJsClient(Collections.singletonList(new WebSocketTransport(new StandardWebSocketClient()))));
BlockingQueue<String> blockingQueue = new LinkedBlockingQueue<>();
StompSession session = stompClient
.connect("ws://localhost:" + server.getPort() + "/test", new WebSocketHttpHeaders(), new StompSessionHandlerAdapter() {
})
.get();
// waiting until message 2 expired
Thread.sleep(3000);
session.subscribe("/user/john/reply", new StompFrameHandler() {
#Override
public Type getPayloadType(StompHeaders headers) {
return byte[].class;
}
#Override
public void handleFrame(StompHeaders headers, Object payload) {
String message = new String((byte[]) payload);
logger.debug("message: {}, headers: {}", message, headers);
blockingQueue.add(message);
}
});
String message = blockingQueue.poll(1, SECONDS);
Assert.assertEquals("1", message);
message = blockingQueue.poll(1, SECONDS);
Assert.assertEquals("3", message);
}
public static class Config extends AbstractAnnotationConfigDispatcherServletInitializer {
#Override
protected Class<?>[] getRootConfigClasses() {
return new Class[] { };
}
#Override
protected Class<?>[] getServletConfigClasses() {
return new Class[] { Mvc.class };
}
#Override
protected String[] getServletMappings() {
return new String[] { "/" };
}
}
#Configuration
#EnableWebSocketMessageBroker
public static class Mvc extends AbstractWebSocketMessageBrokerConfigurer {
#Override
public void registerStompEndpoints(StompEndpointRegistry stompEndpointRegistry) {
stompEndpointRegistry.addEndpoint("/test")
.withSockJS()
.setWebSocketEnabled(true);
}
#Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableStompBrokerRelay("/user").setRelayHost("localhost").setRelayPort(61614);
}
#Autowired
private SimpMessagingTemplate template;
#Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.setInterceptors(new ChannelInterceptorAdapter() {
#Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor sha = StompHeaderAccessor.wrap(message);
switch (sha.getCommand()) {
case CONNECT:
// after connect we send 3 messages to user john, one will purged after 2 seconds.
template.convertAndSendToUser("john", "/reply", "1");
Map<String, Object> headers = new HashMap<>();
headers.put("expires", System.currentTimeMillis() + 2000);
template.convertAndSendToUser("john", "/reply", "2", headers);
template.convertAndSendToUser("john", "/reply", "3");
break;
}
return super.preSend(message, channel);
}
});
}
}
public static class TomcatWebSocketTestServer {
private static final ApplicationListener WS_APPLICATION_LISTENER =
new ApplicationListener(WsContextListener.class.getName(), false);
private final Tomcat tomcatServer;
private final int port;
private Context context;
public TomcatWebSocketTestServer(int port) {
this.port = port;
Connector connector = new Connector(Http11NioProtocol.class.getName());
connector.setPort(this.port);
File baseDir = createTempDir("tomcat");
String baseDirPath = baseDir.getAbsolutePath();
this.tomcatServer = new Tomcat();
this.tomcatServer.setBaseDir(baseDirPath);
this.tomcatServer.setPort(this.port);
this.tomcatServer.getService().addConnector(connector);
this.tomcatServer.setConnector(connector);
}
private File createTempDir(String prefix) {
try {
File tempFolder = File.createTempFile(prefix + '.', "." + getPort());
tempFolder.delete();
tempFolder.mkdir();
tempFolder.deleteOnExit();
return tempFolder;
} catch (IOException ex) {
throw new RuntimeException("Unable to create temp directory", ex);
}
}
public int getPort() {
return this.port;
}
#SafeVarargs
public final void deployConfig(Class<? extends WebApplicationInitializer>... initializers) {
this.context = this.tomcatServer.addContext("", System.getProperty("java.io.tmpdir"));
// Add Tomcat's DefaultServlet
Wrapper defaultServlet = this.context.createWrapper();
defaultServlet.setName("default");
defaultServlet.setServletClass("org.apache.catalina.servlets.DefaultServlet");
this.context.addChild(defaultServlet);
// Ensure WebSocket support
this.context.addApplicationListener(WS_APPLICATION_LISTENER);
this.context.addServletContainerInitializer(
new SpringServletContainerInitializer(), new HashSet<>(Arrays.asList(initializers)));
}
public void start() throws Exception {
this.tomcatServer.start();
}
public void stop() throws Exception {
this.tomcatServer.stop();
}
}
}
"stompClient.subscribe('/user/Johon/reply' --> '/user/Johon/reply' is a topic and not a queue.
If your Stomp client is not connected to the topic '/user/Johon/reply' he will loose every message sent to that topic.
So your solutions are :
convert your topic '/user/Johon/reply' to a queue, so the message remains on the queue indefinitely or until the server end-processes the message.
use Retroactive Consumer & Subscription Recovery Policy
A retroactive consumer is just a regular JMS Topic consumer who
indicates that at the start of a subscription every attempt should be
used to go back in time and send any old messages (or the last message
sent on that topic) that the consumer may have missed.
http://activemq.apache.org/retroactive-consumer.html
The subscription recovery policy allows you to go back in time when
you subscribe to a topic.
http://activemq.apache.org/subscription-recovery-policy.html
Use Durable Subscribers
Durable topic subscribers that are offline for a long period of time
are usually not desired in the system. The reason for that is that
broker needs to keep all the messages sent to those topics for the
said subscribers. And this message piling can over time exhaust broker
store limits for example and lead to the overall slowdown of the
system.
http://activemq.apache.org/manage-durable-subscribers.html
Durable Subscribers with Stomp :
http://activemq.apache.org/stomp.html#Stomp-ActiveMQExtensionstoSTOMP
CONNECT client-id string Specifies the JMS clientID which is used in
combination with the activemq.subcriptionName to denote a durable
subscriber.
some explanations about TTL
A client can specify a time-to-live value in milliseconds for each
message it sends. This value defines a message expiration time that is
the sum of the message's time-to-live and the GMT when it is sent (for
transacted sends, this is the time the client sends the message, not
the time the transaction is committed).
the default time to live is 0, so the message remains on the queue
indefinitely or until the server end-processes the message
UPDATE
if you want to use external ActiveMQ Broker
remove #EnableWebSocketMessageBroker and add to your activemq.xml below connector and restart the broker.
<transportConnector name="stomp" uri="stomp://localhost:61613"/>
if you want to embedd ActiveMQ Broker, add bean to you WebSocketConfig :
#Bean(initMethod = "start", destroyMethod = "stop")
public BrokerService broker() throws Exception {
final BrokerService broker = new BrokerService();
broker.addConnector("stomp://localhost:61613");
return broker;
}
and required dependencies
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-activemq</artifactId>
</dependency>
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>activemq-stomp</artifactId>
</dependency>
full examples
Spring Boot WebSocket with embedded ActiveMQ Broker
http://www.devglan.com/spring-boot/spring-boot-websocket-integration-example

How to prevent Redis writes for anonymous user sessions

I have this sample application:
package com.example.session;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
#SpringBootApplication
public class DemoRedisDataSessionApplication {
#Configuration
#EnableWebSecurity
#EnableRedisHttpSession(redisNamespace = "demo-redis-data-session")
public static class AppConfiguration extends WebSecurityConfigurerAdapter {
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().withUser("user").password("0000").roles("USER");
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin().and()
.authorizeRequests().antMatchers("/ping").permitAll().and()
.authorizeRequests().anyRequest().fullyAuthenticated();
}
}
#RestController
public static class AppController {
#GetMapping("/ping")
public String ping() {
return "pong";
}
#GetMapping("/secured")
public String secured() {
return "secured";
}
}
public static void main(String[] args) {
SpringApplication.run(DemoRedisDataSessionApplication.class, args);
}
}
When I hit /secured I get 302 redirected to the /login form, which is what I expect if I am not logged in, but I get some unwanted entries in Redis:
127.0.0.1:6379> keys *
1) "spring:session:demo-redis-data-session:sessions:expires:dbb124b9-c37d-454c-8d67-409f28cb88a6"
2) "spring:session:demo-redis-data-session:expirations:1515426060000"
3) "spring:session:demo-redis-data-session:sessions:dbb124b9-c37d-454c-8d67-409f28cb88a6"
I don't want to create this data for every anonymous user (read crawler), so is there a way to prevent these Redis entries when hitting a secured endpoint/page with an anonymous user?
Additional data used for this sample project
 docker-compose.yml
version: "2"
services:
redis:
image: redis
ports:
- "6379:6379"
Spring Boot version
1.5.9.RELEASE
This is not the optimal solution since it creates only one session for all crawlers, but at least I don't get Redis full of unwanted session.
import lombok.extern.log4j.Log4j;
import org.springframework.session.Session;
import org.springframework.session.SessionRepository;
import org.springframework.session.web.http.CookieHttpSessionStrategy;
import org.springframework.session.web.http.MultiHttpSessionStrategy;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
#Log4j
#Component
public class CrawlerManagerSessionStrategyWrapper implements MultiHttpSessionStrategy {
private CookieHttpSessionStrategy delegate;
private volatile String crawlerSessionId;
public CrawlerManagerSessionStrategyWrapper() {
this.delegate = new CookieHttpSessionStrategy();
}
public String getRequestedSessionId(HttpServletRequest request) {
String sessionId = getSessionIdForCrawler(request);
if (sessionId != null)
return sessionId;
else {
return delegate.getRequestedSessionId(request);
}
}
public void onNewSession(Session session, HttpServletRequest request, HttpServletResponse response) {
delegate.onNewSession(session, request, response);
if (isCrawler(request)) {
crawlerSessionId = session.getId();
}
}
public void onInvalidateSession(HttpServletRequest request, HttpServletResponse response) {
delegate.onInvalidateSession(request, response);
}
public HttpServletRequest wrapRequest(HttpServletRequest request, HttpServletResponse response) {
return request;
}
public HttpServletResponse wrapResponse(HttpServletRequest request, HttpServletResponse response) {
return response;
}
private String getSessionIdForCrawler(HttpServletRequest request) {
if (isCrawler(request)) {
SessionRepository<Session> repo = (SessionRepository<Session>) request.getAttribute(SessionRepository.class.getName());
if (crawlerSessionId != null && repo != null) {
Session session = repo.getSession(crawlerSessionId);
if (session != null) {
return crawlerSessionId;
}
}
}
return null;
}
private boolean isCrawler(HttpServletRequest request) {
// Here goes the logic to understand if the request comes from a crawler, for example by checking the user agent.
return true;
}
}
The only thing to implement is the isCrawler method to state if the request comes from a crawler.

spring websocket+rabbitmq stomp:How can i get all online user and disconnect some user’s connect?

If spring store user's websocket session,how can get it,if i use someHandler to put session in a map,that mean map‘s size maybe bigger than 10w+,I don't think it good。
SessionHandler.java
package hello;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.web.socket.WebSocketSession;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
#Service
public class SessionHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(SessionHandler.class);
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
private final Map<String, WebSocketSession> sessionMap = new ConcurrentHashMap<>();
public SessionHandler() {
scheduler.scheduleAtFixedRate(new Runnable() {
#Override
public void run() {
sessionMap.keySet().forEach(k -> {
try {
sessionMap.get(k).close();
sessionMap.remove(k);
} catch (IOException e) {
LOGGER.error("Error while closing websocket session: {}", e);
}
});
}
}, 10, 10, TimeUnit.SECONDS);
}
public void register(WebSocketSession session) {
sessionMap.put(session.getId(), session);
}
}
like this:
https://github.com/isaranchuk/spring-websocket-disconnect/blob/master/src/main/java/hello/SessionHandler.java
Implement the HandshakeInter as below.
public class HttpSessionIdHandshakeInterceptor implements HandshakeInterceptor {
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(false);
if (session != null) {
attributes.put(SESSION_ATTR, session.getId());
}
}
return true;
}
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response,
WebSocketHandler wsHandler, Exception ex) {
}
}
Then Add to the endpoint.
public class WebSocketConfig extends
AbstractWebSocketMessageBrokerConfigurer {
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/portfolio")
.withSockJS()
.setInterceptors(new HttpSessionIdHandshakeInterceptor());
}
...
}
Next we can create a ChannelInterceptorAdapter that uses the session id to update the last accessed time using Spring Session. For example:
#Bean public ChannelInterceptorAdapter sessionContextChannelInterceptorAdapter() {
return new ChannelInterceptorAdapter() {
#Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
Map<String, Object> sessionHeaders = SimpMessageHeaderAccessor.getSessionAttributes(message.getHeaders());
String sessionId = (String) sessionHeaders.get(SESSION_ATTR);
if (sessionId != null) {
Session session = sessionRepository.getSession(sessionId);
if (session != null) {
sessionRepository.save(session);
}
}
return super.preSend(message, channel);
}
};
}

Resources