How to retrieve List by a Consumer with Spring and rabbitmq - spring-mvc

I have two projects
WebApp(SpringMVC)
Microservice
The idea is that I have a page in which I list all the users from DB, so basically I need a listener on WebApp side and a producer in the microservice side, typically the flow is as follow
Whitout rabbitmq(synchronous)
Click page "List Users"
UserController that redirect me to a specific service
public List<User>getUsers(){//no args!!
service.getUsers();//no args
}
UserService with logic to access DB and retrieve all users
public List<User>getUsers(){//no args!!
//connect to DB and retrieve all users
return users
}
Render users on jsp
With RabbitMQ and assuming users has already been produced the list of users on the microservice's side
My question is about if I introduce rabbitmq then I need a method in which I listen a message(List of products as a JSON) but now the flow change a little bit comparing with the first one because
Click button "List Users"
Controller need a method findAll(Message message), here I need pass a message because the service is expecting one as the service is Listener
public List<User>getUsers(Message message){
service.getAllUsers(**String message**);
}
The service as right now is listen a message, I need to pass a Message
arg in which I will be listen the queues
#RabbitListener(queues = "${queue}", containerFactory = "fac")
public List<User> getUsers(String message){
//Transform JSON to POJO
//some logic...
return users;
}
So basically my question is the second flow is correct?
If so how I have to pass the Message object from controller to service
because in controller I do not needed a Message, but in order to
listen I have, is this correct?
If so how pass the message arg
There is a better way to achieve this?
Regards

Use a Jackson2JsonMessageConverter and the framework will do all the conversion for you.
Controller:
List<User> users = template.convertSendAndReceiveAsType("someExchange", "someRoutingKey",
myRequestPojo, new ParameterizedTypeReference<List<User>>() {});
Server:
#RabbitListener(queues = "${queue}", containerFactory = "fac")
public List<User> getUsers(SomeRequestPojo request){
//some logic...
return users;
}
Wire the converter into both the listener container factory and the RabbitTemplate.
Request Pojo and Users have to be Jackson-friendly (no-arg constructor, setters).

Related

OAuth2 Share Principal Object with Multiple Gateway Instances

I have integrated Spring Cloud Gateway with OAuth2 server. It works well with single instance gateway. here is my security config.
#EnableWebFluxSecurity
public class GatewaySecurityConfiguration {
#Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
http
.authorizeExchange().pathMatchers("/user/v3/api-docs", "/actuator/**").permitAll()
.anyExchange().authenticated()
.and()
.oauth2Login()
.and()
.csrf().disable();
return http.build();
}
But, When I scale gateway to 2 instances, some requests works expected, however some requests return 401.
load balancer (kubernetes nodeport service)
/ \
gateway gateway
\ /
(microservice clusters)
When I logged in first instance of gateway, the principal object is created successfully and also assign session to redis. If next request comes to second instance, it returns 401 because it has not principal.
how can i solve this problem?
ps: i am using redis for web sessions to commonize session informations between gateways.
TL;DR
You can share session principal information on Redis through WebSession. But you can't share access token(JWT), because they are stored in-memory at servers.
Solution-1: Your requests should always go to the server where you are logged in. (details below)
Solution-2: Implement new ReactiveOAuth2AuthorizedClientService bean that stores sessions in redis. (details below too)
Long Answer
From Spring Cloud documentation (https://cloud.spring.io/spring-cloud-static/Greenwich.SR5/multi/multi__more_detail.html);
The default implementation of ReactiveOAuth2AuthorizedClientService
used by TokenRelayGatewayFilterFactory uses an in-memory data store.
You will need to provide your own implementation
ReactiveOAuth2AuthorizedClientService if you need a more robust
solution.
The first thing you know: When you login successfully, the access token(as jwt) is returned by oauth2 server, and server creates session and this session is mapped to access token on ConcurrentHashMap (authorizedClients instance InMemoryReactiveOAuth2AuthorizedClientService class).
When you request API Gateway to access microservices with your session id, the access token(jwt) is resolved by TokenRelayGatewayFilterFactory in gateway, and this access token is set in Authorization header, and the request is forwarding to microservices.
So, let me explain how TokenRelayGatewayFilterFactory works (assume that you use WebSession through Redis and you have 2 gateway instances and you logged in at instance-1.)
If your request goes to instance-1, the principal is get back by session id from redis, then authorizedClientRepository.loadAuthorizedClient(..) is called in filter. This repository is instance of AuthenticatedPrincipalServerOAuth2AuthorizedClientRepository object. The isPrincipalAuthenticated() method returns true, so the flow goes on authorizedClientService.loadAuthorizedClient(). this service is defined as ReactiveOAuth2AuthorizedClientService interface, and it has only one implementation ( InMemoryReactiveOAuth2AuthorizedClientService). This implementation has ConcurrentHashMap(key: principal object, value: JWT)
If your request goes to instance-2, all flow above are valid. But reminder that ConcurrentHashMap no access token to principal, because the access token is stored in instance-1's ConcurrentHashMap. So, the access token is empty, then your request downstreams without Authorization header. You will get 401 Unauthorized.
Solution-1
So, your requests should always go to the server where you are logged in to get valid access token.
If you use NGINX as load balancer, then use ip_hash in upstream.
If you use kubernetes service as load balancer, then use ClientIP in session affinity.
Solution-2
InMemoryReactiveOAuth2AuthorizedClientService is only implementation of ReactiveOAuth2AuthorizedClientService. So, create new implementation that uses Redis, and then do it primary bean.
#RequiredArgsConstructor
#Slf4j
#Component
#Primary
public class AccessTokenRedisConfiguration implements ReactiveOAuth2AuthorizedClientService {
private final SessionService sessionService;
#Override
#SuppressWarnings("unchecked")
public <T extends OAuth2AuthorizedClient> Mono<T> loadAuthorizedClient(String clientRegistrationId, String principalName) {
log.info("loadAuthorizedClient for user {}", principalName);
Assert.hasText(clientRegistrationId, "clientRegistrationId cannot be empty");
Assert.hasText(principalName, "principalName cannot be empty");
// TODO: When changed immutability of OAuth2AuthorizedClient, return directly object without map.
return (Mono<T>) sessionService.getSessionRecord(principalName, "accessToken").cast(String.class)
.map(mapper -> {
return new OAuth2AuthorizedClient(clientRegistration(), principalName, accessToken(mapper));
});
}
#Override
public Mono<Void> saveAuthorizedClient(OAuth2AuthorizedClient authorizedClient, Authentication principal) {
log.info("saveAuthorizedClient for user {}", principal.getName());
Assert.notNull(authorizedClient, "authorizedClient cannot be null");
Assert.notNull(principal, "principal cannot be null");
return Mono.fromRunnable(() -> {
// TODO: When changed immutability of OAuth2AuthorizedClient , persist OAuthorizedClient instead of access token.
sessionService.addSessionRecord(principal.getName(), "accessToken", authorizedClient.getAccessToken().getTokenValue());
});
}
#Override
public Mono<Void> removeAuthorizedClient(String clientRegistrationId, String principalName) {
log.info("removeAuthorizedClient for user {}", principalName);
Assert.hasText(clientRegistrationId, "clientRegistrationId cannot be empty");
Assert.hasText(principalName, "principalName cannot be empty");
return null;
}
private static ClientRegistration clientRegistration() {
return ClientRegistration.withRegistrationId("login-client")
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.clientId("dummy").registrationId("dummy")
.redirectUriTemplate("dummy")
.authorizationUri("dummy").tokenUri("dummy")
.build();
}
private static OAuth2AccessToken accessToken(String value) {
return new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, value, null, null);
}
}
Notes:
SessionService is my custom class that interacts redis with reactivehashoperations instance.
The best way is to store OAuth2AuthorizedClient instead of access token. But, it is too hard now (https://github.com/spring-projects/spring-security/issues/8905)
The TokenRelayGatewayFilterFactory uses an in-memory data store to store the OAuth2AuthorizedClient which includes the (JWT) access token. This data store is not shared between multiple gateways.
To share the OAuth2AuthorizedClient information with Spring Session through Redis provide the following configuration:
#Bean
public OAuth2AuthorizedClientRepository authorizedClientRepository() {
return new HttpSessionOAuth2AuthorizedClientRepository();
}
For reactive WebSessions:
#Bean
public ServerOAuth2AuthorizedClientRepository authorizedClientRepository() {
return new WebSessionServerOAuth2AuthorizedClientRepository();
}
Further information for this configuration can be found at https://github.com/spring-projects/spring-security/issues/7889

Signalr Core Adding user to Groups OnConnected Security

I've seen one article that said groups were bad to use if you needed anything to be secure, but I'm wondering if that is still true or how someone would take advantage of this scenario:
Say I had one company with multiple subdivisions. I want to send notifications only to a particular subdivision when an action happens. Even though it's odd, say in this scenario that it would be bad security wise if a different subdivision saw the notification.
The client hub does not have any "send" methods on it so it looks like this:
[Authorize]
public class NotificationsHub : Hub
{
}
I then have a service class on the api that uses the IHubContext to send a notification to the group
public class NotificationService : INotificationService{
public async Task UpdateScheduleAsync(string subdivision, string message)
{
await _hubContext.Clients.Group(subdivision).SendAsync("ReceiveMessage",message);
}
}
If I added in the NotificationsHub the method:
public override async Task OnConnectedAsync()
{
//may or may not use the identifier
var someUserIdentityInfo = Context.UserIdentifier;
//would grab the subdivision the authorized user is apart of
var subdivision = GrabSubdivisonFromIdentity(someUserIdentityInfo);
//add to the subdivison group
foreach(var division in subdivison){
await Groups.AddToGroupAsync(Context.ConnectionId, division);
}
await base.OnConnectedAsync();
}
Is there any way for a person to connect to the group if they know the other division names? Is this still a bad idea?
I'm trying to avoid keeping a list of "logged in" users and sending out the change by user individually one by one for each change.

Pushing message to groups in SignalR

Just trying to wrap my mind around handling pushing data to some users through SignalR.
Say, in a private chat situation, Person A sends a message to Person B. If I'm understanding the concept correctly, I need to create a Group for each user who logged into the system. This way, when the new message from Person A comes in, in my SignalR Hub, I'd send the message to the group for Person B which only has only one user in it i.e. Person B.
In this approach, I'd essentially create a unique Group for each user so that I have the flexibility to push data to each unique user. I could use the UserId as the Group Id.
Is this the way to handle pushing data selectively to each unique user?
You can grab client Ids manully like so and then send them using the hubcontext to avoid creating a lot of groups (you just need to implement a menthod which get you're connections from your resource in my case im using dtos)
I'm assuming your hubs have some type of connection manager, here's a sample of one from the docs
In my case I have a dynamic configuration which I use to configure my hubs using the Type of DTO, and I send hubs using a generic hub base here is a sample implementation:
Warning: things may slightly be different I'm using .Net-Core in this example
//NOTE: THub must be the one Registered with MapHubs in the Startup
//class or your connections won't be there because Hubs are not contravarient
public abstract class HubBase<TDTO, THub> : Hub
Where THub: HubBase<TDTO, THub>
{
protected IHubContext<THub> hubContext;
HubBase(IHubContext<THub> hubContext)
{
this._hubContext = hubContext;
}
protected abstract List<string> GetConnectionsByDto(TDTO dto)
protected async Task SendFilteredAsync(string eventName, TDTO dto)
{
var clientIds = await this.GetConnectionsByDto(dto);
if (false == clientIds.Any())
{
return;
}
var broadcastTasks = clientIds.Select(async clientId =>
await this._hubContext.Clients.Client(clientId).SendAsync(eventName, dto));
await Task.WhenAll(broadcastTasks);
}
}

It is possible to retrieve host address in application service in abp framework?

It is possible to have host address in app services?
For example we want send email to customer with specific link point to site address. How is this possible?
This came up via Google & the existing answer didn't really help. I don't necessarily agree that app services are the wrong domain for this; in ABP for example, a service is closely connected to a controller and services usually only exist in order to service web requests there. They often execute code in an authorised state that requires a signed-in user, so the whole thing is happening in the implicit domain context of an HTTP request/response cycle.
Accordingly - https://learn.microsoft.com/en-us/aspnet/core/fundamentals/http-context?view=aspnetcore-2.2#use-httpcontext-from-custom-components
Add services.AddHttpContextAccessor(); to something like your Startup.cs - just after wherever you already call services.AddMvc().
Use dependency injection to get hold of an IHttpContextAccessor in your service - see below.
Using constructor-based dependency injection, we add a private instance variable to store the injected reference to the context accessor and a constructor parameter where the reference is provided. The example below has a constructor with just that one parameter, but in your code you probably already have a few in there - just add another parameter and set _httpContextAccessor inside the constructor along with whatever else you're already doing.
using HttpContext = Microsoft.AspNetCore.Http.HttpContext;
using IHttpContextAccessor = Microsoft.AspNetCore.Http.IHttpContextAccessor;
// ...
public class SomeService : ApplicationService
{
private readonly IHttpContextAccessor _httpContextAccessor;
public SomeService(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
}
Now service code can read the HTTP context and from there things like the HTTP request's host and port.
public async Task<string> SomeServiceMethod()
{
HttpContext context = _httpContextAccessor.HttpContext;
string domain = context.Request.Host.Host.ToLowerInvariant();
int? port = context.Request.Host.Port;
// ...
}
HttpContext.Current.Request.Url
can get you all the info on the URL. And can break down the url into its fragments.

how can I send a message with signalr to the client?

I want to use signalR for my clint browser website so it can receive messages from the server when a new order is added. So I want it to react to a server side event that is not triggered by any browser.
There are multiple users on the website. A user should be notified when there is a new order placed for him on he server. How an I notify only a specific user, and do this from the method that adds the user?
Is there any code like:
var chat=new Chat();
chat.Send("hihi");
placed in AddOrder method, with
public class Chat : Hub
{
public void Send(string message)
{
// Call the addMessage method on all clients
Clients.refresh(message);
}
}
You can override the default client id (used to identify the users browser window) and replace with your own. Your client id would come from your membership provider.
Create a new class and implement IConnectionIdGenerator.
public class UserIdClientIdFactory : IConnectionIdGenerator
{
public string GenerateConnectionId(IRequest request)
{
return Guid.NewGuid().ToString();
}
}
The method above just creates a new Guid, but you would return the customers id from your membership provider.
You then need to register this new class with SignalR dependencyresolver, so in the Application_Start method in the global.asax file add the following line
GlobalHost.DependencyResolver.Register(typeof(IConnectionIdGenerator),
() => new UserIdClientIdFactory());
When a new order is placed you would then get the specific client and broadcast a message to them, for example:
//clientId matches the user id from you membership provider.
var clients = GlobalHost.ConnectionManager.GetHubContext().Clients;
clients[clientId].yourClientSideCallBackMethodGoesHere(someValue);
You have to store the Context.ConnectionId for all connected users, tie that to your website users and then use Clients[connectionId].addMessage(data);
One way you can do this is to hold a collection of Users (website users) each paired to a connection Id. You can then use SignalR events OnConnected / OnDisconnected to pop users in and out of this list.
E.g.
public override Task OnConnected()
{
// Add users here with Context.ConnectionId
}
public override Task OnDisconnected()
{
// Remove users from collection here by identifying them with Context.ConnectionId
}

Resources