spring security oauth2 usage with async requests - asynchronous

From what I can tell, the SpringSecurityFilter chain runs twice per request for #Async requests, because it runs on the inbound request thread, is passed to the async code which runs on a different thread, and then when it attempts to write to the response thread the SpringSecurityFilter chain runs again.
This is causing a problem near access_token expiry because I am using RemoteTokenServices, and what happens is the original request is authenticated, the service activity takes about a second, and then RemoteTokenServices is called again, at which point the access_token has expired, so the request returns a 401.
What is the recommended solution here? I have been unable to prevent the SecurityFilterChain from running the second time on the response thread. Am I doing something wrong, or is this expected behavior? I see the SecurityContext correctly passed thru to the #Async thread, but it is null in the response thread.
Is there a way to ensure the SecurityFilterChain only runs once per request? Or is the solution to accept the multiple filter calls per request and handle it with caching somehow?
I'm using spring-boot 1.3.3.RELEASE and spring-security-oauth2 2.0.9.RELEASE.
logs:
INFO [..nio-exec-1] [Caching...] loadAuthentication: 0bc97f92-9ebb-411f-9e8e-e7dc137aeffe
DEBUG [..nio-exec-1] [Caching...] Entering CachingRemoteTokenService auth: null
DEBUG [..nio-exec-1] [Audit...] AuditEvent [timestamp=Wed Mar 30 12:27:45 PDT 2016, principal=testClient, type=AUTHENTICATION_SUCCESS, data={details=remoteAddress=127.0.0.1, tokenType=BearertokenValue=<TOKEN>}]
INFO [..nio-exec-1] [Controller] Callable testing request received
DEBUG [MvcAsync1] [TaskService] taskBegin
DEBUG [MvcAsync1] [TaskService] Entering TaskService auth: org.springframework.security.oauth2.provider.OAuth2Authentication#47c78d1a: Principal: testClient; Credentials: [PROTECTED]; Authenticated: true; Details: remoteAddress=127.0.0.1, tokenType=BearertokenValue=<TOKEN>; Granted Authorities: ROLE_CLIENT
DEBUG [MvcAsync1] [TaskService] end of task
INFO [..nio-exec-2] [Caching...] loadAuthentication: 0bc97f92-9ebb-411f-9e8e-e7dc137aeffe
DEBUG [..nio-exec-2] [Caching...] Entering CachingRemoteTokenService auth: null
DEBUG [..nio-exec-2] [RemoteTokenServices] check_token returned error: invalid_token
DEBUG [..nio-exec-2] [Audit...] AuditEvent [timestamp=Wed Mar 30 12:27:47 PDT 2016, principal=access-token, type=AUTHENTICATION_FAILURE, data={type=org.springframework.security.authentication.BadCredentialsException, message=0bc97f92-9ebb-411f-9e8e-e7dc137aeffe}]
relevant code:
controller:
#RequestMapping(value = "/callable",
method = RequestMethod.GET,
produces = { MediaType.APPLICATION_JSON_VALUE })
public #ApiResponseObject Callable<ApiResponse> runCallable(HttpServletRequest httpServletRequest)
throws InterruptedException {
log.info(String.format("Callable testing request received"));
Callable<ApiResponse> rv = taskService::execute;
return rv;
}
async service:
#Override
public ApiResponse execute() {
log.debug("taskBegin");
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
log.debug("Entering TaskService auth: " + auth);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
ApiResponse rv = new ApiResponse();
rv.setStatus(HttpStatus.OK.value());
log.debug("end of task");
return rv;
}
RemoteTokenServices implementation (note caching is commented out):
public class CachingRemoteTokenService extends RemoteTokenServices {
private static Log log = LogFactory.getLog(CachingRemoteTokenService.class);
#Override
//#Cacheable(cacheNames="tokens", key="#root.methodName + #accessToken")
public OAuth2Authentication loadAuthentication(String accessToken)
throws org.springframework.security.core.AuthenticationException,
InvalidTokenException {
log.info("loadAuthentication: " + accessToken);
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
log.debug("Entering CachingRemoteTokenService auth: " + auth);
return super.loadAuthentication(accessToken);
}
#Override
//#Cacheable(cacheNames="tokens", key="#root.methodName + #accessToken")
public OAuth2AccessToken readAccessToken(String accessToken) {
log.info("readAccessToken: " + accessToken);
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
log.debug("Entering CachingRemoteTokenService auth: " + auth);
return super.readAccessToken(accessToken);
}
}
and finally my security config:
#Configuration
public class Oauth2ResourceConfig {
private static Log log = LogFactory.getLog(Oauth2ResourceConfig.class);
#Value("${client.secret}")
private String clientSecret;
#Value("${check.token.endpoint}")
private String checkTokenEndpoint;
#Bean
#Lazy
public ResourceServerTokenServices tokenService() {
CachingRemoteTokenService tokenServices = new CachingRemoteTokenService();
tokenServices.setClientId("test-service");
tokenServices.setClientSecret(clientSecret);
tokenServices.setCheckTokenEndpointUrl(checkTokenEndpoint);
return tokenServices;
}
#Configuration
#EnableResourceServer
protected static class ResourceServerConfig extends ResourceServerConfigurerAdapter {
#Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/health-check").permitAll()
.antMatchers("/**").access("#oauth2.isClient() and #oauth2.hasScope('trust')");
#Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.resourceId("test-service");
}
}
}

got an answer here: https://github.com/spring-projects/spring-security-oauth/issues/736
apparently the fix is to configure security.filter-dispatcher-types=REQUEST, ERROR

Related

SignalR core console client not receiving notifications

I have looked around at some of the Similar questions and didn't figure this problem out. I have a simple Hub in my .NET core Web API project. Here is the Hub:
public class NotificationHub : Hub<INotificationClient>
{
public async Task SendMessage(string user, string msg)
{
await Clients.All.ReceiveMessage(user, msg);
}
public Task SendMessageToCaller(string msg)
{
return Clients.Caller.ReceiveMessage(msg);
}
public Task SendMessageToPartner(string user, string msg)
{
return Clients.Client(user).ReceiveMessageToPartner(msg);
}
}
Here is the Interface:
public interface INotificationClient
{
Task ReceiveMessage(string user, string msg);
Task ReceiveMessage(string msg);
Task ReceiveMessageToPartner( string msg);
}
Here is the code from the controller:
[Route("[controller]")]
[ApiController]
public class NotificationsController : ControllerBase
{
private IHubContext<NotificationHub> _hub;
public NotificationsController(IHubContext<NotificationHub> hub)
{
_hub = hub;
}
[HttpGet]
public async Task<IActionResult> Get()
{
var msg = new NotificationData { ClientId = "12345", Notification = "Somone just connected" };
await _hub.Clients.All.SendAsync("Notification", msg);
return Ok(new { Message = "Request complete" });
}
}
Lastly here is the console client code:
Console.WriteLine("Press a key to start listening");
Console.ReadKey();
Console.WriteLine("Client Listening!");
var connection = new HubConnectionBuilder()
.WithUrl("http://localhost:61514/notifications")
.Build();
connection.On<NotificationData>("Notification", (notificationData) =>
Console.WriteLine($"Somebody connected: {notificationData.ClientId}"));
connection.StartAsync().GetAwaiter().GetResult();
Console.WriteLine("Listening. Press a key to quit");
Console.ReadKey();
Here is the startup of the web app with the mappings:
public void ConfigureServices(IServiceCollection services)
{
services.AddSignalR();
services.AddControllers();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapHub<NotificationHub>("/Notifications");
});
}
I keep getting this error: System.IO.IOException: 'The server disconnected before the handshake could be started.' I must be missing something along the way here.
Update:
Turned on the logs and got this error:
dbug: Microsoft.AspNetCore.Http.Connections.Client.HttpConnection[8]
Establishing connection with server at 'http://localhost:61514/notifications'.
dbug: Microsoft.AspNetCore.Http.Connections.Client.HttpConnection[9]
Established connection 'BdROAEEQnGUeDYAW5EspRA' with the server.
dbug: Microsoft.AspNetCore.Http.Connections.Client.HttpConnection[7]
Starting transport 'ServerSentEvents' with Url: http://localhost:61514/notifications.
info: Microsoft.AspNetCore.Http.Connections.Client.Internal.ServerSentEventsTransport[1]
Starting transport. Transfer mode: Text.
dbug: Microsoft.AspNetCore.Http.Connections.Client.Internal.ServerSentEventsTransport[3]
Starting receive loop.
dbug: Microsoft.AspNetCore.Http.Connections.Client.Internal.ServerSentEventsTransport[9]
Received 30 bytes. Parsing SSE frame.
dbug: Microsoft.AspNetCore.Http.Connections.Client.Internal.ServerSentEventsTransport[4]
Receive loop stopped.
dbug: Microsoft.AspNetCore.Http.Connections.Client.Internal.ServerSentEventsTransport[100]
Starting the send loop.
dbug: Microsoft.AspNetCore.Http.Connections.Client.HttpConnection[18]
Transport 'ServerSentEvents' started.
dbug: Microsoft.AspNetCore.Http.Connections.Client.Internal.ServerSentEventsTransport[102]
Send loop canceled.
dbug: Microsoft.AspNetCore.Http.Connections.Client.Internal.ServerSentEventsTransport[101]
Send loop stopped.
Unhandled exception. info: Microsoft.AspNetCore.Http.Connections.Client.HttpConnection[3]
HttpConnection Started.
info: Microsoft.AspNetCore.SignalR.Client.HubConnection[24]
Using HubProtocol 'json v1'.
System.IO.IOException: The server disconnected before the handshake could be started.
at Microsoft.AspNetCore.SignalR.Client.HubConnection.HandshakeAsync(ConnectionState startingConnectionState, CancellationToken cancellationToken)
at Microsoft.AspNetCore.SignalR.Client.HubConnection.StartAsyncCore(CancellationToken cancellationToken)
at Microsoft.AspNetCore.SignalR.Client.HubConnection.StartAsyncCore(CancellationToken cancellationToken)
at Microsoft.AspNetCore.SignalR.Client.HubConnection.StartAsyncInner(CancellationToken cancellationToken)
at System.Threading.Tasks.ForceAsyncAwaiter.GetResult()
at Microsoft.AspNetCore.SignalR.Client.HubConnection.StartAsync(CancellationToken cancellationToken)
at NotificationClient.Program.Main(String[] args)
So after chonking on this all day, I found out what the problem was, so I thought I'd post the solution here just in case someone else is having this problem.
The Hub Endpoint was pointing to the controller. So when the client was linking it was hitting the controller twice causing a the server to close the connection. So I changed this line:
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapHub<NotificationHub>("/Notify");
});
from "/Notifications" which was hitting the controller, to "Notify" as in the code above, re-pointed the client from Notifications to Notify
var connection = new HubConnectionBuilder()
.WithUrl("http://localhost:61514/Notify")
.ConfigureLogging(logging =>
{
logging.AddConsole();
logging.SetMinimumLevel(LogLevel.Debug);
})
.Build();
and messages started to flow in.

Spring Security form post authentication

I have a page to access with form post request (webview page for mobile apps). My application can't have a login form but I need to secure it. Mobile applications will call this webview page with authentication parameters such as (email/password). I need to call third-party api with given authentication parameters and decide it was authenticated or not. Which approach should I use for my scenarios ?
If it possible to pass authentication parameters in the Authorization header, you can enable http basic authentication in your application:
#Override
public void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.httpBasic();
}
Otherwise, you can implement your own security filter to create a UsernamePasswordAuthenticationToken (or any other class implementing Authentication) instance from your specific authentication parameters and pass to AuthenticationManager; but in the case of another class, you need to make the authentication provider below support it by overriding the public boolean supports(Class<?> authentication) method.
Then implement a custom AuthenticationProvider that will delegate authentication to the third-party API, e.g.:
public class RestAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
private static final String AUTH_URL = "http://third-party-service/authentication/basic";
private RestTemplate restTemplate;
public RestAuthenticationProvider() {
this.restTemplate = new RestTemplate();
}
#Override
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
if (authentication.getCredentials() == null) {
this.logger.debug("Authentication failed: no credentials provided");
throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
if (!authentication.getCredentials().toString().equals(userDetails.getPassword())) {
this.logger.debug("Authentication failed: invalid credentials");
throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
}
#Override
protected UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) {
String password = authentication.getCredentials().toString();
try {
ResponseEntity<String> authenticationResponse = authenticate(username, password);
if (authenticationResponse.getStatusCode().value() == 401) {
throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
return createUser(authenticationResponse.getBody());
} catch (BadCredentialsException ex) {
throw ex;
} catch (Exception ex) {
throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
}
}
private ResponseEntity<String> authenticate(String username, String password) {
HttpEntity entity = new HttpEntity(createHeaders(username, password));
return restTemplate.exchange(AUTH_URL, HttpMethod.GET, entity, String.class);
}
private HttpHeaders createHeaders(String username, String password) {
HttpHeaders headers = new HttpHeaders();
headers.set("Content-Type", "application/json");
String authorization = username + ":" + password;
String basic = Base64.getEncoder().encodeToString(authorization.getBytes());
headers.set("Authorization", "Basic " + basic);
return headers;
}
private UserDetails createUser(String json) {
return null; // TODO: Implement
}
}
And finally, make Spring Security to use your provider:
#Override
protected void configure(AuthenticationManagerBuilder auth) {
auth.authenticationProvider(new RestAuthenticationProvider())
.eraseCredentials(false);
}
I need to call third-party api with given authentication parameters
and decide it was authenticated or not
Assuming you need to use username / password to send to 3rd party, when you first login into the app, you can create a long lived token in back-end and sent it to the app to store in secure store. Later when you want to load the protected webview, send this token along with the request (in header or body via javascript) and in the server side pick the user / password corresponding to the token and authenticate with 3rd party.
This way you will never need to store password on client side and you can manually make the token at backend inactive too. If you like to go standard way, then you may take a look at Password Grant of OAuth 2 / OpenID Connect / . With the correct infrastructure in place you can get access tokens during login process and use that for your protected page. Spring Security has support for this flow - you can take a look here.

Which is the best way to validate current password?

I created a form where the user can update his data account. In this form the user is also able to change the account password, before doing so, I ask him the current password, this is the field:
<div class="form-group">
<label>Current Password</label>
<input class="form-control" id="oldPassword"
asp-for="#Model.ExistingPassword" type="password" />
<div class="invalid-feedback"></div>
</div>
as you can see the oldPassword input bound the property ExistingPassword which is part of the ViewModel of that View and have the following declaration:
[Required, MinLength(6), MaxLength(50), DataType(DataType.Password)]
public string ExistingPassword { get; set; }
when the form is submitted I call the following ajax function:
$.post(url, user, function (response) {
//Some stuff
}).done(function (response) {
alert("Updated executed");
}).fail(function (jqXHR, textStatus, errorThrown) {
alert("Error happened!");
});
the parameter of the function are taken by the form, in particular:
url: $(this).attr('action');
user: $(this).serialize();
the action of the form will call the following controller: User\UpdateUser.
Inside the UpdateUser method I execute the following check:
public async Task<UserProfileViewModel> UpdateUserAsync(UserProfileViewModel updatedUser)
{
if (!await _userManager.CheckPasswordAsync(originalUser, updatedUser.ExistingPassword))
throw new Exception("Invalid password");
essentially, the condition check if the current password is correct, if not, then an exception will raised.
Now, my question with this is: how can I know which type of exception the method has generated?
I need to know which type of exception the method UpdateUser has generated because there are different exceptions in the method.
Suppose the Invalid Password exceptions is raised, I need to display a message inside invalid-feedback div, next to oldPassword, so the user know why the update has failed.
Thanks in advance for any help.
Normally, I recommend not using an exception except in actual exception circumstances, but given the way you've designed this, you have a few options.
I'd suggest creating a custom "UpdateUserException" that you can throw that will include additional information, which can be provided by an enum or just string.
public class UpdateUserException : Exception {
public UpdateUserError ErrorCondition;
public UpdateUserException(UpdateUserError error, string message)
{
ErrorCondition = error;
Message = message;
}
}
then you would throw it
throw new UpdateUserException(UpdateUserError.BadPassword, "Invalid Password");
then you would catch it
try {}
catch (UpdateUserException e)
{
if (e.ErrorCondition == UpdateUserException.BadPassword)
{
// handle your exception.
}
}
Have a look at the UserManager ChangePassword Method.
You can bind the UserManager to use DependencyInjection like this (in Startup.cs)
public async void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
...
app.UseAuthentication();
app.UseMvc();
var scopeFactory = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>();
using (var scope = scopeFactory.CreateScope())
{
UserManager<User> userManager = scope.ServiceProvider.GetRequiredService<UserManager<User>>();
}
}
And then in your Controller s Constructor
private readonly UserManager<User> _userManager;
public AccountController(UserManager<User> userManager)
{
_userManager = userManager;
}
And finally your endpoint:
[HttpPost("ChangePassword")]
public async Task<IActionResult> ChangePassword([FromBody]ChangePasswordRequest changePasswordParams)
{
if (changePasswordParams == null)
return BadRequest($"{nameof(changePasswordParams)} must not be null!");
if (string.IsNullOrWhiteSpace(changePasswordParams.OldPassword) || string.IsNullOrWhiteSpace(changePasswordParams.NewPassword))
return BadRequest("old and new passwords have to be provided, but they both are empty.");
var userId = User.Claims.FirstOrDefault(c => c.Type == "id")?.Value;
var user = await _userManager.FindByIdAsync(userId);
var result = await _userManager.ChangePasswordAsync(user, changePasswordParams.OldPassword, changePasswordParams.NewPassword);
if (result.Succeeded)
return NoContent();
return BadRequest(result.Errors);
}
after that you can handle the errors in a switch statement.
Using Exceptions for handled errors are not recommended since they generally ends up with Internal Server error and actually It is beyond of its purpose.
The best approach would be to send BadRequest as It is stated by #maerlin.
However, If you insist to use Exceptions in your application or your applciation is architected to work in this way. I suggest you to inherit new CustomApplcationException class from ApplicationException and then inherit UpdateUserException and vs. from CustomApplicationException class. After that, I Suggest you to handle your exceptions in ErrorHandlingMiddleware and return HandledExceptions at least with BadRequest (400) status code.
The Example Code would be
public class ExceptionMiddleware
{
private readonly RequestDelegate _next;
private readonly ILoggerManager _logger;
public ExceptionMiddleware(RequestDelegate next, ILoggerManager logger)
{
_logger = logger;
_next = next;
}
public async Task InvokeAsync(HttpContext httpContext)
{
try
{
await _next(httpContext);
}
catch (CustomApplicationException cae)
{
await HandleCustomExceptionAsync(httpContext, cae);
}
catch (Exception ex)
{
_logger.LogError($"Something went wrong: {ex}");
await HandleExceptionAsync(httpContext, ex);
}
}
private static Task HandleExceptionAsync(HttpContext context, Exception exception)
{
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
return context.Response.WriteAsync(new ErrorDetails()
{
StatusCode = context.Response.StatusCode,
Message = "Internal Server Error from the custom middleware."
}.ToString());
}
private static Task HandleCustomExceptionAsync(HttpContext context, Exception exception)
{
context.Response.StatusCode = 400;
return context.Response.WriteAsync(new ErrorDetails()
{
StatusCode = context.Response.StatusCode,
Message = exception.Message
}.ToString());
}
}
then you need to regiter middleware in your Startup.cs
app.UseMiddleware<ExceptionMiddleware>();
please see https://code-maze.com/global-error-handling-aspnetcore/ and http://www.talkingdotnet.com/global-exception-handling-in-aspnet-core-webapi/ for further details.

Spring Integration tcp client multiple connections

I use Spring Integration tcp-outbound-adapter and tcp-inbound-adapter in order to communicate with a third party external system through TCP.
The connection factory I use is of type "client" and has single-use="false", because the nature of communication with the external system is a session of several dozens requests and replies.
The external system expects I will open a new TCP connection for each session.
Is there any way to do that with Spring Integration?
My code uses SI successfully for one such session. But I want my system to open several such connections so I can handle several concurrent sessions.
Currently, if I send a message of a new session to the inbound adapter, it uses the same TCP connection.
Please help.
UPDATE:
While using the ThreadAffinity solution given by Gary here, we get this exception when we do more than 4 concurrent requests. Any idea why is that?
11:08:02.083 [pool-1-thread-2] 193.xxx.yyy.zz:443:55729:46c71372-5933-4707-a27b-93cc4bf78c59 Message sent GenericMessage [payload=byte[326], headers={replyChannel=org.springframework.messaging.core.GenericMessagingTemplate$TemporaryReplyChannel#2fb866, errorChannel=org.springframework.messaging.core.GenericMessagingTemplate$TemporaryReplyChannel#2fb866, ip_tcp_remotePort=55718, ip_connectionId=127.0.0.1:55718:4444:7f71ce96-eaac-4b21-8b2c-bf736102f818, ip_localInetAddress=/127.0.0.1, ip_address=127.0.0.1, id=2dc3e330-d703-8a61-c46c-012233cadf6f, ip_hostname=127.0.0.1, timestamp=1481706480700}]
11:08:12.093 [pool-1-thread-2] Remote Timeout on 193.xxx.yyy.zz:443:55729:46c71372-5933-4707-a27b-93cc4bf78c59
11:08:12.093 [pool-1-thread-2] Tcp Gateway exception
org.springframework.integration.MessageTimeoutException: Timed out waiting for response
at org.springframework.integration.ip.tcp.TcpOutboundGateway.handleRequestMessage(TcpOutboundGateway.java:146)
at org.springframework.integration.handler.AbstractReplyProducingMessageHandler.handleMessageInternal(AbstractReplyProducingMessageHandler.java:109)
at org.springframework.integration.handler.AbstractMessageHandler.handleMessage(AbstractMessageHandler.java:127)
at org.springframework.integration.dispatcher.AbstractDispatcher.tryOptimizedDispatch(AbstractDispatcher.java:116)
at org.springframework.integration.dispatcher.UnicastingDispatcher.doDispatch(UnicastingDispatcher.java:148)
at org.springframework.integration.dispatcher.UnicastingDispatcher.dispatch(UnicastingDispatcher.java:121)
at org.springframework.integration.channel.AbstractSubscribableChannel.doSend(AbstractSubscribableChannel.java:77)
at org.springframework.integration.channel.AbstractMessageChannel.send(AbstractMessageChannel.java:423)
at org.springframework.integration.channel.AbstractMessageChannel.send(AbstractMessageChannel.java:373)
at org.springframework.messaging.core.GenericMessagingTemplate.doSend(GenericMessagingTemplate.java:115)
at org.springframework.messaging.core.GenericMessagingTemplate.doSend(GenericMessagingTemplate.java:45)
at org.springframework.messaging.core.AbstractMessageSendingTemplate.send(AbstractMessageSendingTemplate.java:105)
at org.springframework.integration.handler.AbstractMessageProducingHandler.sendOutput(AbstractMessageProducingHandler.java:292)
at org.springframework.integration.handler.AbstractMessageProducingHandler.produceOutput(AbstractMessageProducingHandler.java:212)
at org.springframework.integration.handler.AbstractMessageProducingHandler.sendOutputs(AbstractMessageProducingHandler.java:129)
at org.springframework.integration.handler.AbstractReplyProducingMessageHandler.handleMessageInternal(AbstractReplyProducingMessageHandler.java:115)
at org.springframework.integration.handler.AbstractMessageHandler.handleMessage(AbstractMessageHandler.java:127)
at org.springframework.integration.dispatcher.AbstractDispatcher.tryOptimizedDispatch(AbstractDispatcher.java:116)
at org.springframework.integration.dispatcher.UnicastingDispatcher.doDispatch(UnicastingDispatcher.java:148)
at org.springframework.integration.dispatcher.UnicastingDispatcher.dispatch(UnicastingDispatcher.java:121)
at org.springframework.integration.channel.AbstractSubscribableChannel.doSend(AbstractSubscribableChannel.java:77)
at org.springframework.integration.channel.AbstractMessageChannel.send(AbstractMessageChannel.java:423)
at org.springframework.messaging.core.GenericMessagingTemplate.doSend(GenericMessagingTemplate.java:115)
at org.springframework.messaging.core.GenericMessagingTemplate.doSendAndReceive(GenericMessagingTemplate.java:150)
at org.springframework.messaging.core.GenericMessagingTemplate.doSendAndReceive(GenericMessagingTemplate.java:45)
at org.springframework.messaging.core.AbstractMessagingTemplate.sendAndReceive(AbstractMessagingTemplate.java:42)
at org.springframework.integration.core.MessagingTemplate.sendAndReceive(MessagingTemplate.java:97)
at org.springframework.integration.gateway.MessagingGatewaySupport.doSendAndReceive(MessagingGatewaySupport.java:441)
at org.springframework.integration.gateway.MessagingGatewaySupport.sendAndReceiveMessage(MessagingGatewaySupport.java:409)
at org.springframework.integration.ip.tcp.TcpInboundGateway.doOnMessage(TcpInboundGateway.java:120)
at org.springframework.integration.ip.tcp.TcpInboundGateway.onMessage(TcpInboundGateway.java:98)
at org.springframework.integration.ip.tcp.connection.TcpConnectionInterceptorSupport.onMessage(TcpConnectionInterceptorSupport.java:159)
at org.springframework.integration.ip.tcp.connection.TcpNetConnection.run(TcpNetConnection.java:182)
at org.springframework.integration.ip.tcp.connection.TcpConnectionInterceptorSupport.run(TcpConnectionInterceptorSupport.java:111)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at java.lang.Thread.run(Thread.java:745)
It depends on what constitutes a "session" - if all the requests from a session on the client side all run on a single thread, you could write a simple wrapper for the connection factory that stores the connection in a ThreadLocal. You would need some mechanism to call the factory wrapper after the last request to close the connection and remove it from the ThreadLocal.
If the requests for a session can occur on multiple threads, it would be a bit more complicated but you could still do it with a ThreadLocal that maps to a connection instance.
EDIT
Here's an example...
#SpringBootApplication
public class So40507731Application {
public static void main(String[] args) throws Exception {
ConfigurableApplicationContext context = SpringApplication.run(So40507731Application.class, args);
MessageChannel channel = context.getBean("clientFlow.input", MessageChannel.class);
MessagingTemplate template = new MessagingTemplate(channel);
ThreadAffinityClientConnectionFactory affinityCF = context.getBean(ThreadAffinityClientConnectionFactory.class);
ExecutorService exec = Executors.newCachedThreadPool();
CountDownLatch latch = new CountDownLatch(2);
exec.execute(() -> {
String result = new String(template.convertSendAndReceive("foo", byte[].class));
System.out.println(Thread.currentThread().getName() + " " + result);
result = new String(template.convertSendAndReceive("foo", byte[].class));
System.out.println(Thread.currentThread().getName() + " " + result);
affinityCF.release();
latch.countDown();
});
exec.execute(() -> {
String result = new String(template.convertSendAndReceive("foo", byte[].class));
System.out.println(Thread.currentThread().getName() + " " + result);
result = new String(template.convertSendAndReceive("foo", byte[].class));
System.out.println(Thread.currentThread().getName() + " " + result);
affinityCF.release();
latch.countDown();
});
latch.await(10, TimeUnit.SECONDS);
context.close();
exec.shutdownNow();
}
#Bean
public TcpNetClientConnectionFactory delegateCF() {
TcpNetClientConnectionFactory clientCF = new TcpNetClientConnectionFactory("localhost", 1234);
clientCF.setSingleUse(true); // so each thread gets his own connection
return clientCF;
}
#Bean
public ThreadAffinityClientConnectionFactory affinityCF() {
return new ThreadAffinityClientConnectionFactory(delegateCF());
}
#Bean
public TcpOutboundGateway outGate() {
TcpOutboundGateway outGate = new TcpOutboundGateway();
outGate.setConnectionFactory(affinityCF());
return outGate;
}
#Bean
public IntegrationFlow clientFlow() {
return f -> f.handle(outGate());
}
#Bean
public TcpNetServerConnectionFactory serverCF() {
return new TcpNetServerConnectionFactory(1234);
}
#Bean
public TcpInboundGateway inGate() {
TcpInboundGateway inGate = new TcpInboundGateway();
inGate.setConnectionFactory(serverCF());
return inGate;
}
#Bean
public IntegrationFlow serverFlow() {
return IntegrationFlows.from(inGate())
.transform(Transformers.objectToString())
.transform("headers['ip_connectionId'] + ' ' + payload")
.get();
}
public static class ThreadAffinityClientConnectionFactory extends AbstractClientConnectionFactory
implements TcpListener {
private final AbstractClientConnectionFactory delegate;
private final ThreadLocal<TcpConnectionSupport> connection = new ThreadLocal<>();
public ThreadAffinityClientConnectionFactory(AbstractClientConnectionFactory delegate) {
super("", 0);
delegate.registerListener(this);
this.delegate = delegate;
}
#Override
protected TcpConnectionSupport obtainConnection() throws Exception {
TcpConnectionSupport tcpConnection = this.connection.get();
if (tcpConnection == null || !tcpConnection.isOpen()) {
tcpConnection = this.delegate.getConnection();
this.connection.set(tcpConnection);
}
return tcpConnection;
}
public void release() {
TcpConnectionSupport connection = this.connection.get();
if (connection != null) {
connection.close();
this.connection.remove();
}
}
#Override
public void start() {
this.delegate.start();
setActive(true);
super.start();
}
#Override
public void stop() {
this.delegate.stop();
setActive(false);
super.stop();
}
#Override
public boolean onMessage(Message<?> message) {
return getListener().onMessage(message);
}
}
}
Result:
pool-2-thread-2 localhost:64559:1234:3d898822-ea91-421d-97f2-5f9620b9d369 foo
pool-2-thread-1 localhost:64560:1234:227f8a9f-1461-41bf-943c-68a56f708b0c foo
pool-2-thread-2 localhost:64559:1234:3d898822-ea91-421d-97f2-5f9620b9d369 foo
pool-2-thread-1 localhost:64560:1234:227f8a9f-1461-41bf-943c-68a56f708b0c foo

How to handle timeout of AsynchronousResponse object in RestEasy

i am implementing a jax-rs service with RestEasy on JBoss AS 7.1.2 an i would like to use asynchronous HTTP processsing as described here: http://docs.jboss.org/resteasy/docs/1.0.0.GA/userguide/html/Asynchronous_HTTP_Request_Processing.html
For thr AsynchronousResponse I define a timeout of 10 seconds. When this period expires, the request is responded with a 200 OK and an empty body. I would like to modify this behaviour so i need to be notified about the timeout event.
In my solution, I would like to handle the timeout event in a NotificationManager object, which keeps the AsycnhronousResponse for the time being. Please see the code below for details.
So far, i could not figure out how to do that. Does anyone have more experience with the RestEasy Asynchronous HTTP processing?
#POST
#Path("/blabla")
#Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})
#Consumes({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})
public void subscribeLongPolling (
final #Suspend(10000) AsynchronousResponse response,
JAXBElement<LongPollingRequestParameters> rqParam,
#Context HttpServletRequest req) throws Exception {
//do some stuff with req
Thread t = new Thread("ThreadSubscribeTo:" + channelID)
{
#Override
public void run() {
//hand over to Notification Manager to return notifications in case some exist
try {
NotificationManager nm = new NotificationManager();
nm.setAsyncResponseObject(response);
logger.info("Response object registered in NotificationManager");
}catch (Exception e){
e.printStackTrace();
}
}
};
t.start();
logger.info("Releasing Thread");
}
public class NotificationManager {
private AsynchronousResponse response;
private NotificationList nList;
public synchronized void setAsyncResponseObject(AsynchronousResponse response) {
this.response = response;
if (nList.getAny().size() > 0) {
logger.info("Stored notification send to web client: " + nList.getAny().get(0).toString());
sendNotification(nList.getAny().remove(0));
}
}
public synchronized void sendNotification(Object message){
if (response != null){
logger.info("Response object found. Send notification immediately: " + message.toString());
Response responseObject = Response.ok(message, MediaType.APPLICATION_XML).build();
response.setResponse(responseObject);
response = null;
}else{
logger.info("Response object not found notification will be stored");
addNotification(message);
}
}
}
Thanks in advance,
Alex

Resources