I am trying to set timeout and SSL (https) for WS call:
PS: No need to mark this as duplicated, the only similar question has never been answered.
I tried HttpsUrlConnectionMessageSender that adds support for (self-signed) HTTPS certificates but it does support timeout.
when I switch to HttpComponentsMessageSender that supports timeout (Connection and read timeouts) it does support SSL.
I want to combile timeout and ssl to when calling WS:
webServiceTemplate.setDefaultUri(uri);
response = webServiceTemplate.marshalSendAndReceive(inputs, new SoapHandler(createCredentials(), soapAction));
Finally, did it using HttpComponentsMessageSender. Here is my code:
HttpComponentsMessageSender messageSender = new HttpComponentsMessageSender();
HttpClient httpClient = HttpClientFactory.getHttpsClient(sslUtils, timeout);
messageSender.setHttpClient(httpClient);
webServiceTemplate.setMessageSender(messageSender);
I also created a new factory class HttpClientFactory that sets the SSL and timeout:
import java.io.IOException;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import org.apache.http.HttpException;
import org.apache.http.HttpRequest;
import org.apache.http.HttpRequestInterceptor;
import org.apache.http.client.HttpClient;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLContextBuilder;
import org.apache.http.conn.ssl.SSLContexts;
import org.apache.http.conn.ssl.TrustSelfSignedStrategy;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.protocol.HTTP;
import org.apache.http.protocol.HttpContext;
public class HttpClientFactory {
private static CloseableHttpClient client;
private HttpClientFactory() {
}
public static HttpClient getHttpsClient(SslUtils sslUtils, int timeout) throws Exception {
if (client != null) {
return client;
}
SSLContext sslcontext = getSSLContext(sslUtils);
SSLConnectionSocketFactory factory = new SSLConnectionSocketFactory(sslcontext, new HostnameVerifier() {
#Override
public boolean verify(String hostname, SSLSession session) {
return true;
}
});
HttpClientBuilder httpClientBuilder = HttpClients.custom();
httpClientBuilder.addInterceptorFirst(new ContentLengthHeaderRemover());
RequestConfig config = RequestConfig.custom()
.setConnectTimeout(timeout)
.setConnectionRequestTimeout(timeout)
.setSocketTimeout(timeout)
.build();
return httpClientBuilder.setSSLSocketFactory(factory)
.setDefaultRequestConfig(config)
.build();
}
private static class ContentLengthHeaderRemover implements HttpRequestInterceptor {
#Override
public void process(HttpRequest request, HttpContext context) throws HttpException, IOException {
request.removeHeaders(HTTP.CONTENT_LEN);
}
}
public static void releaseInstance() {
client = null;
}
private static SSLContext getSSLContext(SslUtils sslUtils) throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException, KeyManagementException {
KeyStore ks = KeyStore.getInstance("JKS");
ks.load(sslUtils.getKeystore().getInputStream(), sslUtils.getKeyPwd().toCharArray());
sslUtils.getKeystore().getInputStream().close();
KeyStore ts = KeyStore.getInstance("JKS");
ts.load(sslUtils.getTrustStore().getInputStream(), sslUtils.getTrustPwd().toCharArray());
sslUtils.getTrustStore().getInputStream().close();
SSLContextBuilder sslContextBuilder = SSLContexts.custom();
try {
sslContextBuilder = SSLContexts.custom().loadKeyMaterial(ks, ssl.getKeyPwd().toCharArray());
} catch (UnrecoverableKeyException e) {
e.printStack();
}
sslContextBuilder.loadTrustMaterial(ts, new TrustSelfSignedStrategy());
return sslContextBuilder.build();
}
}
For information the SslUtils is just a bean class that holds the keystore and truststore informations' :
public class SslUtils {
private Resource keystore;
private String keyPwd;
private Resource trustStore;
private String trustPwd;
// Getters and Setters
}
This works for me and let me use both SSL and timeout at the same. I hope this will help others.
In a case of HTTPS protocol with basic authentication, you may not need a certificate, you can set the encoded username:password into the header of the request
package com.james.medici.app.ws;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.oxm.jaxb.Jaxb2Marshaller;
import org.springframework.ws.client.core.WebServiceTemplate;
import org.springframework.ws.transport.http.HttpUrlConnectionMessageSender;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.util.Base64;
#Slf4j
#Configuration
public class SoapClientConfiguration {
#Value("${james.medici.url}")
private String defaultUri;
#Value("${james.medici.username}")
private String userName;
#Value("${james.medici.passcode}")
private String userPassword;
public static final String SEPARATOR = ":";
public static final String AUTHORIZATION = "Authorization";
public static final String BASIC = "Basic ";
class CustomHttpUrlConnectionMessageSender extends HttpUrlConnectionMessageSender {
#Override
protected void prepareConnection(HttpURLConnection connection) throws IOException {
Base64.Encoder enc = Base64.getEncoder();
String userpassword = StringUtils.joinWith(SEPARATOR, userName, userPassword);
String encodedAuthorization = enc.encodeToString(userpassword.getBytes());
connection.setRequestProperty(AUTHORIZATION, BASIC + encodedAuthorization);
super.prepareConnection(connection);
}
}
#Bean
public Jaxb2Marshaller marshaller() {
Jaxb2Marshaller marshaller = new Jaxb2Marshaller();
marshaller.setContextPath("com.james.medici.app.ws.model");
return marshaller;
}
#Bean
public WebServiceTemplate webServiceTemplate() {
log.info(defaultUri);
WebServiceTemplate webServiceTemplate = new WebServiceTemplate();
webServiceTemplate.setMarshaller(marshaller());
webServiceTemplate.setUnmarshaller(marshaller());
webServiceTemplate.setDefaultUri(defaultUri);
webServiceTemplate.setMessageSender(new CustomHttpUrlConnectionMessageSender());
return webServiceTemplate;
}
}
Related
My Application is a Spring Webflux application with Spring boot version 2.6.6.
Since, I have a chat and notification requirement for the logged in user, trying to use RSocket over websocket for notification & messaging along with Webflux for web based application.
Using Spring security for my web application with the config below and it is working. Now, not sure if I will be able to use the same security for RSocket as RSocket over websocket will be established when the user is logged in.
My Webflux security,
/**
*
*/
package com.TestApp.service.admin.spring.security;
import static java.util.stream.Collectors.toList;
import static org.springframework.security.config.Customizer.withDefaults;
import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.security.reactive.PathRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.messaging.rsocket.RSocketStrategies;
import org.springframework.messaging.rsocket.annotation.support.RSocketMessageHandler;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.config.annotation.rsocket.EnableRSocketSecurity;
import org.springframework.security.config.annotation.rsocket.RSocketSecurity;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.SecurityWebFiltersOrder;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.messaging.handler.invocation.reactive.AuthenticationPrincipalArgumentResolver;
import org.springframework.security.rsocket.core.PayloadSocketAcceptorInterceptor;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.authentication.ServerAuthenticationFailureHandler;
import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler;
import org.springframework.security.web.server.authentication.logout.LogoutWebFilter;
import org.springframework.security.web.server.authentication.logout.RedirectServerLogoutSuccessHandler;
import org.springframework.security.web.server.authentication.logout.ServerLogoutHandler;
import org.springframework.security.web.server.authentication.logout.ServerLogoutSuccessHandler;
import org.springframework.security.web.server.authorization.HttpStatusServerAccessDeniedHandler;
import org.springframework.security.web.server.context.ServerSecurityContextRepository;
import org.springframework.security.web.server.csrf.CookieServerCsrfTokenRepository;
import org.springframework.security.web.server.util.matcher.AndServerWebExchangeMatcher;
import org.springframework.security.web.server.util.matcher.OrServerWebExchangeMatcher;
import org.springframework.security.web.server.util.matcher.PathPatternParserServerWebExchangeMatcher;
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher.MatchResult;
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers;
import org.springframework.web.server.WebSession;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.testapp.service.admin.spring.TestAppProperties;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
#EnableWebFluxSecurity
public class AdminSecurityConfig {
private static final Logger LOGGER = LoggerFactory.getLogger(AdminSecurityConfig.class);
private static final String[] DEFAULT_FILTER_MAPPING = new String[] { "/**" };
private static final String authenticateHeaderValue = "TestApp";
private static final String unauthorizedJsonBody = "{\"message\": \"You are not authorized\"}";
#Autowired
private TestAppProperties testAppProps;
#Bean
public SecurityWebFilterChain securitygWebFilterChain(final ServerHttpSecurity http,
final ReactiveAuthenticationManager authManager,
final ServerSecurityContextRepository securityContextRepository,
final TestAppAuthenticationFailureHandler failureHandler,
final ObjectProvider<TestAppLogoutHandler> availableLogoutHandlers) {
http.securityContextRepository(securityContextRepository);
return http.authorizeExchange().matchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
.pathMatchers(TestAppProps.getSecurity().getIgnorePatterns()).permitAll()
.anyExchange().authenticated().and().formLogin().loginPage(TestAppProps.getSecurity().getLoginPath())
.authenticationSuccessHandler(authSuccessHandler()).and().exceptionHandling()
.authenticationEntryPoint((exchange, exception) -> Mono.error(exception))
.accessDeniedHandler(new HttpStatusServerAccessDeniedHandler(HttpStatus.UNAUTHORIZED)).and().build();
}
#Bean
public ServerAuthenticationSuccessHandler authSuccessHandler() {
return new TestAppAuthSuccessHandler("/");
}
#Bean
public ServerLogoutSuccessHandler logoutSuccessHandler(String uri) {
RedirectServerLogoutSuccessHandler successHandler = new RedirectServerLogoutSuccessHandler();
successHandler.setLogoutSuccessUrl(URI.create(uri));
return successHandler;
}
#Bean(name = "failure-handler-bean")
public TestAppAuthenticationFailureHandler defaultFailureHandler() {
try {
new ObjectMapper().reader().readTree(unauthorizedJsonBody);
} catch (final IOException e) {
throw new IllegalArgumentException("'unauthorizedJsonBody' property is not valid JSON.", e);
}
return new TestAppAdminAuthFailureHandler(authenticateHeaderValue, unauthorizedJsonBody);
}
#Bean
public AuthenticatedPrinciplaProvider TestAppSecurityPrincipalProvider() {
return new TestAppSecurityContextPrincipleProvider();
}
}
public class TestAppSecurityContextPrincipleProvider implements AuthenticatedPrinciplaProvider {
#Override
public Mono<WhskrUserDetails> retrieveUser() {
return principalMono.flatMap(principal -> {
if (principal instanceof UsernamePasswordAuthenticationToken) {
final TestAppUserDetails user = (TestAppUserDetails) ((UsernamePasswordAuthenticationToken) principal)
.getPrincipal();
LOGGER.debug("User principal found for ID {} Org {} ", user.getUserId(), user.getOrgId());
return Mono.just(user);
}
return Mono.error(() -> new IllegalArgumentException(NO_USER_AUTH_ERROR));
})
}
}
This is working as expected. Have a login page and user gets redirected to the home page after the successful login.
Now, I am adding RSocket over websocket for messaging and notification for the logged in user.
implementation 'org.springframework.boot:spring-boot-starter-webflux'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.security:spring-security-messaging'
implementation 'org.springframework.security:spring-security-rsocket'
implementation 'org.springframework.boot:spring-boot-starter-rsocket'
RSocketSecurityConfig,
#EnableWebFluxSecurity
public class AdminRSocketSecurityConfig {
private static final Logger LOGGER = LoggerFactory.getLogger(AdminRSocketSecurityConfig.class);
private static final String[] DEFAULT_FILTER_MAPPING = new String[] { "/**" };
private static final String authenticateHeaderValue = "TestApp";
private static final String unauthorizedJsonBody = "{\"message\": \"You are not authorized\"}";
#Autowired
private TestAppProperties TestAppProps;
#Autowired
private AuthenticatedPrinciplaProvider secContext;
static final String RSOCKET_CONVERTER_BEAN_NAME = "RSocketAuthConverter";
private static final String HEADERS = "headers";
private static final MimeType COMPOSITE_METADATA_MIME_TYPE = MimeTypeUtils
.parseMimeType(WellKnownMimeType.MESSAGE_RSOCKET_COMPOSITE_METADATA.getString());
private static final MimeType APPLICATION_JSON_MIME_TYPE = MimeTypeUtils
.parseMimeType(WellKnownMimeType.APPLICATION_JSON.getString());
#Bean
public RSocketStrategies rsocketStrategies() {
return RSocketStrategies.builder()
.encoders(encoders -> encoders.add(new Jackson2CborEncoder()))
.decoders(decoders -> decoders.add(new Jackson2CborDecoder()))
.routeMatcher(new PathPatternRouteMatcher())
.build();
}
#Bean
public RSocketMessageHandler messageHandler(RSocketStrategies strategies) {
RSocketMessageHandler handler = new RSocketMessageHandler();
HandlerMethodArgumentResolver resolver = new AuthenticationPrincipalArgumentResolver();
handler.getArgumentResolverConfigurer().addCustomResolver(resolver);
handler.setRSocketStrategies(strategies);
return handler;
}
#Bean
public PayloadSocketAcceptorInterceptor authorization(final ReactiveAuthenticationManager authManager,
final RSocketSecurity security) {
security.authorizePayload(authorize -> authorize.setup().authenticated()).authenticationManager(authManager);
return security.build();
}
}
RSocketController,
#Controller
public class RSocketController {
private static final Logger LOGGER = LoggerFactory.getLogger(RSocketController.class);
private static final Map<Integer, Map<Integer, RSocketRequester>> CLIENT_REQUESTER_MAP = new HashMap<>();
static final String SERVER = "Server";
static final String RESPONSE = "Response";
static final String STREAM = "Stream";
static final String CHANNEL = "Channel";
#Autowired
private AuthenticatedPrinciplaProvider secContext;
#ConnectMapping
// void onConnect(RSocketRequester rSocketRequester, #Payload Integer userId) {
void onConnect(RSocketRequester rSocketRequester) {
secContext.retrieveUser().flatMap(usr -> {
LOGGER.info("Client connect request for userId {} ", usr.getUserId());
rSocketRequester.rsocket().onClose().doFirst(() -> {
CLIENT_REQUESTER_MAP.put(usr.getUserId(), rSocketRequester);
}).doOnError(error -> {
LOGGER.info("Client connect request for userId {} ", usr.getUserId());
}).doFinally(consumer -> {
LOGGER.info("Removing here for userId {} ", usr.getUserId());
if (CLIENT_REQUESTER_MAP.get(usr.getBranchId()) != null) {
CLIENT_REQUESTER_MAP.remove(usr.getUserId(), rSocketRequester);
}
}).subscribe();
return Mono.empty();
}).subscribe();
}
}
From the RSocket over WebSocket client, the call is not going to the controller as auth is failing.
But, When I set "authorize.setup().permitAll()" in my RSocketSecurityConfig authorization(), the call goes to the controller, but the retrieveUser() fails.
I am not sure, How can I use the same security which is being used for my web based application for RSocket security as well?
So, When user is not logged in to my web app, the rsocket over websocket should fail and it should work only when the user is logged in. The RSocket initial call is happening once the user is logged in.
I am unable to moock webclient
WebClientConfig.java
public #Bean("oauthWebClient") WebClient oauthWebClient() {
return WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(
HttpClient.create().option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 1000)
.doOnConnected(connection -> connection.addHandlerLast(new ReadTimeoutHandler(10))
.addHandlerLast(new WriteTimeoutHandler(10)))))
.filter(logOAuthResponse()).baseUrl(oauthTokenUrl)
.defaultHeaders(headers -> {
headers.setBasicAuth(secretHeaderValue);
headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE);
})
.defaultUriVariables(oauthUriVariables)
.build();
}
serviceUti.java
package com.test.stats.volts.busops.utils;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.retry.annotation.Recover;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.WebClientRequestException;
import org.springframework.web.reactive.function.client.WebClientResponseException;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.test.stats.volts.busops.exception.VoltsDataAccessException;
import com.test.stats.volts.busops.exception.MtnDataAccessNotRespondingException;
import com.test.stats.volts.busops.exception.VoltsDataAcessException;
import com.test.stats.volts.busops.exception.UnableToGetOAuthTokenException;
#Component("voltsDataAccessServiceUtil")
public class ServiceUtil {
private static final Logger logger = LoggerFactory.getLogger(ServiceUtil.class);
#Autowired
#Qualifier("voltsDataAccessWebClient")
WebClient voltsDataAccessClient;
#Autowired
#Qualifier("oauthWebClient")
WebClient oauthClient;
#Value("${volts.dataaccess.end.point.save-meeting}")
private String saveMeetingEndPoint;
#Value("${volts.dataaccess.oauth.header-key.name}")
private String oauthHeaderKeyName;
#Value("${volts.dataccess.oauth.response.token.key-name}")
private String oauthResponseTokenKeyName;
#Value("#{${volts.dataaccess.api.endpoints.default.uri-variables}}")
private Map<String, String> voltsDataAccessUriVariables;
#Autowired
ObjectMapper mapper;
#Value("#{${volts.dataaccess.oauth.uri-variables}}")
private Map<String, String> oauthUriVariables;
#Value("${volts.dataaccess.oauth.scope}")
private String oauthScope;
#Value("${volts.dataaccess.oauth.grant_type}")
private String grantType;
#Value("${volts.dataaccess.oauth.secret.header-name}")
private String secretHeaderName;
#Value("${volts.dataaccess.oauth.secret.header-value}")
private String secretHeaderValue;
#Value("${volts.dataaccess.end.point.fetch-meeting}")
private String fetchMeetingEndPoint;
#Value("${volts.dataaccess.query.param.api-key-value}")
private String apiKey;
#Retryable(maxAttempts = 2)
public String postOAuthToken() throws VoltsDataAccessException {
try {
String respEntity = oauthClient.post()
.uri(uriBuilder -> uriBuilder.queryParam("scope", oauthScope).queryParam("grant_type", grantType)
.build())
.headers(headers -> headers.setBasicAuth(secretHeaderValue))
.retrieve()
.bodyToMono(String.class)
.block();
if (respEntity == null)
throw new UnableToGetOAuthTokenException(
"Exception while fetching OAUthToken as response from OAUth service is null resposne body");
try {
return mapper.readValue(respEntity, ObjectNode.class).get(oauthResponseTokenKeyName).asText();
} catch (JsonProcessingException e) {
logger.error("Exception while pasring token from Oauth ", e);
throw new UnableToGetOAuthTokenException(
"Exception while fetching OAUthToken as response from OAUth service is null resposne body");
}
} catch (WebClientRequestException e) {
logger.error("Request Exception to OAUth: message {}", e.getMessage(), e);
logger.error("", e);
throw new VoltsDataAccessException(e.getMessage(), HttpStatus.REQUEST_TIMEOUT.value());
}
catch (WebClientResponseException e) {
throw new VoltsDataAccessException(e.getMessage(), e.getRawStatusCode());
}
}
#Retryable(value = VoltsDataAcessException.class)
public ResponseEntity<VoltsResp> postSaveMeetingDetails(SaveVoltsReq voltsReq)
throws VoltsDataAcessException {
try {
ResponseEntity<VoltsResp> resp = voltsDataAccessClient.post()
.uri(saveMeetingEndPoint, uri -> uri.queryParam("apikey", apiKey).build())
.headers(headers -> headers.setBearerAuth(postOAuthToken()))
.contentType(MediaType.APPLICATION_JSON).bodyValue(voltsReq).retrieve()
.toEntity(VoltsResp.class).block();
return resp;
} catch (WebClientRequestException e) {
throw new VoltsDataAcessException(e.getMessage(), HttpStatus.REQUEST_TIMEOUT.value());
} catch (WebClientResponseException e) {
throw new VoltsDataAcessException(e.getMessage(), e.getRawStatusCode());
}
}
}
My test classs
WebClienrTests.java
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.web.reactive.function.client.WebClient;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.test.stats.volts.busops.config.WebClientConfig;
import com.test.stats.volts.busops.utils.ServiceUtil;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
#SpringBootTest(classes = { ServiceUtil.class, WebClientConfig.class,
ObjectMapper.class })
#RunWith(SpringRunner.class)
class ServiceUtilTests {
public static MockWebServer mockServer = new MockWebServer();
#MockBean
WebClientConfig webClientCondfig;
#Autowired
ServiceUtil serviceUtil;
#MockBean(name = "oauthWebClient")
WebClient oauthClient;
#MockBean(name = "voltsDataAccessWebClient")
WebClient voltsDataAccessWebClient;
#BeforeEach
void startMockServer() throws Exception {
mockServer.start(9090);
oauthClient = WebClient.create(mockServer.url("/").url().toString());
}
#AfterEach
void stopMockServer() throws Exception {
mockServer.shutdown();
}
#Test
void testPostOAuth() throws Exception {
mockServer.enqueue(new MockResponse().setResponseCode(200).setBody("OAuthToken"));
String expected = serviceUtil.postOAuthToken();
assertThat(expected).isNotNull().isEqualTo("OAuthToken");
}
}
When I run above test I get this error
java.lang.NullPointerException:
At postAuthToken() while setting headers .headers(headers -> headers.setBasicAuth(secretHeaderValue))
Please help on how to mock whole thing
I am exposing a SOAP web service using Spring Boot. This web service is secured using Web Service Security (WSS) which is configured with this security_policy.xml:
<xwss:SecurityConfiguration
xmlns:xwss="http://java.sun.com/xml/ns/xwss/config">
<xwss:RequireUsernameToken
passwordDigestRequired="true" nonceRequired="true" />
</xwss:SecurityConfiguration>
Until this point, the application is working just fine. It is able to authenticate successfully.
Now, I need to add a specific HTTP header based on the WSS username. It is, adds the HTTP header "x-auth-type" with the values:
"test-auth-type" when the username is "test"
"production-auth-type" when the username is "production"
"undefined-auth-type" otherwise
I thought it was easy to add an EndpointInterceptor in which I can set the HTTP header based on the user, but is not been possible to me until now.
My Web Service Configuration class looks like this:
package com.godev.soapwebserviceswithspring;
import java.util.Collections;
import java.util.List;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.ws.config.annotation.EnableWs;
import org.springframework.ws.config.annotation.WsConfigurerAdapter;
import org.springframework.ws.server.EndpointInterceptor;
import org.springframework.ws.server.endpoint.interceptor.PayloadLoggingInterceptor;
import org.springframework.ws.soap.security.xwss.XwsSecurityInterceptor;
import org.springframework.ws.soap.security.xwss.callback.SimplePasswordValidationCallbackHandler;
import org.springframework.ws.soap.server.endpoint.interceptor.PayloadValidatingInterceptor;
import org.springframework.ws.transport.http.MessageDispatcherServlet;
import org.springframework.ws.wsdl.wsdl11.DefaultWsdl11Definition;
import org.springframework.xml.xsd.SimpleXsdSchema;
import org.springframework.xml.xsd.XsdSchema;
#EnableWs
#Configuration
public class WebServiceConfig extends WsConfigurerAdapter {
private static final String WS_SCHEMA_PATH = "godev_contract.xsd";
private static final String NAMESPACE_URI = "http://godev.com/soap/webservices/demo";
#Bean
public ServletRegistrationBean<MessageDispatcherServlet> messageDispatcherServlet(
ApplicationContext applicationContext) {
MessageDispatcherServlet servlet = new MessageDispatcherServlet();
servlet.setApplicationContext(applicationContext);
servlet.setTransformWsdlLocations(true);
return new ServletRegistrationBean<>(servlet, "/ws/*");
}
#Bean(name = "xml_message")
public DefaultWsdl11Definition defaultWsdl11Definition(XsdSchema billsSchema) {
DefaultWsdl11Definition wsdl11Definition = new DefaultWsdl11Definition();
wsdl11Definition.setPortTypeName("XmlMessagePort");
wsdl11Definition.setLocationUri("/ws");
wsdl11Definition.setTargetNamespace(NAMESPACE_URI);
wsdl11Definition.setSchema(billsSchema);
return wsdl11Definition;
}
#Bean
public XsdSchema countriesSchema() {
return new SimpleXsdSchema(new ClassPathResource(WS_SCHEMA_PATH));
}
#Bean
PayloadLoggingInterceptor payloadLoggingInterceptor() {
return new PayloadLoggingInterceptor();
}
#Bean
PayloadValidatingInterceptor payloadValidatingInterceptor() {
final PayloadValidatingInterceptor payloadValidatingInterceptor = new PayloadValidatingInterceptor();
payloadValidatingInterceptor.setSchema(new ClassPathResource(WS_SCHEMA_PATH));
return payloadValidatingInterceptor;
}
#Bean
XwsSecurityInterceptor securityInterceptor() {
XwsSecurityInterceptor securityInterceptor = new XwsSecurityInterceptor();
securityInterceptor.setCallbackHandler(callbackHandler());
securityInterceptor.setPolicyConfiguration(new ClassPathResource("security_policy.xml"));
return securityInterceptor;
}
#Bean
SimplePasswordValidationCallbackHandler callbackHandler() {
SimplePasswordValidationCallbackHandler callbackHandler = new SimplePasswordValidationCallbackHandler();
callbackHandler.setUsersMap(Collections.singletonMap("admin", "pwd123"));
return callbackHandler;
}
#Override
public void addInterceptors(List<EndpointInterceptor> interceptors) {
interceptors.add(payloadLoggingInterceptor());
interceptors.add(payloadValidatingInterceptor());
interceptors.add(securityInterceptor());
}
}
My Web Service Endpoint class looks like this:
package com.godev.soapwebserviceswithspring;
import org.springframework.ws.server.endpoint.annotation.Endpoint;
import org.springframework.ws.server.endpoint.annotation.PayloadRoot;
import org.springframework.ws.server.endpoint.annotation.RequestPayload;
import org.springframework.ws.server.endpoint.annotation.ResponsePayload;
import com.godev.soap.webservices.demo.GetXmlMessageRequest;
import com.godev.soap.webservices.demo.GetXmlMessageResponse;
#Endpoint
public class XmlMessageEndpoint {
private static final String NAMESPACE_URI = "http://godev.com/soap/webservices/demo";
#PayloadRoot(namespace = NAMESPACE_URI, localPart = "getXmlMessageRequest")
#ResponsePayload
public GetXmlMessageResponse getXmlDocument(#RequestPayload GetXmlMessageRequest request) {
GetXmlMessageResponse response = new GetXmlMessageResponse();
response.setXmlMessage("<xml>empty document</xml>");
return response;
}
}
Any advice will be very appreciated!
It works for me:
Inject the Security element present in the SOAP header in the Endpoint:
#Endpoint
public class XmlMessageEndpoint {
private static final String NAMESPACE_URI = "http://godev.com/soap/webservices/demo";
#PayloadRoot(namespace = NAMESPACE_URI, localPart = "getXmlMessageRequest")
#ResponsePayload
public GetXmlMessageResponse getXmlDocument(#RequestPayload GetXmlMessageRequest request, #SoapHeader("{" + Security.SECURITY_NAMESPACE + "}Security") SoapHeaderElement securityHeader) {
GetXmlMessageResponse response = new GetXmlMessageResponse();
response.setXmlMessage("<xml>empty document</xml>");
return response;
}
In order to parse the securityHeader into something usable, you need to define a couple of POJOs. In my case, I only need the username
POJO for Security element:
#XmlAccessorType(XmlAccessType.FIELD)
#XmlRootElement(namespace = Security.SECURITY_NAMESPACE, name = "Security")
#Getter
#Setter
public class Security {
public static final String SECURITY_NAMESPACE = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd";
#XmlElement(namespace = Security.SECURITY_NAMESPACE, name = "UsernameToken")
private UsernameToken usernameToken;
}
POJO for UsernameToken element:
#XmlAccessorType(XmlAccessType.FIELD)
#XmlRootElement(namespace = Security.SECURITY_NAMESPACE, name = "UsernameToken")
#Getter
#Setter
public class UsernameToken {
#XmlElement(namespace = Security.SECURITY_NAMESPACE, name = "Username")
private String username;
}
And finally, you can parse the securityHeader using something like this:
public class SoapParser {
public static Security parseSecurityElement(SoapHeaderElement soapHeaderElement) {
Security securityElement = null;
try {
JAXBContext context = JAXBContext.newInstance(Security.class);
Unmarshaller unmarshaller = context.createUnmarshaller();
securityElement = (Security) unmarshaller.unmarshal(soapHeaderElement.getSource());
} catch (JAXBException e) {
e.printStackTrace();
}
return securityElement;
}
}
I hope, it helps!
Even though the message is received by the MessageListener, I don't want to remove from the Queue, I want to do some processing in onMessage method and based on the result:
I want to commit(); for Success - so the message will be completely removed from the Queue.
For Failures - don't commit - rollback(); so the message will be redelivered (some times by default) and then goes to Dead letter Queue (DLQ). That’s OK for us.
I use: SpringBoot and hornetq (spring-boot-starter-hornetq-1.4.7.RELEASE).
Settings:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.env.Environment;
import org.springframework.jms.config.DefaultJmsListenerContainerFactory;
import org.springframework.jms.config.JmsListenerContainerFactory;
import org.springframework.jms.connection.UserCredentialsConnectionFactoryAdapter;
import org.springframework.jndi.JndiObjectFactoryBean;
import org.springframework.jndi.JndiTemplate;
import javax.jms.ConnectionFactory;
import javax.naming.Context;
import javax.naming.NamingException;
import java.util.Properties;
import static com.test.hornetq.Receiver.LOG;
import static javax.jms.Session.SESSION_TRANSACTED;
#Configuration
public class JmsConfig {
private String host;
private String port;
private String connectionFactoryJndiName;
private String jndiInit;
private String user;
private String password;
private String jmsReceiverConcurrency;
public JmsConfig(final Environment env) {
host = env.getProperty("host");
port = env.getProperty("port");
connectionFactoryJndiName = env.getProperty("connectionfactory.jndiname");
jndiInit = env.getProperty("jndiInit");
user = env.getProperty("user");
password = env.getProperty("password");
jmsReceiverConcurrency = env.getProperty("jmsReceiverConcurrency");
}
#Bean
public JndiTemplate jndiTemplate() {
final JndiTemplate jndiTemplate = new JndiTemplate();
jndiTemplate.setEnvironment(getProperties());
return jndiTemplate;
}
#Bean
public JndiObjectFactoryBean jmsConnectionFactory() throws NamingException {
final JndiObjectFactoryBean jndiObjectFactoryBean = new JndiObjectFactoryBean();
jndiObjectFactoryBean.setJndiTemplate(jndiTemplate());
jndiObjectFactoryBean.setJndiName(connectionFactoryJndiName);
jndiObjectFactoryBean.afterPropertiesSet();
return jndiObjectFactoryBean;
}
#Bean
#Primary
public ConnectionFactory connectionFactory() throws NamingException {
final UserCredentialsConnectionFactoryAdapter adapter = new UserCredentialsConnectionFactoryAdapter();
adapter.setTargetConnectionFactory((ConnectionFactory) jmsConnectionFactory().getObject());
adapter.setUsername(user);
adapter.setPassword(password);
return adapter;
}
#Bean
public JmsListenerContainerFactory<?> myJmsContainerFactory() throws NamingException {
final DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
factory.setConnectionFactory(connectionFactory());
factory.setSubscriptionDurable(false);
factory.setConcurrency(jmsReceiverConcurrency);
factory.setMaxMessagesPerTask(1);
factory.setSessionTransacted(true);
factory.setSessionAcknowledgeMode(SESSION_TRANSACTED);
factory.setErrorHandler(t -> {
LOG.error("Error in listener!", t);
});
return factory;
}
private Properties getProperties() {
final Properties jndiProps = new Properties();
jndiProps.setProperty(Context.INITIAL_CONTEXT_FACTORY, jndiInit);
jndiProps.setProperty(Context.PROVIDER_URL, "http-remoting://" + host + ":" + port);
jndiProps.setProperty(Context.SECURITY_PRINCIPAL, user);
jndiProps.setProperty(Context.SECURITY_CREDENTIALS, password);
return jndiProps;
}
}
And receiver:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jms.annotation.JmsListener;
import org.springframework.stereotype.Component;
import javax.jms.JMSException;
import javax.jms.MapMessage;
import javax.jms.Session;
#Component
public class Receiver {
#JmsListener(destination = "${destination.name}", containerFactory = "myJmsContainerFactory")
public void onReceive(final MapMessage message, Session session) throws JMSException {
try {
System.out.println(">>>> " + message);
session.rollback();
} catch (Exception ex) {
System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>THROW ");
throw ex;
}
}
}
But when I do rollback(); nothing happen and message doesn't comeback.
The code work. The problem was in hornetq settings in server side.
<pre-acknowledge>true</pre-acknowledge>
Extra Acknowledge Modes
Please note, that if you use pre-acknowledge mode, then you will lose transactional semantics for messages being consumed, since clearly they are being acknowledged first on the server, not when you commit the transaction. This may be stating the obvious but we like to be clear on these things to avoid confusion!
In what way can I avoid certificate validation in spring-boot-admin?
Link error image:
https://ibb.co/fkZu8y
I configure the RestTemplate for avoid the certificate in a class, but I do not know how to send it, I guess it must be in the client, the spring-boot-admin-starter-client works automatically.
This is the code for avoid the certificate validation.
public class SSLUtil {
public RestTemplate getRestTemplate() throws KeyStoreException, NoSuchAlgorithmException, KeyManagementException {
TrustStrategy acceptingTrustStrategy = new TrustStrategy() {
#Override
public boolean isTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
return true;
}
};
SSLContext sslContext = org.apache.http.ssl.SSLContexts.custom().loadTrustMaterial(null, acceptingTrustStrategy)
.build();
SSLConnectionSocketFactory csf = new SSLConnectionSocketFactory(sslContext, new NoopHostnameVerifier());
CloseableHttpClient httpClient = HttpClients.custom().setSSLSocketFactory(csf).build();
HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory();
requestFactory.setHttpClient(httpClient);
RestTemplate restTemplate = new RestTemplate(requestFactory);
return restTemplate;
}
}
Application.properties
spring.application.name=Admin-Application
server.port=1111
security.user.name=admin
security.user.password=admin123
#Configuration
public static class SecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
// Page with login form is served as /login.html and does a POST on
// /login
http.formLogin().loginPage("/login.html").loginProcessingUrl("/login").permitAll();
// The UI does a POST on /logout on logout
http.logout().logoutUrl("/logout");
// The ui currently doesn't support csrf
http.csrf().disable().authorizeRequests()
// Requests for the login page and the static assets are
// allowed
// http.authorizeRequests()
.antMatchers("/login.html", "/**/*.css", "/img/**", "/third-party/**").permitAll();
// ... and any other request needs to be authorized
http.authorizeRequests().antMatchers("/**").authenticated();
// Enable so that the clients can authenticate via HTTP basic for
// registering
http.httpBasic();
}
}
I'm using Spring Boot Admin 2.1.3 together with Eureka.
It seems SBA has moved from RestTemplate to WebClient. So I create a WebClient which has a SSLContext with a trust manager set to InsecureTrustManagerFactory, that trusts everything. Then I use this webclient and instantiate SBA's InstanceWebClient. Not sure if there is an easier approach, but this worked for me.
import de.codecentric.boot.admin.server.config.AdminServerProperties;
import de.codecentric.boot.admin.server.web.client.HttpHeadersProvider;
import de.codecentric.boot.admin.server.web.client.InstanceExchangeFilterFunction;
import de.codecentric.boot.admin.server.web.client.InstanceWebClient;
import io.netty.channel.ChannelOption;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
import io.netty.handler.timeout.ReadTimeoutHandler;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.netty.ConnectionObserver;
import reactor.netty.http.client.HttpClient;
import javax.net.ssl.SSLException;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
#Configuration
#EnableConfigurationProperties(AdminServerProperties.class)
public class SslConfiguration {
private final AdminServerProperties adminServerProperties;
public SslConfiguration(AdminServerProperties adminServerProperties) {
this.adminServerProperties = adminServerProperties;
}
#Bean
public InstanceWebClient instanceWebClient(HttpHeadersProvider httpHeadersProvider,
ObjectProvider<List<InstanceExchangeFilterFunction>> filtersProvider) throws SSLException {
List<InstanceExchangeFilterFunction> additionalFilters = filtersProvider.getIfAvailable(Collections::emptyList);
return InstanceWebClient.builder()
.defaultRetries(adminServerProperties.getMonitor().getDefaultRetries())
.retries(adminServerProperties.getMonitor().getRetries())
.httpHeadersProvider(httpHeadersProvider)
.webClient(getWebClient())
.filters(filters -> filters.addAll(additionalFilters))
.build();
}
private WebClient getWebClient() throws SSLException {
SslContext sslContext = SslContextBuilder
.forClient()
.trustManager(InsecureTrustManagerFactory.INSTANCE)
.build();
HttpClient httpClient = HttpClient.create()
.compress(true)
.secure(t -> t.sslContext(sslContext))
.tcpConfiguration(tcp -> tcp.bootstrap(bootstrap -> bootstrap.option(
ChannelOption.CONNECT_TIMEOUT_MILLIS,
(int) adminServerProperties.getMonitor().getConnectTimeout().toMillis()
)).observe((connection, newState) -> {
if (ConnectionObserver.State.CONNECTED.equals(newState)) {
connection.addHandlerLast(new ReadTimeoutHandler(adminServerProperties.getMonitor().getReadTimeout().toMillis(),
TimeUnit.MILLISECONDS
));
}
}));
ReactorClientHttpConnector reactorClientHttpConnector = new ReactorClientHttpConnector(httpClient);
return WebClient.builder().clientConnector(reactorClientHttpConnector).build();
}
}
To disable the SBA Admin server from validating SSL certs from the clients it tries to connect to, you can use the following:
For SBA version 2.6.2 it is more or less outlined right from their documentation: https://codecentric.github.io/spring-boot-admin/current/#_using_mutual_tls
Here is the complete configuration overriding bean:
package com.markham.mkmappadmin.config;
import javax.net.ssl.SSLException;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.reactive.ClientHttpConnector;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
import reactor.netty.http.client.HttpClient;
/**
* Custom http client class which overrides Spring Boot Admin's server default client.<br>
* The custom client will bypass any SSL Validation by configuring an instance of
* {#link InsecureTrustManagerFactory}
* #author Hanif Rajabali
* #see Spring Boot Admin 2.6.2 Using Mutual TLS
*/
#Configuration
public class CustomHttpClientConfig {
#Bean
public ClientHttpConnector customHttpClient() throws SSLException {
SslContext sslContext = SslContextBuilder
.forClient()
.trustManager(InsecureTrustManagerFactory.INSTANCE)
.build();
HttpClient httpClient = HttpClient.create().secure(
ssl -> ssl.sslContext(sslContext)
);
return new ReactorClientHttpConnector(httpClient);
}
}
What I still haven't figured out is how to disable it from the SBA client. I have a custom RestTemplate Config defined below, but the SBA client doesn't seem to be picking it up even though I see that the SBA client code is using the BlockingRegistrationClient i.e) RestTemplate
package com.markham.mkmemailerws.config;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.X509Certificate;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
/**
* Need to explicitly build Spring Boot's auto configured
* {#link #restTemplate(RestTemplateBuilder)}
*
* #author Hanif Rajabali
*
*/
#Configuration
public class RestTemplateConfig {
// #Bean
// public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) {
// return restTemplateBuilder.build();
// }
/**
* The following will bypass ssl validation altogether. Not ideal.
*/
#Bean
public RestTemplate restTemplate(RestTemplateBuilder builder)
throws NoSuchAlgorithmException, KeyManagementException {
TrustManager[] trustAllCerts = new TrustManager[] { new X509TrustManager() {
public java.security.cert.X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
public void checkClientTrusted(java.security.cert.X509Certificate[] certs, String authType) {
}
public void checkServerTrusted(java.security.cert.X509Certificate[] certs, String authType) {
}
} };
SSLContext sslContext = SSLContext.getInstance("SSL");
sslContext.init(null, trustAllCerts, new java.security.SecureRandom());
CloseableHttpClient httpClient = HttpClients.custom().setSSLContext(sslContext)
.setSSLHostnameVerifier(NoopHostnameVerifier.INSTANCE).build();
HttpComponentsClientHttpRequestFactory customRequestFactory = new HttpComponentsClientHttpRequestFactory();
customRequestFactory.setHttpClient(httpClient);
return builder.requestFactory(() -> customRequestFactory).build();
}
}
Try http.csrf().disable().authorizeRequests()
Above code will disable csrf token. Below is my code for OAuth where I disabled csrf to reduce complexity.
#RestController
#EnableOAuth2Sso
#EnableResourceServer
#SpringBootApplication
public class SpringBootWebApplication extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().authorizeRequests()
.antMatchers("/api/**", "/dashboard", "/welcome","/about").authenticated().antMatchers("/**").permitAll()
.anyRequest().authenticated().and().logout().logoutSuccessUrl("/").permitAll();
}