How does Spring Boot RSocket detect disconnection? - java

everyone.
I want to make chatting server using RSocket.
The code below stores user information when the user accesses it.
private final List<RSocketRequester> CLIENTS = new ArrayList<>();
#ConnectMapping
public void onConnect(RSocketRequester reqer) {
log.info("RSocket Connect Mapping Start : reqer {}", reqer.hashCode());
reqer.rsocket()
.onClose()
.doFirst(() -> CLIENTS.add(reqer))
.doOnError(error -> log.info("RSocket Connect Error : error {}", error))
.doFinally(consumer -> {
CLIENTS.remove(reqer);
})
.subscribe();
log.info("RSocket Connect Mapping End : Clients {}", this.CLIENTS.size());
}
spring:
rsocket:
server:
port: 6565
transport: websocket
mapping-path: /rs
I want to get user information when the user disconnects.
Can the RSocket detect when the user disconnects?
help me please.

If you configured rsocket security you can access AuthenticationPrincipal at onConnect().
Simple example
#ConnectMapping
public void onConnect(RSocketRequester requester, #AuthenticationPrincipal Jwt principal) {
requester.rsocket()
.onClose()
.doFirst(() -> {
System.out.println(principal.getSubject());
System.out.println("-----------------CONNNEEEECTED-----------------");
})
.doOnError(error -> {
})
.doFinally(consumer -> {
System.out.println(principal.getSubject());
System.out.println("-----------------DISCONNNNNECTED-----------------");
})
.subscribe();
}

Related

Can I use Spring Cloud Gateway for role based authorization?

I have several microservices in my architecture. I want to implement an API Gateway to route request to services. To achieve that, I implement spring-cloud-gateway and this is my application.yml
server:
port: 9090
spring:
application:
name: "API-GATEWAY"
cloud:
gateway:
routes:
- id: task-service
uri: 'http://localhost:8083'
predicates:
- Path=/task/**
So far everything works as expected. a request localhost:9090/task/123 is to localhost:8083/task/123. Here comes to second part.
I want some users access to only some endpoints. In my JWT token, I have role field.
{
"accountName": "erdem.ontas",
"surname": "Öntaş",
"roles": [
"ADMIN",
"USER"
],
}
I don't want specify authorization in every service separately, is there any way to specify role based access in spring-cloud-gateway? For example I want USER role to be able to access to GET http://localhost:9090/task/ but not to GET http://localhost:9090/dashboard/
If you do not want and need to create full OAuth 2 Server/Client infrastructure and want to keep it simple just create a custom GatewayFilter in which just check if the JWT token extracted from the header has the preconfigured roles.
So start with a simple GatewayFilter
#Component
public class RoleAuthGatewayFilterFactory extends
AbstractGatewayFilterFactory<RoleAuthGatewayFilterFactory.Config> {
public RoleAuthGatewayFilterFactory() {
super(Config.class);
}
#Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
var request = exchange.getRequest();
// JWTUtil can extract the token from the request, parse it and verify if the given role is available
if(!JWTUtil.hasRole(request, config.getRole())){
// seems we miss the auth token
var response = exchange.getResponse();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
return chain.filter(exchange);
};
}
#Data
public static class Config {
private String role;
}
#Override
public List<String> shortcutFieldOrder() {
// we need this to use shortcuts in the application.yml
return Arrays.asList("role");
}
}
Here we just create a simple filter which receives the required role from the config (application.yml) and checks if the request is authorized to continue.
To use the filter just add filters into you route config.
server:
port: 9090
spring:
application:
name: "API-GATEWAY"
cloud:
gateway:
routes:
- id: task-service
uri: 'http://localhost:8083'
filters:
- RoleAuth=ADMIN
predicates:
- Path=/task/**
So this way the RoleAuth filter can be reused over the several routes.

Sending triggers to client using websockets in spring boot and angular

I want to initiate a trigger(maybe a notification) from backend(based in spring boot) to a particular user whose userId is xyz.
the one way i have found is:
initially i connect to a websocket end point and subscribe to channel "/user/Notifications/xyz"
following is the relevant code in my angular typescript
connectToUserWebSocket(userId) {
let socket = new SockJS('http://localhost:5000/fellowGenius');
this.ws = Stomp.over(socket);
let that = this;
this.ws.connect(
{},
(frame) => {
that.ws.subscribe('/user/Notifications/' +userId, (message) => {
console.log("user subscribed");
});
},
(error) => {
alert('STOMP error ' + error);
}
);
}
Now once i have subscribed to my channel . I want to send a trigger to client which is initiated by backend itself so i run a code in my java service.
My relevant java code is:
#SendTo("/user/Notifications/{userId}")
public String sendMeetingNotificationWebSocket(#DestinationVariable String userId) {
return "hello";
}
my websocket configurations are:
#Configuration
#EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer{
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/fellowGenius").setAllowedOrigins("*").addInterceptors(new HttpSessionHandshakeInterceptor()).withSockJS();
}
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/inbox/","/user/Notifications/");
}
}
But the problem is that even i can see one web socket connected in my spring boot console.
But i don't get a response from the function on the client side.
Please help me with this problem.

RSocket Java Client to access Binance websocket API

I am trying to connect to Binance websocket endpoint using RSocket with Spring, but I am getting the following exception.
java.lang.AssertionError: expectation "consumeNextWith" failed (expected: onNext(); actual: onError(java.nio.channels.ClosedChannelException))
Here is my testing code:
#SpringBootTest
#Slf4j
class RSocketClientIntegrationTest {
private static RSocketRequester requester;
#BeforeAll
public static void setupOnce(#Autowired RSocketRequester.Builder builder) {
// given
requester = builder
.connectWebSocket(URI.create("wss://stream.binance.com:9443/ws"))
.block();
}
#Test
void shouldRetrieveStockPricesFromTheService() {
//when
final Flux<String> aggregatedTradeStream = requester
.route("/bnbbtc#aggTrade")
.retrieveFlux(String.class)
.take(1)
.doOnError(e -> log.error(e.toString()));
// then
StepVerifier.create(aggregatedTradeStream)
.consumeNextWith(response -> {
assertThat(response).isNotNull();
})
.verifyComplete();
}
}
Can I connect straight into a WebSocket endpoint using RSocket or do I have to create a RSocket server that handles the websocket and then from this proxy server I connect to the websocket?
RSocket over WebSocket uses websocket as a transport to carry RSocket payloads. So you can't connect to a raw WebSocket and expect anything to work.
Try testing the above program against an endpoint like
wss://rsocket-demo.herokuapp.com/rsocket
See docs at https://rsocket-demo.herokuapp.com/

Java Vertx unable to provide user id password for my post request using webClient

I am new to Java and Vert.x
I am Using vertx 3.5.3
I am trying to implement basic Authentication so that i can log into the remote sever before sending the post request
I tried the below code which is giving a compiler error .
I am getting a compiler error .
The method basicAuthentication(String, String) is undefined for the type HttpRequest What am i doing wrong .
The requirement is that i need to send user id and password for the remote server and access end point with a post request using a json Post body using Vert.x web client
I tried the below code
package com.aexp.csrt.qs.cb.resources;
import com.aexp.csrt.qs.models.cb.passTrou.SourceQueryModel;
import io.vertx.reactivex.ext.web.RoutingContext;
import io.vertx.reactivex.core.Vertx;
import io.vertx.ext.web.client.WebClient;
import io.vertx.ext.web.client.WebClientOptions;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.client.HttpResponse;
import io.vertx.core.buffer.Buffer;
public class QueryExecutorFTS implements QueryExecutor {
private WebClient webClient;
#Override
public void executeQuery(SourceQueryModel Qm, RoutingContext rc) {
WebClientOptions options = new WebClientOptions().setMaxPoolSize(10).setConnectTimeout(5000)
.setVerifyHost(false);
JsonObject jreq = new JsonObject(Qm.getSourceQuery().getSourceDsl().getQuery());
Vertx vertx = rc.vertx();
webClient = WebClient.create(vertx.getDelegate(), options);
webClient
.basicAuthentication("myid", "mypassword")
.post(8094, "lpdospdb51079.phx.aexp.com", "/api/index/Text_search_name_idx/query")
.sendJsonObject(jreq,
ar -> {
if (ar.succeeded()) {
HttpResponse<Buffer> response = ar.result();
rc.response().setStatusCode(200).end(response.bodyAsString());
} else {
ar.cause().printStackTrace();
rc
.response()
.setStatusCode(500)
.setStatusMessage(ar.cause().getMessage())
.end();
}
})
;
}
}
Assuming you using the session handler on the server side like so:
router.route().handler(CookieHandler.create());
router.route().handler(SessionHandler.create(LocalSessionStore.create(vertx)));
router.route().handler(UserSessionHandler.create(authProvider));
you now need to first authenticate your client with the url you defined for login.
your login path -> /login
router.route(HttpMethod.POST, path)
.handler(FormLoginHandler.create(authProvider));
you normally POST the user / pass JSON
now on the client side you need two steps
POST your cred to the login URL
MultiMap form = MultiMap.caseInsensitiveMultiMap();
form.add("username", user);
form.add("password", password);
client
.post(port, "localhost", "/login")
.putHeader("content-type", "multipart/form-data")
.sendForm(form, ar -> {
if (ar.succeeded()) {
log.info("RESOURCES: {}", ar.result().bodyAsString());
log.info("COOKIES: {}", ar.result().cookies());//.getHeader("vertx-web.session"));
ar.result().cookies().stream().filter(c -> c.startsWith("vertx-web.session")).findFirst().ifPresent(s -> {
// do something with the session cookie - >cookie.accept(s.substring(0, s.indexOf(";")));
});
} else {
log.error("Something went wrong", ar.cause());
}
});
once you have the cookie you can call the server again.
client.
.get(port, "localhost", "/resource")
.putHeader("Cookie", cookie)
.send(ar -> { ...

Send Notification to specific user in spring boot websocket

I want to send notification to specific client.
e.g username user
#Configuration
#EnableWebSocketMessageBroker
public class WebSocketConfiguration extends
AbstractWebSocketMessageBrokerConfigurer {
#Override
public void registerStompEndpoints(StompEndpointRegistry stompEndpointRegistry) {
stompEndpointRegistry.addEndpoint("/socket")
.setAllowedOrigins("*")
.withSockJS();
}
#Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/topic", "/queue");
registry.setApplicationDestinationPrefixes("/app");
}
Controller
#GetMapping("/notify")
public String getNotification(Principal principal) {
String username = "user";
notifications.increment();
logger.info("counter" + notifications.getCount() + "" + principal.getName());
// logger.info("usersend:"+sha.getUser().getName()) ; //user
template.convertAndSendToUser(principal.getName(), "queue/notification", notifications);
return "Notifications successfully sent to Angular !";
}
Client-Side
Angular Service
connect() {
let socket = new SockJs(`api/socket`);
let stompClient = Stomp.over(socket);
return stompClient;
}
Angular Component
let stompClient = this.webSocketService.connect();
stompClient.connect({}, frame => {
stompClient.subscribe('/user/queue/notification', notifications => {
console.log('test'+notifications)
this.notifications = JSON.parse(notifications.body).count;
}) });
I am have searched many other questions and tried but none of them worked for me
e.g here answered by Thanh Nguyen Van and here
Console
Opening Web Socket...
stomp.js:134 Web Socket Opened...
stomp.js:134 >>> CONNECT
accept-version:1.1,1.0
heart-beat:10000,10000
stomp.js:134 <<< CONNECTED
version:1.1
heart-beat:0,0
stomp.js:134 connected to server undefined
reminder.component.ts:18 test callsed
stomp.js:134 >>> SUBSCRIBE
id:sub-0
destination:/user/queue/notification
thanks in advance .
The answer of gerrytan to Sending message to specific user on Spring Websocket mentions a web socket configuration change, to register the /user prefix. In your case I guess it means to replace
registry.enableSimpleBroker("/topic", "/queue");
with
registry.enableSimpleBroker("/topic", "/queue", "/user");
He also says that in controller you don't need the /user prefix because it is added automatically. So you could try this:
template.convertAndSendToUser(principal.getName(), "/queue/notification", notifications);
and this:
template.convertAndSendToUser(principal.getName(), "/user/queue/notification", notifications);
On the client side you need to provide the username that you used to connect to server. You might insert it directly:
stompClient.subscribe('/user/naila/queue/notification', ...)
or get it from a header. But Markus says at How to send websocket message to concrete user? that even here you don't need the username, so it might work like this:
stompClient.subscribe('/user/queue/notification', ...)
Seems you are missing a slash in your destination:
template.convertAndSendToUser(principal.getName(), "/queue/notification", notifications);

Categories

Resources